JVM:02.垃圾回收
一、如何判断对象可以回收
1. 引用计数法
被引用就+1,不被引用就-1,清零就回收。但不能解决循环引用的问题
2. 可达性分析算法
JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象。扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
可以作为 GC Root 的对象
- 栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中本地方法引用的对象
查看GC Root 可以使用Eclipse Memory Analyzer 工具进行分析,略过
3. 四种引用
3.1 强引用
普通变量赋值即为强引用,如 A a = new A();
通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
3.2 软引用(SoftReference)
例如:SoftReference a = new SoftReference(new A());
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
软引用自身想释放时需要配合引用队列来释放
典型例子是反射数据
3.3 弱引用
例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放
典型例子是 ThreadLocalMap 中的 Entry 对象
3.4 虚引用
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存
3.5 终结器引用
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。
二、垃圾回收及其算法
1.垃圾回收
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
- 回收区域是堆内存,不包括虚拟机栈
- 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
- GC 具体的实现称为垃圾回收器
- GC 大都采用了分代回收思想
- 根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC
2. 垃圾回收算法
1. 标记清除法
找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象。
- 标记阶段:沿着 GC Root 对象的引用链找,给直接或间接引用到的对象加上标记。标记速度与存活对象线性关系
- 清除阶段:释放未加标记的对象占用的内存。清除速度与内存大小线性关系
优点:速度较快
缺点:会产生内存碎片
2. 标记整理法
- 标记阶段、清理阶段与标记清除法类似。标记速度与存活对象线性关系
- 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生。清除与整理速度与内存大小成线性关系
优点:避免内存碎片产生
缺点:性能上较慢
3. 标记复制法
将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
- 标记阶段与前面的算法类似
- 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
- 复制完成后,交换 from 和 to 的位置即可
优点:避免内存碎片产生
缺点:占用成倍的空间
三种垃圾回收算法JVM会根据不同的情况使用,协同工作。新生代采用标记复制法、老年代一般采用标记整理法
标记的方法:三色标记法
- 黑色 – 已标记
- 灰色 – 标记中
- 白色 – 还未标记
起始的三个对象还未处理完成,用灰色表示,该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色,依次类推,沿着引用链都标记了一遍,最后为标记的白色对象,即为垃圾
并发漏标问题,暂时略过(黑马-面试专题-JVM)
三、分代垃圾回收
1. 新生代与老年代
Java会将堆内存等分为两块:新生代和老年代。
大部分对象朝生夕灭,用完立刻就可以回收,存放于新生代,回收的比较频繁。
另有少部分对象会长时间存活,每次很难回收,存放于老年代,回收不太频繁
新生代又细分为:伊甸园(eden)、幸存区From、幸存区To。
新创建的对象首先分配在eden,eden空间不足时,触发垃圾回收(新生代的垃圾回收一般称为 minor gc ),eden中存活的对象复制到幸存区To中,存活的对象年龄加一,然后交换 from to
minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行
接着继续往eden中添加对象,eden空间不足时,再次触发垃圾回收,eden和幸存区From中幸存的对象复制到幸存区To中,存活的对象年龄再加一,然后交换 from to (交换之后的幸存区To都是空的)。接着再向eden里添加新元素……
当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(对象头中4bit用来存放,所以最大就15)。有的情况是内存实在有限,没加几次就直接晋升老年代了
- 还有一种现象是大对象直接晋升老年代,判断了eden里放不下,就去老年代看看能不能放下,能放下就直接放在老年代
当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长!full GC 之后如果空间还是不足,则触发OOM
2. GC相关JVM参数
相关JVM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
打印GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
3. GC分析
1 |
|
通过上面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。
4. GC规模
- Minor GC 发生在新生代的垃圾回收,暂停时间短
- Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1回收器特有(后面会讲)
- Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
如果是新的子线程里的内存溢出,并不会影响主线程或者其他线程的正常运行
四、垃圾回收器
部分相关概念:
- 并行回收:指多条垃圾回收线程并行工作,但此时用户线程仍处于等待状态。
- 并发回收:指用户线程与垃圾回收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
- 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
垃圾回收器一般分为:串行、吞吐量优先、响应时间优先
1. Serial GC:串行
VM指令:
1 |
|
Serial GC
串行回收器,单线程,eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束,这也是著名的STW问题(Stop The World!)
SerialOld GC
是 Serial 回收器的老年代版本,old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
整体来说适用于堆内存较少的情况
2. Parallel GC:吞吐量优先
VM指令:
1 |
|
Parallel GC
并行回收器,多线程,eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程 , 注重吞吐量
吞吐量优先就是暂停用户线程,将其他线程都用去GC,这样短时间内可以处理大量垃圾,让STW时间最短。缺点就是需要暂停用户线程
Parallel Old GC
Parallel GC老年代版本, old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
细节:
默认开启的收集线程数与CPU的数量相同。该收集器的目标是达到一个可控制的吞吐量。可以使用下面的参数来限制垃圾收集的线程数:
1 |
|
Parallel GC 还可设置自适应调节的参数。自适应调节策略:开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略
1 |
|
ParallelGC 收集器可以使用两个参数控制吞吐量:
1 |
|
3. CMS GC:响应时间优先
VM指令:
1 |
|
CMS GC
Concurrent Mark Sweep GC,(并发标记清除),并发回收器,一种以获取最短回收停顿时间为目标的老年代回收器
- 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法。注重响应时间
- 并发标记时不需暂停用户线程
- 重新标记时仍需暂停用户线程
- 如果由于内存碎片或者浮动垃圾过多而并发失败(即回收速度赶不上创建速度),会触发 Full GC,耗费时间较长
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
细节:CMS 收集器的运行过程分为下列4步:
- 初始标记:仅仅标记GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记:从GC Roots的直接关联对象开始遍历所有对象的过程,耗时较长但不需要停顿用户线程
- 重新标记:并发标记期间,并发运行的用户线程可能会产生垃圾,因此需要对整个堆空间进行重新标记,需要STW。耗时长
- 并发清除:对标记的对象进行清除回收,用户线程可并发执行。但此时的用户线程还是可能会产生新垃圾,这些垃圾就叫浮动垃圾,只能由下一次GC来处理,因此堆内存也需要预留一些空间(这个比例由下面第二个参数决定)给这些浮动垃圾,不能等整个堆内存都满了才GC。浮动垃圾如果也非常多,内存不够放,就会并发失败
响应时间优先:由于第2、4步可以并发执行用户线程,所以整个程序的响应时间比较快,称为响应时间优先
并发失败:由于CMSGC使用的是标记清除,可能导致会有很多内存碎片,或者是浮动垃圾过多,都会进而导致并发失败。老年代就会退化为serialOld回收器,进行标记整理,很耗费时间,这也是CMSGC的一个缺点
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
搭配使用:CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 回收器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记整理算法)使用。
其他细节:
1 |
|
4. G1 回收器
定义: Garbage First
从JDK9变为默认GC,JDK9废弃了之前的CMS回收器。
它将堆内存划分为多个大小相等的区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备。
特点:
- 同时注重吞吐量和响应时间(低延迟)
- 适合超大堆内存
- 整体上是标记整理算法,两个区域之间是复制算法
相关参数:
JDK8 并不是默认开启的,所需要参数开启。9以后就不用开了
1 |
|
G1 垃圾回收阶段:
4.1 G1垃圾回收阶段
(1)Young Collection
新生代回收:回收新生代垃圾 E:eden,S:幸存区,O:老年代
初始时,所有区域都处于空闲状态
创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
复制完成,将之前的伊甸园内存释放
随着时间流逝,伊甸园的内存又有不足
将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
释放伊甸园以及之前幸存区的内存
(2)Young Collection + CM
新生代回收+并发标记:
在 Young GC 时会进行 GC Root 的初始标记
当老年代占用内存超过阈值后,进行并发标记,这时无需暂停用户线程,阈值由下面的参数决定
1
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
(3)Mixed Collection
会对新生代 + 老年代 + 幸存区等进行混合回收,然后回收结束,重新进入新生代收集。
上一步的并发标记之后,会有最终标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象
进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(这几个地方垃圾多,回收了能释放的空间多)的区域(图中标红的O)(这个策略也是Gabage First 名称的由来)
暂停时间目标由此命令决定。回收的少自然暂停的时间就短
1
-XX:MaxGCPauseMillis=ms
下图显示了伊甸园和幸存区的存活对象复制 与 老年代和幸存区晋升的存活对象的复制。两个复制阶段也会STW
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
G1的Full GC
G1 在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
4.2 G1 新生代跨代引用
4.3 …
黑马视频后面还有些细节内容暂时略过,碰到再说
5. 回收器对比
不同回收器的Full GC情况不同,新生代的GC都可以叫minor gc,
- SerialGC(串行)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC(并行)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS(并发)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,且垃圾产生速度快于垃圾回收速度,便会触发 Full GC
- G1(并发)
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,且垃圾产生速度快于垃圾回收速度,便会触发 Full GC
五、垃圾回收调优
预备知识:
掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
查看当前所有JVM虚拟机参数命令,在Terminal输入:
1 |
|
1. 调优领域
gc只是调优的一个领域而已,其他还有:内存、锁竞争、cpu 占用、io等
2. 调优目标
低延迟:CMS(大部分公司也在用) G1(jdk9开始设为默认GC) ZGC
高吞吐量:ParallelGC
3. 最快的 GC 是不发生GC
首先排除减少因为自身编写的代码而引发的内存问题
- 数据是不是太多?
resultSet = statement.executeQuery(“select * from 大表”) –> 应该加个 limit n - 数据表示是否太臃肿
- 对象图 –> 只查自己所需要的就行
- 对象大小 引用一个Object最小是16字节, 包装类型是Integer 24个字节, 但int 等基本数据类型只要4个字节,能用基本类型可以用基本数据类型
- 是否存在内存泄漏
- static Map map等结构 …
- 解决:软引用、弱引用、第三方缓存实现(redis等)
4. 新生代调优
新生代的特点:
- 所有的 new 操作分配内存都是非常廉价的,伊甸园中分配很快
- TLAB (thread-lcoal allocation buffer):每个线程自己私有的一点点内存进行内存分配,多个线程同时分配内存时不会干扰
- 死亡对象回收零代价(因为都是标记复制,没用的对象直接释放掉内存)
- 大部分对象用过即死(朝生夕死)
- Minor GC 所用时间远小于 Full GC
因为新生代的这些特点,所以GC调优也是先从新生代开始,那么如何新生代调优?
- 加大新生代内存空间,但大小有说法
- 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
- 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
- 总的来说新生代还是要尽可能地设大,Oracle官方给出的建议是占据1/4到1/2为宜。新生代GC算法一般是标记复制,复制占的时间比标记长,但因为朝生夕死,这些时间也不会太长
- 新生代内存设置为能容纳:[ 并发量 * ( 请求-响应 ) ] 的数据为宜
- 幸存区需要能够保存 当前活跃对象+需要晋升的对象。幸存区太小,JVM会动态的调整晋升的阈值,有些其实也不是太重复用的对象,直接给晋升了,那就得等老年代GC后才能回收,无形中延长了这部分对象的寿命。因此我们希望这些存活时间短的对象留在幸存区,以便新生代GC就直接回收掉
- 晋升阈值配置得当,让长时间存活的对象尽快晋升。真正长时间存活的对象如果一直留在新生代,又要耗费幸存区内存又浪费新生代GC时间
新生代调优案例:Full GC 和 Minor GC 频繁
分析:新生代内存太小导致Minor GC 频繁,同时幸存区太小使得JVM把存活时间短的对象晋升到老年区,导致频繁Full GC
解决:加大新生代内存空间,适当增加晋升阈值,让存活时间短的对象留在幸存区
5. 老年代调优
以CMS为例
CMS 的老年代内存越大越好
先尝试不做调优,如果没有 Full GC 那么老年代空间很充裕,不用扩大。即使发生Full GC,也应该先尝试调优新生代。
观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
调整老年代空间占用了多少时,进行GC,即给浮动垃圾留一定空间。越低则触发GC越早,一般是75%-80%
1
-XX:CMSInitiatingOccupancyFraction=percent
老年代调优案例1:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
分析:确定是CMS的前提下,四个阶段前两个耗时很短,第三个重新标记耗费时间较长,它是重新扫描整个推空间。如果在重复标记时刚好业务大量发生,在新生代创建了大量对象,那么这个重新标记的时间就会很长。
解决:因此可以在重复标记之前先对新生代进行一次GC,下面这个参数就是这个设置的开关
1 |
|
老年代调优案例2:jdk1.7环境,采用CMS GC,老年代充裕情况下,发生 Full GC
分析:jdk1.7 及以前,堆是放在永久代里的,永久代空间设置小了,会触发整个堆的一次Full GC。
解决:增大永久代的初始值和最大值