参考
JVM 垃圾回收(1)《根对象/四种引用》
JVM 垃圾回收(2)《垃圾回收算法/分代回收》
垃圾回收器
串行的垃圾回收器
- 底层是单线程的垃圾回收器,即垃圾回收发生时,把其他的线程都暂停,这时候一个单线程的垃圾回收器就登场了,即一个线程来完成
垃圾回收。 - 适用场景是堆内存较小时,而且CPU核数多了也没用,因为你只有一个线程,适合个人电脑(CPU核数少的工作环境)。
这就好比有一个居民楼,有一个保洁工人打扫卫生,这一个保洁工人类似于单线程的垃圾回收器,如果这个楼层比较矮,那保洁工人可能一天就能把卫生打扫完了,但如果楼层特别高,那一个保洁工人来打扫那这个工作量可能是非常大的,干好几天都干不完
。
开启串行的垃圾回收器的JVM参数:
-XX:+UseSerialGC = Serial + SerialOld
串行垃圾回收器分为两个部分,一个是Serial
,还有一个是SerialOld
。Serial
是工作在新生代,他采用的回收算法是复制
。而SerialOld
是工作在老年代,他采用的回收算法是标记+整理
算法。即这个JVM参数开启了串行的垃圾回收器,包括了新生代的和老年代的两个垃圾回收器,其实新生代的和老年代的垃圾回收器是分别运行的,比如新生代内存不足他使用Serial
来完成垃圾回收工作,等到老年代触发垃圾回收时,Serial
来完成minor GC,SerialOld
来完成老年代的fullGC。
(图片取自黑马教程,以下同)
串行垃圾回收器工作模式
假设我现在有多核CPU,比如CPU0 ~ CPU3。刚开始这些线程(CPU0~CPU3)都在运行,这时候发现堆内存可能不够了,不够了就会触发一次垃圾回收,在触发了垃圾回收的时候,首先要让这些线程在安全点停下来,这是因为在垃圾回收的过程中,可能这个对象的地址要发生改变,为了保证我安全的使用这些对象地址,那需要把所有的用户正在工作的线程都到达这个安全点暂停下来,这时候我来完成垃圾回收的工作,就不会有其他线程来干扰我了。否则的话如果我正在移动一个对象,他的地址改了,那其他的线程来找这个对象,他这个就可能找到错误的地址,从而发生问题。这里需要注意的是,Serial
还是SerialOld
都是单线程的垃圾回收器,因此他只有一个垃圾回收线程在运行,那在这个垃圾回收线程运行的同时呢,其他的用户线程都要阻塞,也就是暂停,等待垃圾回收线程的结束,等到垃圾
回收线程结束以后呢,那我们其他的用户线程再恢复运行。
该图既适合我们的Serial
,也适合SerialOld
,他们工作的方式都是单线程,并且都是要执行这个stw操作。只不过他们用的回收算法有所不同。
网友1:不是单线程吗?
网友2:应用程序是可以多线程啊。
网友3:回收器是单线程的,又不表示程序是单线程的。
网友4:单线程指的是垃圾回收是单线程。
吞吐量优先的垃圾回收器
- 多线程
好比上面的比喻例子中,如果楼层很高,但可以多找几个保洁工人,他们每个人打扫N层,可以规定时间内完成垃圾回收的任务
。 - 适用于堆内存较大的场景。一般需要多核的CPU来支持,因为如果有多个线程,但假设只有一个CPU,那么他也是工作时多个线程轮流去争抢单核CPU的时间片,所以这个效率还不如单线程呢。显然多核CPU是服务器电脑,即适合工作在服务器上(
网友1:我电脑4核
)。好比多个保洁工人打扫卫生,但是扫帚只有一把,那打扫卫生必须轮流使用这把扫帚,这个效率显然跟你一个人来打扫是一样的
。
可以使用 -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
这两个参数开启,其实1.8里默认开启了,即1.8里默认使用的是并行的垃圾回收器。前面的UseParallelGC
代表新生代的垃圾回收器,采用的是复制
算法,后面的UseParallelOldGC
是工作在老年代的垃圾回收器,采用的是标记加整理
的算法。(Parallel就是并行的意思。这两个开关有意思的是,只要开其中一个,另外一个也会连带开启。
)
假设有多核CPU,有四个线程都在跑,这时候假如内存不足了,触发了一次垃圾回收,那么这时候这些用户线程就会跑到安全点停下来,停下来以后,垃圾回收器会开启多个线程来进行垃圾回收,多个回收线程一拥而上,大家一块儿尽快把垃圾清理掉,这里垃圾回收线程的个数默认情况下跟你的CPU核数相关,只要你的CPU核数小于一个值,他就会和你的CPU核数一模一样,比如现在4核CPU,那将来就开四个垃圾回收线程来完成回收工作,回收结束以后,再恢复其他用户线程的运行。
当然这个线程数可以通过参数 -XX:ParallelGCThreads=n
来控制,即可以指定ParallelGC
线程运行数。ParallelGC
的特点是根据一个目标
设置ParallelGC
的工作方式,与之相关的参数比如有三个,-XX:+UseAdaptiveSizePolicy
这是采用一个自适应的大小调整策略
,调整的主要是新生代的大小,新生代分为伊甸园和两个生存区即from和to,如果把UseAdaptiveSizePolicy
开关打开,那么在ParallelGC
工作的时候他就会动态的去调整伊甸园和生存区的比例,包括整个堆的大小他都会进行调整,包括晋升阈值也会受这个开关的影响(这只是开关
)。
除了这个开关以外,还有两个目标,第一个目标是 -XX:GCTimeRatio=ratio
,第二个目标是-XX:MaxGCPauseMillis=ms
,即ParallelGC
比较只能,他可以根据你的一个设定目标来尝试去调整堆的大小来达到你期望的那个目标,比如GCTimeRatio
是主要是调整吞吐量的目标,即调整垃圾回收时间和总时间的占比,比如有个公式 1/1+ratio(网友1:注意,应该是“1/(1+ratio)”
),ratio的默认值是99,那么1/(1+ratio)就等于0.01,意思是你垃圾回收的时间不能超过总时间的1/100,换句话说如果你工作了100分钟,那么100分钟内只有1分钟的时间能用于垃圾回收,如果达不到这个目标,那么ParallelGC
回收器就会尝试去调整堆的大小来达到这个目标,一般是给堆增大,因为增大了以后垃圾回收的次数就变的不频繁了,这样就可以达到我们垃圾回收的总时间下降从而达到吞吐量的提高。
第二个目标是MaxGCPauseMillis
,PauseMillis是暂停的毫秒数,前面有个Max所以是最大暂停毫秒数,他的默认值是200毫秒,这里需要注意的是,这两个目标其实是冲突的,因为你调整了GCTimeRatio
即可能会让堆要变大,因为一般情况下会把堆调大,堆调大了,吞吐量就会提升了,但是堆变大了,那么每一次垃圾回收所花费的时间可能就会增长,那么没办法达到这个每一次垃圾回收的暂停时间达到下面的指标了(比如200毫秒
),反正你要让这个暂停时间变的短,那意味着你要让堆的空间变小,这样垃圾回收的时间就会变短,但这样的话吞吐量又下来了,所以这两个目标,要取一个折中,即根据你的应用的实际情况去合理的指定,他俩是一个对立的目标。一般ratio的默认值是99,但是这个99很难达到,所以一般这个值设为19,也就是1/(1+19),等于0.05,即比如100分钟内,允许5分钟的一个暂停,即垃圾回收时间,这个还是比较容易达到的。
所谓吞吐量就是 CPU用于运行用户代码的时间与 CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉了1分钟,那吞吐量就是99%。那么如果我们想要提高吞吐量,就应该尽量减少垃圾回收时间,那就尽可能少的运行 GC的次数,但是问题来了,这就跟你倒垃圾一样,你如果很久不倒垃圾,那垃圾不就是越堆越多&#x