JVM 基本知识
简介
JVM 内存模型
JVM 类加载
JVM 垃圾回收
算法
- 标记-清理:分两步:标记要回收的对象,回收被标记的对象的空间
- 复制:内存空间一分为二,每次只使用其中一块,当其中一块用完了,将存活的对象复制到另一块,然后把原先的一块清理掉。
- 标记-整理:先标记要回收的对象,移动存活对象到一端,清除端界之外的对象
-
分代收集:新生代,老年代
-
-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
- -XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要
- -XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小
- -XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1
- -XX:PermSize
设置非堆内存初始值,默认是物理内存的1/64;
- XX:MaxPermSize
设置最大非堆内存的大小,默认是物理内存的1/4;
- -Xmx4096m
最大可用堆内存
- -Xms2096m
最小使用堆内存
线程
先来简单了解下什么是线程: * 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
- 为了充分利用cpu资源,提升性能,在软件开发中采用了多线程并行完成任务的处理。
Java启动线程方法
继承Thread类
- 重写run方法
- 使用start启动线程
- 每次创建对象,性能差
- 缺乏统一管理
- 缺乏定时执行,定期执行
实现Runnable接口
线程池
为什么要用线程池
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
Java 线程池
-
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService
-
newCachedThreadPool
- newFixedThreadPool
- newScheduledThreadPool
- newSingleThreadExecutor
newCachedThreadPool
-
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
-
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程
- 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache
newFixedThreadPool
- 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool
- 创建一个定长线程池,支持定时及周期性任务执行
newSingleThreadExecutor
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
锁
- 锁-是为了解决并发操作引起的脏读、数据不一致的问题
在了解锁之前先简单介绍下CAS:
- CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。
- CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。
-
JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。
-
优点:
竞争不大的时候系统开销小。
- 缺点:
循环时间长开销大。
ABA问题。
只能保证一个共享变量的原子操作。
在java中有了并发,就有了锁,大概有如下几种类型:
- 独享锁
- 共享锁
- 互斥锁
- 读写锁
- 公平锁
- 非公平锁
- 可重入锁
- 乐观锁
- 悲观锁
- 分段锁
- 自旋锁
- 偏向锁
- 轻量级锁
-
重量级锁
-
锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。
-
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO双向队列来完成获取锁线程的排队工作。
非公平锁
- 当锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁。
公平锁
- 当锁处于无线程占有的状态,在其他线程抢占该锁的时候,都需要先进入队列中等待。
偏向锁
-
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁
-
线程1检查对象头中的Mark Word中是否存储了线程1,
- 如果没有,则CAS操作将Mark Word中的线程ID替换为线程1。
- 此时,锁偏向线程1,后面该线程进入同步块时不需要进行CAS操作,只需要简单的测试一下Mark Word中是否存储指向当前线程的偏向锁;
- 如果成功表明该线程已经获得锁。
- 如果失败,则再需要测试一下Mark Word中偏向锁标识是否设置为1(是否是偏向锁),
- 如果没有设置,则使用CAS竞争锁,
-
如果设置了,则尝试使用CAS将偏向锁指向当前线程
-
偏向锁在Java6和Java7中默认是开启的,但是在应用程序启动几秒后才激活,如果有必要可以关闭延迟:
-XX:BiasedLockingStartupDelay=0
- 如果确定应用程序中所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:
-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁。
- 两个参数合并使用
-XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking
- 偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁
轻量级锁和自旋锁
-
轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作
-
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁
-
如果偏向锁失败,Java虚拟机就会让线程申请轻量级锁,轻量级锁在虚拟机内部,使用一个成为BasicObjectLock的对象实现的,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象放置在Java栈帧中。在BasicLock对象内部还维护着displaced_header字段,用于备份对象头部的Mark Word.
乐观锁
- 偏向锁,轻量级锁,自旋锁都是乐观锁
可重入锁
- 可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取
- ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁
分段锁
- 分段锁是一种锁的设计,并不是一种具体的锁。对于ConcuttentHashMap就是通过分段锁实现高效的并发操作
自旋锁
-
自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。
-
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
-
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源
重量级锁
- 当轻量级锁失败,虚拟机就会使用重量级锁
Java中具体锁
- ReentrantLock:互斥锁,独享锁
- ReadWriteLock:读写锁,共享锁,
- ReentrantReadWriteLock
- Synchronized:非公平锁,悲观锁
- CountDownLatch:共享锁
ReentrantLock
- ReentrantLock = 一个AQS同步器(维护同步状态) + 一个AQS同步队列 + 多个Condition等待队列
- new ReentrantLock(); 此种创建方式会创建出一个非公平锁。
- new ReentrantLock(true); 此种方式会创建出一个公平锁。
ReentrantReadWriteLock
- new ReentrantReadWriteLock();
常用方法:
- readLock().lock();写锁
- writeLock().lock();读锁
- readLock().unlock();解锁
-
writeLock().unlock();解锁
-
如果目前是读锁,其他读锁也可以进请求,写锁不能进。
- 如果目前是写锁,那么其他所有的锁都不可以进。
- 适用于读多写少的情况,如果是写多读少用ReentrantLock。
Synchronized
- synchronized是基于Monitor来实现同步的
- Monitor 的工作机理
* 线程进入同步方法中。
* 为了继续执行临界区代码,线程必须获取 Monitor 锁。
* 如果获取锁成功,将成为该监视者对象的拥有者。
* 任一时刻内,监视者对象只属于一个活动线程(The Owner)
* 拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
* 其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,
* 这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
* 同步方法执行完毕了,线程退出临界区,并释放监视锁。