垃圾回收饮湖上初晴后雨 苏轼
水光潋滟晴方好,山色空蒙雨亦奇。
欲把西湖比西子,淡妆浓抹总相宜。
- 一、如何判断对象是垃圾
- 1.1 引用计数法
- 1.2 可达性分析算法
- 二、垃圾回收算法
- 2.1 标记清除算法
- 2.2 标记整理算法
- 2.3 复制算法
- 三、分代垃圾回收
- 3.1 回收流程
- 四、垃圾回收器
- 4.1 串行回收器
- 4.1.1 简介
- 4.1.2 工作方式
- 4.2 吞吐量优先回收器
- 4.3 响应时间优先回收器
- 4.3.1 定义
- 4.3.2 CMS和PS的区别
- 4.3.3 工作流程
- 4.3.4 相关参数
- 4.4 G1
- 4.4.1 定义
- 4.4.2 工作流程
- 1)Young Collection
- 2)Young Collection + Concurrent Mark
- 3)Mixed Collection
- 4.4.3 Young Collection跨代引用
- 4.4.4 重新标记
之前,我们已经聊完了JVM内存结构,现在我们开始垃圾回收模块。相信熟悉Java的同学一定会经常听说垃圾回收这个词,可能也知道这是Java语言的一大特色(相比于C语言,Java语言会自动进行垃圾回收,这样开发者就可以不用关心内存的是否释放,大大减轻了自己的工作量,同时也能避免因忘记释放内存导致内存泄漏的情况发生。),但是并不是所有人都明白垃圾回收的工作原理。
下面,我们将一起揭开垃圾回收的面纱,弄清楚它的工作机制。
不要误会哈
1.1 引用计数法定义:对于一个对象,每当有一个引用指向它,计数加一,每当一个指向它的引用失效时,计数减一。计数为0的对象将被视为垃圾被回收。
缺点:循环引用。下面A对象和B对象的计数都是1,虽然它们已经不能被使用(外界拿不到它们了),但由于计数不为0,所以也不会被当成垃圾进行回收,就会造成内存泄漏问题。
定义:根对象不可达的对象即是垃圾。
根对象:GC Root,是JVM确定当前绝对不会被回收的对象。
根可达:对象到GC Roots存在直接或间接的引用关系。
在下图中,A对象是根对象,它直接引用B对象。B对象引用C对象和D对象,也就相当于A对象间接引用这C对象和D对象。所以B,C,D都是根可达的。E显而易见是根不可达的,所以E对象就是垃圾,需要回收它占用的内存。
JVM中的垃圾回收器就是采用可达性分析来探索所有存活的对象。JVM通过扫描对象中的对象,看是否能够沿着以GC Root对象为起点的引用链找到该对象,找不到,则说明此对象是垃圾对象,需要回收。
有同学可能会好奇GC Root到底是哪些对象,这些我们将在后面的小模块中说明。
找到了什么是垃圾,现在我们就需要关系怎么去回收了。JVM中垃圾回收有3中经典的方式,分别是标记清除(Mark Sweep),标记整理(Mark Compact)和复制(Copy)。下面我将一一说明。
2.1 标记清除算法 定义:首先回收器会从根节点开始通过引用遍历堆中对象,并标记所有根可达的对象。之后回收器会从头到尾遍历堆中对象,回收掉未被标记的对象。
优点:速度块
缺点:容易产生内存碎片。如下图,因为垃圾对象在堆中很可能不是连续分布,所以这种算法回收的内存也会是一块隔着一块的。回收的内存块有的可能太小以至于根本用不了,就产生了内存碎片。
定义:标记垃圾的机制和上面一样,也是以根对象,通过引用遍历堆中对象并标记。回收器第二次遍历的时候会在直接让存活对象往前移,在这个过程中,垃圾对象占用的空间会直接被覆盖,也就是了回收其内存。如下图。
优点:没有内存碎片。
缺点:因为要移动对象,所以速度很慢
定义:将内存分为From区和To区,回收器会将有用的对象从From区复制到To区,这样From区就整体回收掉。然后再交换From和To的位置,To区域总是空的。如下图。
优点:不会产生碎片
缺点:会占用双倍的内存空间
在实际的垃圾回收中,JVM不会固定地只使用某一种算法,而是结合三种算法,协同工作,这就是我们下面要聊的分代垃圾回收。
首先我们来看看JVM堆的结构,JVM把堆内存划分成了两大块:新生代和老年代。新生代又划分成三个小区域:伊甸园、幸存区From和幸存区To。长时间使用的对象就放在老年代中,用完了就可以丢弃的对象放在新生代中。这样就可以根据区域来使用不同的垃圾回收策略。老年代垃圾回收就很久发生一次,新生代就比较频繁。
-
当新建一个对象时,会将其放在伊甸园中。
-
当新建的对象越来越多,伊甸园的空间已经被占满,放不了下一个对象时,会触发一次Minor GC。Minor GC会用根可达算法找到伊甸园中活着的对象,再用复制算法将这些对象放入幸存区To中,并将这些对象的寿命加一。最后再交换From区和To区的位置(也可以理解为改个名字,要保证To区始终是空的,这点上面已经说过了)。Minor GC会引发Stop the World(垃圾回收线程之外的用户线程在Minor GC时要暂停,直到复制完毕,才能继续运行,后面以STW代替)。
-
当下一次伊甸园又被沾满时,会触发第二次Minor GC。这时会扫描伊甸园和From区,并且把这些存活的对象放到To区中,寿命加一。然后将伊甸园和From区中的垃圾对象回收掉。最后再交换From区和To区的位置。
-
当一个对象经过15次GC还依旧存活,就会将其晋升到老年代。(当然这个次数是不固定)
-
当老年代空间不足,会先尝试触发Minor GC,如果回收之后空间仍不足,就触发一次Full GC,它也会引起STW,并且时间更长。这是因为新生代采用的是复制算法,而老年代采用的是标记加整理,速度很慢。如果Full GC之后仍无足够空间,就会触发OOM。
串行垃圾回收器分为Serial回收器和Serial Old回收器,前者工作在新生代,后者工作在老年代。
Serial和Serial Old都是单线程运行,即垃圾回收时只会有一个垃圾回收线程在工作。
Serial采用复制算法回收对象,Serial Old则采用标记整理法。
下面是打开串行回收器开关的参数。
-XX:+UseSerialGC4.1.2 工作方式
两者虽然采用的回收算法不同,但是工作方式一致。如下图,当前有4个用户线程在运行。某一时刻,堆内存不够了,需要垃圾回收。因为垃圾回收涉及对象地址的转移,所以在回收之前,需要让这些线程在一个安全点阻塞住。
由于这两个回收器都是单线程时,所以当垃圾回收时,只会有一个垃圾回收线程在运行。用户线程在此期间会一致阻塞,即STW,等代垃圾回收结束。
吞吐量:在一个给定的时间段内,用户线程运行的时间所占的百分比(在这段时间内,GC运行也要占用CPU的运行时间),也就是说GC占比越小,吞吐量越大。
Parallel Scavenge也叫做吞吐量优先回收器,它是多线程回收器,工作在新生代,采用复制算法回收垃圾。
和PS配合工作的收集器是Parallel Old,它也是多线程回收器,工作在老年代,采用标记整理算法回收垃圾。
在垃圾回收时,用户线程在安全点阻塞。PS会开启多个回收线程来进行垃圾回收,回收线程个数和CPU核数相关,一般和CPU核数一样。
下面是吞吐量优先回收器的开关:
开启一个,另一个会默认开启
-XX:+UseParallelGC -XX:+UseParallelOldGC
同时PS还有一个自适应策略开关,打开之后即表示采用自适应的大小调整策略。当垃圾回收时,会动态的调整新生代伊甸园和幸存区的比例,晋升阈值以及堆的大小。
-XX:+UseAdaptiveSizePolicy 自适应策略开关 -XX:ParallelGCThreads=n 控制线程数
自适应策略通过下面两个参数来同台调整堆的大小,可以将这个两个参数理解成两个要达成的目标。
GCTimeRatio用于调整垃圾回收时间和总时间的占比,公式为:1/(1+ratio)。ratio默认值为99。即要保证垃圾回收时间占总时间的比例不超过这个值。如果达不到,就会动态调整。一般是把堆调大,减少垃圾回收次数。但是ratio=99很难达到,一般设置为19,也就说程序运行20min中,垃圾回收的时间不能超过1min。
MaxGCPauseMills用于控制最大暂停毫秒数(STW时间),默认值是200。即要保证由GC引起的程序暂停时间要少于200ms。要让暂停时间短,就要减少堆的大小,这样扫描的对象会比较少。
这两个目标是冲突的,要想垃圾回收时间占比少,就要减少垃圾回收次数,就需要增加堆内存,但是堆内存增加后,垃圾回收时要处理的对象也变多了,最大暂停毫秒数也会增加。
-XX:+GCTimeRadio=ratio -XX:+MaxGCPauseMills=ms4.3 响应时间优先回收器 4.3.1 定义
响应时间:STW时间。
CMS(ConcurrentMarkSweepGC)也叫做响应时间优先回收器。从它的名字可以了解到它是并发的、采用标记清除算法的一种回收器。
CMS工作在老年代,和它配合的是工作在新生代的ParNew回收器。因为是并发回收器,当并发失败的时候,CMS会退化为SerialOld回收器。
注:ParNew回收器是串行回收器Serial的多线程版本。
注:并发失败是指当新生代进行垃圾回收时,老年代没有空间来存晋升的对象,就会STW,触发Full GC。
这里要和PS回收器做个区分,PS是并行的,指的是会有有多个垃圾回收线程同时运行,但在垃圾回收期间,是不允许用户线程运行的。CMS是并发的,这个并发是指在垃圾回收期间是允许垃圾回收线程和用户线程同时运行的。
4.3.3 工作流程如下图。
- 一开始有四个用户用户线程正在运行,某一时刻老年代空间不足,CMS开始工作。
- 首先让用户线程在安全点阻塞,垃圾回收线程进行初始标记(只标记根对象,速度很快),这一阶段需要STW,但暂停时间很短。
- 初始标记完成之后,用户线程可以继续执行,同时回收线程并发地根据根对象找出存活对象。
- 并发标记完成之后再重新标记一次,因为上一阶段可能产生了对象引用的变化。
- 重新标记结束之后,用户线程恢复运行,垃圾线程开始清理。
在这种工作机制下,只会在初始标记阶段让用户进程暂停,可以大大提高响应时间。
- 回收器开关
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC
- 设置STW工作线程数,一般最好和CPU核心数量相当
-XX:ParallelGCThreads=n
- 设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右
-XX:ConcGCThreads=n
- 控制垃圾回收时机。因为在CMS并发清理的时候,用户线程可能还会产生垃圾,这些垃圾只能等到下一次垃圾回收时清理。所以不能等到堆完全被占满的时候再垃圾回收,要预留一些空间给垃圾回收过程中产生的新的垃圾。
这个值设置的就是当堆占用率达到多少时垃圾回收。
-XX:CMSInitiatingOccupanyFraction=percent
- 是否在重新标记前先对新生代做一次GC。重新标记是从GC Root开始扫描,而新生代和老年代的对象之间可能存在跨代引用,所以这里扫描的对象是全堆。因为新生代的对象大多数是朝生夕死,先对新生代做一次GC,可以减少扫描对象的数量。
-XX:+CMSScavengeBeforeRemark4.4 G1 4.4.1 定义
Garbage First回收器简称G1,它是一个分代的,采用增量式垃圾回收,整体上是标记整理的一种回收器,适用于超大堆内存,同时注重吞吐量和低延迟。
增量式回收:它并不会等GC执行完,才将控制权交回程序,,而是一步一步执行,跑一点,再跑一点,,逐步完成垃圾回收,在程序运行中穿插进行。极大地降低了GC的最大暂停时间。
首先是新生代收集,当老年代空间不足时,会进行新生代收集和并发标记,然后进行一次混合收集,一直这样循环。如下图。
G1会把堆划分成一个个大小相等的区域,每个区域都可以独立作为伊甸园,幸存区和老年代。
如下图,空白格表示空闲区域,E表示伊甸园。当伊甸园区域逐渐被占满,就会触发一次新生代的垃圾回收,并且触发STW。
回收时,JVM采用复制算法将伊甸园中对象复制到幸存区S中。
当幸存区的空间也很小时,幸存区中够年龄的对象会晋升到老年代,不够年龄的会复制到另一个幸存区。O表示老年代。不够晋升的会复制到另一个幸存区。
首先会在YGC时对GC Root进行初始标记,当老年代占用堆空间达到一个阈值时,会进行并发标记,即从GC Root出发去标记其他对象。
阈值可以通过下面的参数控制:
控制老年占堆的比的阈值,默认是45% -XX:InitiatingHeapOccupancyPercent=percent3)Mixed Collection
在混合收集阶段,会对E,S,O进行全方面的垃圾回收。
&emspE中幸存对象会复制到S区;S区中不够年龄的也会复制到另一个S区,够年龄的会复制到O区;
对于老年代,也是复制算法,但是由于老年代的空间比较大,如果全复制会导致完不成设置的目标,这是就会选择最有价值的空间(回收后释放的空间最多)来进行回收,这样复制对象少了,就可以完成暂停时间的目标。如果暂停时间的目标可以达到,那么就会复制所有的对象。
暂停时间目标
-XX:MaxGCPauseMills=ms 设置STW时间,是垃圾回收的目标4.4.3 Young Collection跨代引用
YGC首先找到新生代中对象的根对象,然后利用根可达找到有效对象,最后将有效对象复制到幸存区。
问题在第一步找根对象,根对象有一部分是在老年代,而老年代中对象很多,如果全扫描,效率很低。G1使用了卡表的技术,将老年代进一步细分,分成一个个card,每一个card大约是512k。
如果有一个card中的对象引用了新生代中的对象(那么这个对象就是根对象),就把这个card标记为脏卡。这样在找GC Root的时候就只用扫描脏卡中的对象即可,避免扫描全堆。
下图中灰色的区域就是脏卡。
我们已经知道在初始标记时,会STW,并标记除GC Root。在并发标记时,会根据这些根对象找到存活对象。而在并发标记之后还要进行一次重新标记,这是为什么呢?
这是因为在并发标记阶段,用户线程也在运行,所以就有可能导致引用的变化。一个对象在被标记成存活对象后,被用户线程舍弃了(变成垃圾),实际上它应该被回收,但是如果没有重新标记阶段,它就会被当成是存活对象,而不会被回收。重新标记就是对对象做进一步的检查。
重新标记的实现:
在并发标记过程中,如果对象的引用发生变化,JVM就会将其放入一个队列中,等到重新标记的时候,把队列中的对象再检查一遍,看是否有强引用指向这些对象,并进行最终的标记。
希望这篇文章能帮助到你。



