JVM 系列文章目录
第一篇:内存区域与内存异常
第二篇:对象揭秘与堆内存分配策略
第三篇:垃圾回收
第四篇:类加载机制
第五篇:性能优化(上)
目录
一、对象已死吗?
在堆里面几乎存放 Java 世界的所有对象实例,垃圾回收器在对堆进行回收前,首先要确定对象是否 “活着” ,即是否直接或间接被引用。
1、引用计数
给对象添加一个引用计数器,每一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器值为0的对象就是不再被使用的。客观的说,引用计数算法实现简单,判断效率也很高,但是它很难解决对象之间的循环引用问题,所以主流的 Java 虚拟机里并没有选用。
2、可达性分析
这个算法的基本思路就是通过一系列的称为 GC Roots 的对象作为起始点,从这些节点向下搜索,搜索走过的路径称之为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的。
在Java语言中,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native 方法)引用的对象。
3、引用
JDK1.2之后Java对引用概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、强引用(Phantom Reference)四种,引用强度逐渐减弱。
- 强引用:类似 “Object obj = new Object()” 这种引用,垃圾回收器永远不会回收。
- 软引用:用来描述一些还有用但非必需的对象,系统在抛出内存溢出之前,会对该类引用对象进行二次回收,如果内存还不够,才会抛出内存溢出异常。
- 弱引用: 用来描述非必需对象,比软引用还弱,软引用对象只能生存到下一次垃圾回收之前,无论内存是否充足,都会被回收掉。
- 虚引用:它是最弱的一种引用关系,对其生存时间无影响,也无法通过虚引用来获取一个对象的实例,唯一目的就是垃圾回收时会收到一个系统通知。
4、生存还是死亡?
即使是在可达性分析算法中不可达的对象,也并非是 “非死不可” 的,真正宣告一个对象死亡,要经历两次标记过程。
- 如果对象在经过可达性分析之后并没有与 GC Roots 相连的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法或者 finalize() 方法已经被虚拟机调用过,虚拟机将两种情况都视为 “没有必要执行”。
- 如果被判定为没有必要执行 finalize() 方法,该对象会被放在一个叫做 F-Queue 的队列中,并在稍后由虚拟机自动建立、低优先级的 Finalizer 线程区执行它,也就是执行 finalize() 方法,这也是对象逃脱死亡命运的最后一次机会。稍后 GC 会对 F-Queue 队列中的对象进行第二次小规模的标记,如果对象在执行 finalize() 方法的过程中与引用链上的任何一个对象关联,那么在第二次标记时将它移除 “即将回收” 的集合。值得注意的是虚拟机在执行 finalize() 方法并不会等待它结束,原因是防止执行缓慢或发生死循环而导致整个内存回收系统崩溃。
5、方法区回收
- 永久代:JDK8以前方法区的实现为永久代,它是堆中和老年代连着的一块区域,它和老年代的垃圾回收是绑定的,一旦其中一个区域被占满就会触发 FGC。永久代的垃圾回收主要有两部分:废弃常量和无用的类。无用的类回收非常苛刻,只有该类的所有实例都已被回收、该类的 ClassLoader 已经被回收、该类对应的 java.long.class 对象没有被引用且无法在任何地方通过反射访问该类的方法才可以被回收,注意是可以不是必然会回收。
- 元空间:JDK8 方法区的实现为元空间,它被移到了堆里面,所以它的垃圾回收就不是由JVM来控制了。
二、垃圾回收算法
1、标记-清除(Mark-Sweep)
标记-清除分为 “标记” 和 “清除” 两个阶段:首先标记处需要回收的对象,在标记完成后统一回收所有被标记的对象。它主要有两处不足:一个是两个阶段的效率都很低,另一个是会产生很多内存碎片。
2、复制(Copying)
它将可用内存按容量划分为大小相等的两块,每次只是用其中的一块。当一块内存用完了,就将还存活的对象复制到另一块,然后把使用过的内存空间一次清掉。这样不用考虑内存碎片,效率也高,但是将可用内存缩小为原来的一半的代价有点太高了。
3、标记整理(Mark-Compact)
标记过程与 “标记-清除” 一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,直接清理掉端边界以外的内存。
4、分代收集(Generational Collection)
“分代收集” 的算法并没有什么新的思想,只是根据对象存活周期的不同将对象分为几块。一般是把 Java 堆分为年轻代和老年代,然后根据各代的特点采用最合适的算法。在年轻代中,每次垃圾回收都会有大批量的对象死去,只有少量存活,那就采用复制算法;而老年代中对象存活率高,没有额外空间对它进行分配担保,只能采用 “标记-清除” 或 “标记-整理” 算法。
三、垃圾回收器
如果说垃圾回收算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。JVM 规范中并没有规定它该怎样实现,因此不同厂商、不同版本的垃圾回收器差别很大,并且一般都会提供参数,供用户根据自己的特点和要求组合出各个年代的垃圾回收器。
HotSopt 虚拟机的垃圾回收器:
1、Serial
Serial是一个单线程的年轻代回收器,采用复制算法,但它的 “单线程” 的意义并不仅仅说明它只会使用一个 CPU 或一条回收线程去完成回收工作。更重要的是在进行垃圾回收时,必须暂停其他的所有工作线程,直到它回收结束。
Serial / Serial Old 回收器运行示意图:
2、ParNew
ParNew 回收器是 Serial 回收器的多线程版本,除了多线程回收外,其余行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略都和 Serial 完全一样。
3、Parallel Scavenge
Parallel Scavenge 一个年轻代回收器,也是 JDK8 的默认回收器。同样采用复制算法,又是并行的回收器。看起来和 ParNew 都一样,那它们有什么区别呢?
Parallel Scavenge 的特点是它的关注点和其他的回收器不同,CMS 等垃圾回收器的关注点是缩短垃圾回收时用户线程的停顿时间,而 Parallel Scavenge 关注的是达到一个可控的吞吐量,比如虚拟机运行了100分钟,垃圾回收用了1分钟,吞吐量就是99%。
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量能更高效的利用 CPU 的时间,尽快完成程序的运行任务,适合在后台运算而不需要太多交互的任务。
Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量,控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数和设置垃圾收集时间占总时间的比率的 -XX:GCTimeRatio 参数。MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,回收器尽可能的保证回收时间不超过此值。但也不是设的小就会使垃圾回收变得更快,GC 停顿时间缩短是以牺牲吞吐量和新生代的空间来换取的:系统把新生代调小,这样回收的频率会变大,停顿时间会变短,但是吞吐量也降下来了。MaxGCPauseMillis 是一个大于0小于100的整数,也是垃圾回收时间占总时间的比率。
Parallel Scavenge 还有个值得关注的参数:-XX:+UseAdptiveSizePolicy。这是一个开关参数,打开后就不需要手动指定新生代的大小,也就意味着新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)将失效。JVM 会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomiscs)。如果对于垃圾回收器不了解,手动优化比较困难,这是一个不错的选择。只需设置好内存数据大小(如 -Xmx 设置最大堆),然后使用 -XX:MaxGCPauseMillis 或 -XX:GCTimeRatio 给 JVM 设置一个优化目标,那些具体细节参数的调节就由JVM自适应完成,这也是 Parallel Scavenge 收集器与 ParNew 收集器一个重要区别。不过也存在一定的风险,有些情况存在 Survivor 被自动调为很小,可能只有几MB,这个时候 YGC 回收掉 Eden 区后,存活对象进入 Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而频繁触发 FGC。(还真碰到过一次,挺坑,记录在【JVM调优】:解决生产环境系统卡死这篇博客里。)
Parallel Scavenge / Parallel Old 回收器运行示意图:
4、Serial Old
Serial Old 是 Serial 的老年代版本,同样是一个单线程回收器,采用 “标记-整理” 算法。
5、Parallel Old
Parallel Old 是 Parallel Scavenge 的老年代版本,使用多线程和 “标记-整理” 算法。
6、CMS(Concurrent Mark Sweep)
CMS是一种以获取最短停顿时间为目标的老年代回收器。从名字(包含 “Mark Sweep”),可以看出,它是基于 “标记-清除” 算法实现的。他的过程稍微复杂一些,分为4个步骤:
1. 初始标记:
初始标记仍需要 “Stop The World”,它仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
2. 并发标记:
与用户线程同时进行,并发标记就是进行 GC Roots Tracing 的过程。
3. 重新标记:
并发标记期间用户线程继续运作会产生的对象变动,重新标记是为了修正这些对象的标记记录。并发标记仍需要 “Stop The World”,它的停顿时间比初始标记长,但远比并发标记短。
4. 并发清除:
与用户线程同时进行,开始清除未被标记的垃圾。
CMS回收器运行示意图:
CMS 是一款优秀的回收器,耗时最长的并发标记和并发清除过程都和用户线程一起运行,明显降低了回收停顿时间,但它也有三个明显的缺点:
- 对CPU资源很敏感,会占用用户线程的 CPU 资源,导致程序变慢,吞吐量变低。
- 并发清除阶段,用户线程也在运行,会产生 “浮动垃圾”。
- 基于 “标记-清除” 算法实现,会产生内存碎片。
7、G1(Garbage First)
G1 是一款面向服务端应用的垃圾回收器,也是 JDK9 以后的默认回收器。G1 回收器中 Java 堆中的内存布局和其他回收器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留年轻代和老年代的概念,但它们不在物理隔离,而是一群 Region 。
它有以下几个特点:
- 并行与并发:能充分利用 CPU 、多核环境的硬件优势,来缩短 GC 停顿时间,来提高吞吐量。
- 分代回收:分代概念在 G1 中仍然保留,虽然不需要其他回收器来配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理对象以获得更好的回收效果。
- 空间整合:采用 “标记-整理” 算法,从局部(两个 Region)看是基于 “复制” 算法,回收时不会产生内存碎片。
- 可预测的停顿:可以通过 -XX:MaxGCPauseMillis 设置一个停顿目标,默认是200ms,来降低延迟。
G1 回收器运行分为以下几个步骤:
1、初始标记:
仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 YGC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
2、并发标记:
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1 中会使用 SATB(snapshot-at-the-beginning) 算法来解决。
3、最终标记:
对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的 SATB 记录(漏标对象)。
4、筛选回收:
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。
G1回收器运行示意图:
四、垃圾回收器参数总结
参数 | 描述 |
---|---|
UseSerialGC | 虚拟运行在Client模式下的默认值,使用Serial & Serial Old收集器进行垃圾回收 |
UseParNewGC | 使用ParNew & Serial Old收集器进行垃圾回收(不推荐) |
UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行垃圾回收,Serial Old作为CMS出现Concurrent Mode Failure失败后的后备收集器 |
UseParallelGC | 虚拟运行在Server模式下的默认值,使用Parallel scavenge & Serial old收集器进行垃圾回收 |
UseParallelOldGC | 使用Parallel scavenge & Parallel old收集器进行垃圾回收 |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor = 8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小。默认为0,意味着任何对象都会先在新生代分配内存。如果设为10M,则超过10M的对象将不在eden区分配,而直接进入年老代。 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdptiveSizePolicy | 自适应调剂策略开关。打开,可以动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许比为1/(1+99)=1%的GC时间,吞吐量为99%。仅在使用Parallel scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间,仅在使用Parallel scavenge、G1收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效 |