JVM学习-java垃圾回收
Posted 智公博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习-java垃圾回收相关的知识,希望对你有一定的参考价值。
在上一篇中主要描述了Java 内存的分布,对象存储和访问已经各个内存区域可能的异常等,本篇主要描述Java 垃圾回收中有关内存回收策略和垃圾回收器的部分内容;
一、概述
在Java编程中我们一般都是只管申请内存(创建对象),而不需要关注或者主动的销毁对象,对象内存的销毁回收由虚拟机完成,我认为这一点是和C++最大的差别;虽然内存回收由虚拟机“自动”完成,但是我们还是需要深入理解一些内部的细节,以便更好的理解Java和写出更好的Java代码;虚拟机自动的内存回收机制中需要弄清楚几个主要的问题:
- 什么时候需要回收
- 应该回收那些对象
- 如何回收
二、确定回收对象
我们先来看下,在需要执行垃圾回收时(内存回收),如何确定哪些对象是需要回收的;需要回收的对象必须是无用的,即不可能再被使用到,那么问题就变为如何确定对象不可能再被使用到;
(一)、引用计数法
引用计数法就是给每个对象设置一个引用计数值,当对象的引用增加时,这个计数值加1,当引用失效时,计数值减1;当一个对象的计数值为0时就表示这个对象不可再被使用到,即是可以回收的;
引用计数法实现简单高效,算法也简单易懂,但是HotSpot和其他主流虚拟机并没有采用这种算法,因为在有对象是相互循环引用的情况下,就算对象是不可能再被使用到,计数值也不会为0,导致一直不能被回收;
(二)、可达性分析算法
可达性分析算法是通过设置一系列的“GC Roots”对象做为起点,从这些节点向下搜索,搜索所经过的路径称为“引用链”,如果一个对象没有一条引用链可以到达“GC Roots”,这认为这个对象是不可达,即对象不可能再被使用到,可以回收的;可达性算法可以解决上面提到的循环引用问题,一般可以做为“GC Roots”对象的有以下:
1. 虚拟机栈(帧栈中的本地变量)中引用的对象;
2. 方法区中类静态属性引用的对象;
3. 方法区中常量引用的对象;
4. 本地方法栈中JNI引用的对象;
(三)、引用类型
上面讲到的两种判断对象是否可用的算法中,都与“引用”强相关,我们通常理解引用是这样的:一个引用类型指向一个对象的内存地址(或间接指向),可以通过引用访问到对象实例;这个理解通常也是成立的,但是实际Java对“引用”还区分多种类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)
1. 强引用就是通常我们所说的引用,如 Object obj = new Object();只要对象的强引用还存在,垃圾收集器就不会回收这个对象;
软引用可以用在有用但是非必须的对象上;对于有软引用关联的对象,只有在系统内存不足(出现内存溢出异常之前)才会回收这些对象,可以通过SoftReference类实现软引用;
弱引用同样也可用于有用但非必须的对象上,弱引用的的强度比软引用更弱一点,无论内存是否足够,弱引用的对象都会被回收,可以同WeakReference类实现弱引用;
虚引用是强度最弱的一种引用类型,对象是否有虚引用不会对垃圾回收产生影响,也无法通过虚引用获取到对象实例,设置虚引用的唯一目的就是当对象被回收时能收到一个系统通知,可以通过PhantomReference类型实现虚引用;
(四)、自我拯救
通过可达性分析不可达的对象,并不是就一定会被回收,对象还可以进行一次自我拯救的过程:
一个对象要回收,需要经过两次标记过程:可达性分析为不可达的对象,第一次标记并筛选是否需要执行finalize()方法,当对象没有继承finalize()方法或者finalize方法已经执行过一次则不需要执行finalize()方法,否则需要执行;
对象的自我拯救就是发生在finalize()方法内:
当筛选确定需要执行finalize()方法,对象会被加入一个称为F-Queue队列中,并由一个虚拟机低优先级线程去执行对象的finalize()方法,在对象的finalize()方法中,如果将对象和“GC Roots”对象关联上,那对象就不是不可达的,即不被回收,成功拯救自己;
注:虚拟机并不保证对象的finalize()能够执行完成,如果finalize()方法执行非常耗时或者是死循环,将会终止执行;
(五)、方法区回收
在虚拟机规范中对方法区部分的内存没有做强制要求实现垃圾回收;方法区中进行垃圾回收一般能回收的内存比较有限,不像堆内存中每次垃圾回收都会有很多无用对象,在HotSpot虚拟机中永久代的垃圾回收“性价比”就很低;
永久代中的内存回收主要有两部分内存:无用常量和无用的类。回收常量与回收堆中的对象非常类似,而类的回收条件就比较复杂,至少只要满足3个条件才能认为类是“无用的类”:
1. 堆中没有该类的任何实例;
2. 该类的加载器ClassLoader已经被回收;
3. 改类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法;
虚拟机可以对满足上面3个条件的类进行回收,但并不是一定会回收,是否回收类虚拟机通过虚拟机参数:-Xnoclassgc 控制;
在有大量使用到反射机制,动态代理、CGLib、动态生成Jsp的场景下,还是建议需要开启永久代的GC;
三、如何回收-收集算法
对于垃圾收集算法虚拟机规范没有规定,各种虚拟机的实现方式各不相同,这里只讨论几种常见的算法;
(一)、标记-清除算法
这种算法将收集分为两个阶段:标识、清除;首先标记出需要回收的对象,然后进行同意回收标记的对象,标记的过程与上一节可达性算法描述的一致;这种算法设计实现简单,但是存在两个不足,一是标记和收集的效率都不告;二是通过这种方式回后内存后,容易产生内存碎片,可能会导致后续内存分配经常出现内存不足的情况;
(二)、复制算法
复制算法为了有更高的回收效率,它将可用内存按照容量划分为大小相等的两份,没次内存分配只在其中一块上分配,当使用的一块分配完后,将不可回收的对象复制到另外一块内存上,然后将使用中的一块全部内存回收,完成后下次分配内存就从清空出来的一块开始分配;这种算法效率很高,也不会有内存碎片问题,但是需要牺牲一半的可用内存用作复制;
(三)、标记-整理算法
这种算法和标记-清理很相似,就是清理的阶段不一样,完成标记后,并不是直接清理回后掉无用的内存,而是将有用不可回收的对象统一移动到一端,然后直接清理掉另外一端无用的内存;
(四)、分代收集算法
现在很多的主流的虚拟机大都有使用“分代收集”算法:根据对象存活周期的不同将内存分为几块;一般分为新生代和老年代,根据各个年代的特点采取合适的收集算法;在新生代中,正常情况下,每次垃圾回收都会有大量的对象需要回收,就可以选择使用复制算法,而老年代中的对象存活率比较高,就可以使用标记-清理或者标记-整理算法收集;
四、HotSpot虚拟机算法实现
上面从算法设计的角度描述了判断对象是否可用算法和垃圾收集算法,这节了解下HotSpot虚拟机对这些算法的具体实现
(一)、枚举根节点
可达性分析算法中需要搜索与“GC Roots”链接的引用链,但是可以作为“GC Roots”的对象可能很多,如果是通过每个节点都去搜索引用链必然很耗时间;
而且执行可达性分析时,应该是需要GC停顿的,就是用户线程需要停止执行,因为整个执行分析的过程必须保持“一致性”,如果执行分析时用户线程还在修改引用链,那么分析所获取的结果就可能不准确;因此在执行GC的是否必须暂停所有用户线程的执行,Sun公司称之为“Stop The World”;
HotSpot虚拟机并不需要检查每一个“GC Roots”节点,有方法直接知道在什么地方存放着对象引用:使用一组称为 OopMap 的数据结构记录对象引用的位置,GC在扫描时就已经知道这些引用的信息了;
(二)、安全的-safepoint
在OopMap的协助下,HotSpot虚拟机可以快速的完成“GC Roots”枚举,但是对象的引用关系在程序运行过程中是不断变化的,可以说,变化的还很快,如果对每次的对象引用关系变化都记录OopMap也是不现实的,实际上HotSpot虚拟机也并没有为每条变化指令都记录,只是会在“特定的位置”才记录,这些位置称为安全点(safepoint),线程只有执行到安全点位置才可以停下来;安全的既不能设置过多,增加系统运行的负荷,也不能设置过少,导致GC需要等待过长的时间;而安全点一般都是设置在这些位置:方法调用、循环跳转,异常跳转等;
当虚拟机需要执行GC时,并不会刚好每个线程都处于safepoint位置上,如何能让所有的线程都赶紧到下一个safepoint停下的等待GC执行呢,这里有两种方式:抢先式中断和主动式中断;
抢先式中断不需要线程主动执行,当需要执行GC时,首先把所有线程都中断掉,如何有线程不是在safepoint上,则让线程恢复运行到safepoint;很少有虚拟机采用这种方式中断线程来执行GC;
主动式中断则不直接对线程操作,由线程主动配合,为每一个线程设置一个标志,每个线程在safepoint执行时都主动的去轮询这个标志,如果发现标志为true,则自己主动挂起;
(三)、安全区域
使用safepoint似乎已经解决了程序暂停进入GC的问题,但是实际情况下可能会有这种情况:线程处于sleep或者blocked状态,没有在执行自然也去不到safepoint,而GC也不可能等到线程重新获得CPU时间,对于这种情况,虚拟机还设置了安全区域(safe region)来解决;
安全区域是指一段代码之中,引用关系不会发生变化,在这个区域内执行GC是安全的;在线程执行到safe region时,首先标已经进入了safe region,当在这段时间内虚拟机要发生GC时,就可以不用管已经标识safe region 的线程了;
当线程执行到要离开safe region,线程要检查系统是否已经完成“GC Roots”枚举,如果已经完成,线程可以继续执行,否则需要收到可以安全离开safe region 的信号才可继续执行;
四、垃圾收集器
虚拟机是实际是如何进行垃圾回收的是由垃圾收集器具体实现的,虚拟机规范中没有对垃圾收集器如何实现做规定,不同的虚拟机有不同的垃圾收集器,JDK1.7U14之后的的HotSpot虚拟机提供多种垃圾收集器选择:
上图中,几个用于不同分代的收集器,如果两个收集器之间有连线表示它们可以搭配使用;之所以提供这么多种收集器是因为每种收集器都有不同的特点,至于说那个收集器最好是没有定论的,是否最好、最合适是和具体使用场景有关的,有关每个收集器的特点可以从Oracle的网站或者其他资料详细了解,以下简单列出每种收集器一些特点:
(一)、Serial
- 历史最久,曾经是新生代唯一选择
- 单线程,进行时暂停所有用户线程-Stop The World
- Client模式下默认新生代收集器
- 单个线程更加高效简单,没有线程交互开销,一般client应用内存使用不大,停顿时间很短
(二)、ParNew
- Serial的多线程版本,控制参数、收集算法、Stop The World、对象分配策略、回收策略相同
- 新生代收集器
- 除了Serial已外,只有ParNew可以与CMS配合使用
- 单CPU环境下,性能差于Serial
- 默认下GC线程数与CPU数相同,可配置(-XX:ParallelGCThreads)
(三)、Parallel Scavenge
- 新生代收集器,采用复制算法
- 并行多线程收集
- 目标不是缩短停顿时间,而是达到可控制的吞吐量:CPU运行用户线程时间与CPU总消耗时间比
- 为了更高效率利用CPU时间
- 两个参数控制吞吐量:最大停顿时间:-XX:MaxGCPauseMillis(>0),直接吞吐量大小:-XX:GCTimeRatio(0~100)
- 尽可能保证停顿时间不超过设置值,
- 短停顿时间以牺牲吞吐量和新生代空闲换取,GC更频繁,吞吐量下降
- GCTimeRatio 吞吐量倒数,默认值99,即最大1% GC时间
- 吞吐量优先
- 自动化开关参数:-XX:+UseAdaptiveSizePolicy,不需手动设置新生代大小(-Xmn)、Eden与Survivor区比例(-XX:SurvivorRatio)、晋升老年代年龄(-XX:PretenureSizeThreadhold),虚拟机GC自适应调节策略:动态调整这些参数提供最适合的停顿时间或最大吞吐量
- 同是多线程并行,自适应调节是与ParNew最大的区别
(四)、Serial Old
- Serial的老年代版本
- 单线程
- 标记-整理 算法
- 主要用于Client模式
(五)、Parallel Old
- Parallel Scavenge 老年代版本
- 并行多线程
- 标记-整理 算法
- 在此收集器发布前,Parallel Scavenge 只能与Serial Old配置使用,二Serial Old在服务端应用性能较差
- 与Parallel Scavenge 配合,吞吐量优先
(六)、CMS
- Concurrent Mark Sweep,并发低停顿
- 以最短停顿时间为目标
- 标记-清除 算法
- 收集四个阶段:初始标记、并发标记、重新标记、并发清除
- 初始标记:Stop The World,快速标记GC Roots直接关联对象
- 并发标记:并发可达性分析
- 重新标记,Stop The World,修正并发阶段对象引用变动导致的变更
- 最耗时的并发标记、并发清除可以与用户线程一起工作
- 对CPU资源敏感,多线程并发程序通常都是这样,当CPU数少,或CPU资源紧张,性能下降
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure 导致另一次Full GC;
- 浮动垃圾(Floating Garbage):并发清理阶段用户线程还在运行,这个阶段新产生的垃圾不会在该次GC回收,因为已经过了标记阶段;
- 不可在老年代几乎用完才启动GC,必须保留较大一部分空间,用于GC中程序继续运行所需,默认68%
- 通过-XX:CMSInitiatingOccupancyFraction控制这个百分比,设置过高会导致“Concurrent Mode Failure”,过低又导致频繁GC
- CMS GC期间,出现内存不出会出现“Concurrent Mode Failure”失败,虚拟机将会启动后背方案,保证内存分配:临时启动Serial Old 重新一次老年代GC,导致长停顿
- 标记-清除 算法导致内存碎片,大对象分配难,导致频繁GC,运行控制Full GC 时开启合并整理,但是会导致停顿时间加长
(七)、G1(图中?)
- JDK1.7 最新,面向服务端应用
- 并发、并行:充分利用多CPU
- 分代收集,可管理整个堆,包括新生代和老年代
- 标记-整理 、复制 算法,GC不产生碎片
- 低停顿,可预测停顿时间模式
- 内存分区域管理(保留新生代和老年代概念)
- 分区域进行GC,避免全区域GC,高回收价值区域(单位时间内回收更多的内存空间)优先回收,可控制停顿时间
注:上图来源和收集器描述、比较可以参考:https://blogs.oracle.com/jonthecollector/entry/our_collectors
以上是关于JVM学习-java垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章
最新 JVM 垃圾回收器 Shenandoah GC 的实践案例