深入理解JVM之 ==> 内存分配策略 && 垃圾回收器
一、JVM内存分配机制
- JVM内存 ≈ Heap(堆内存) + PermGen(方法区) + Thrend(栈)
- Heap(堆内存)=Young(年轻代)+Old(老年代)
- 官方文档建议整个年轻代占整个堆内存的3/8,老年代占整个堆内存的5/8,但是可以配置为其他比例;
- Young(年轻代)= EdenSpace + FromSurvivor + ToSurvivor
- Eden区与两个存活区的内存大小比例是:8:1:1,同样可以配置为其他比例;
对象优先分配在 Eden 区
大多数情况下,对象分配在新生代的 Eden 区
-
- 当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC,把存活对象移动到 From Survivor 0 存活区,Eden 区被清空;
- 第二次 Eden 区又满了,再次触发 Minor GC,把 Eden 区的存活对象移到 To Survivor 1 存活区,把 From Survivor 0 存活区的存活对象也移到 To Survivor 1 存活区,这时 Eden 区和 From Survivor 0 存活区清空了;
- 第三次 Eden 区又满了,再次触发 Minor GC,把 Eden 区的存活对象移到 From Survivor 0 存活区,把 To Survivor 1 存活区的存活对象也移到 From Survivor 0 存活区,这时Eden 区和 To Survivor 1 存活区清空了;
- 这样 From Survivor 0 和 To Survivor 1 交替互换,轮流为清空,大大拉长了存活对象进入老年代的时间间隔;
大对象直接进入老年代
所谓的大对象是指:需要大量连续的内存空间的 Java 对象,最典型的大对象就是那种很长的字符串和数组。大对象对 Java 虚拟机的内存分配来说就是个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”他们。
Java 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代中进行分配,这样做的目的是为了避免在 Eden 区和两个 Survivor 存活区之间发生大量的内存复制(新生代采用复制算法收集内存)。
长期存活的对象进入老年代
既然 Java 虚拟机采用了分代回收的思想来管理内存,那么内存分配和回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 区出生并经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 存活区容纳的化,将被移动到 Survivor 存活区中,并且对象的年龄设为 1。对象在 Survivor 存活区每经过一次 Minor GC 且没有被回收的话,年龄就增加 1 ,当它的年龄增加到一定程度(默认为 15)时,该对象就会被移动到老年代中。
对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
动态对象年龄判断
为了能更好的试应不同程序的内存状况,Java 虚拟机并不是永远的要求对象的年龄必须达到了参数 -XX:MaxTenuringThreshold 设定的值才能进入老年代。如果在 Survivor 空间中相同年龄的所有对象大小总和大于 Survivor 空间的一般,大于或等于该年龄的对象就可以直接进入老年代,无须达到 -XX:MaxTenuringThreshold 中设置的年龄。
空间分配担保
在发生 Minor GC 之前,Java 虚拟机会先检查老年代最大可用连续内存空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看参数 HandlePromotionFailure 设置的值是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续内存空间是否大于历次晋升到老年代对象总和的平均大小,如果大于,将尝试进行一次 Minor GC(尽管这次 Minor GC 是有风险的),如果小于,或者 HandlePromotionFailure 参数设置为不允许担保失败,那这时改为进行一次 Full GC。
在 JDK 6 Update 24 之后,HandlePromotionFailure 参数不会再影响到虚拟机空间分配担保策略,规则变化为:只要老年代的连续内存空间大于新生代对象总大小,或则历次晋升到老年代的对象大小的总和就会进行 Minor GC,否则将进行 Full GC。
二、垃圾回收算法
标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,将所有被标记的对象统一进行回收。
它的不足有两个:一个是效率问题,标记和清除两个过程的效率都不高。另一个是空间的问题,标记清除后会产生大量不连续的内存碎片,碎片太多会导致后续在内存运行过程中需要分配大对象时,无法找到足够的连续的内存而不得不提前触发一次垃圾回收动作。标记-清除算法的指向过程如下图所示:
复制收集算法
为了解决“标记-清除”算法的效率问题,一种被称为“复制”(Copying)的收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块中,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,就不会存在内存碎片化的问题,内存分配时只要移动堆顶指针,按顺序分配即可,实现简单,运行高效,缺点是:内存的使用率实际只有一半,复制算法的执行过程如下图所示:
新生代将内存区间分为一块较大的 Eden 区和两块较小的 Survivor 存活区,每次使用 Eden 区和一个 Survivor 存活区,这样就大大增加了新生代的内存使用率。
标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端清空,所以在老年代中一般不使用复制收集算法。
根据老年代的特点,于是有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不少直接对可回收对象进行清理,而是让所有对象都向一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的执行过程如下图所示:
分代回收算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么心的思路,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要符出少量存活对象的复制成本就可以完成垃圾回收。
而老年代中因为对象存活率高,没有额外的内存空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
三、垃圾收集器
如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。基于 JDK 1.7 Update 14 之后的 HotSpot 虚拟机包含的所有收集器如下图所示:
上图展示了 7 种作用于不同分代的收集器,如果两个收集器中存在连线,就说明他们可以搭配使用。回收器所处的区域,则表示它是属于新生代的收集器还是老年代的收集器。
1、Serial 收集器
- 新生代收集器,可以和Serial Old、CMS组合使用;
- 采用复制算法;
- 使用单线程进行垃圾回收,回收时会导致Stop The World,用户进程停止;
- 虚拟机运行在 Client 模式下默认的新生代收集器;
- GC日志关键字:DefNew(Default New Generation)
- 图示(Serial+Serial Old)
2、ParNew 收集器
- 新生代收集器,可以和Serial Old、CMS组合使用;
- 采用复制算法;
- 使用多线程进行垃圾回收,回收时会导致Stop The World,其它策略和Serial一样;
- 虚拟机运行在 Server 模式下首选的新生代收集器;
- 在新生代收集器中,除了 Serial 收集器外,只有 ParNew 收集器才能与 CMS 收集器配合使用;
- 使用-XX:ParallelGCthreads参数来限制垃圾回收的线程数;
- GC日志关键字:ParNew(Parallel New Generation)
- 图示(ParNew + Serail Old)
3、Paralle Scavenge
- 新生代收集器,可以和Serial Old、Parallel Old组合使用,不能和CMS组合使用;
- 采用复制算法;
- 使用多线程进行垃圾回收,回收时会导致Stop The World;
- 关注系统吞吐量
- -XX:MaxGCPauseMillis:设置大于0的毫秒数,收集器尽可能在该时间内完成垃圾回收
- -XX:GCTimeRatio:大于0小于100的整数,即垃圾回收时间占总时间的比率,设置越小则希望垃圾回收所占时间越小,CPU能花更多的时间进行系统操作,提高吞吐量
- -XX:UseAdaptiveSizePolicy:参数开关,启动后系统动态自适应调节各参数,如-Xmn、-XX:SurvivorRatio等参数,这是和ParNew收集器重要的区别
- GC日志关键字:PSYoungGen
4、Serial Old
- 年老代收集器,可以和所有的年轻代收集器组合使用(Serial收集器的年老代版本)
- 采用 ”标记-整理“算法,会对垃圾回收导致的内存碎片进行整理
- 使用单线程进行垃圾回收,回收时会导致Stop The World,用户进程停止
- GC日志关键字:Tenured
- 图示(Serial+Serial Old)
5、Parallel Old
- 年老代收集器,只能和Parallel Scavenge组合使用(Parallel Scavenge收集器的年老代版本)
- 采用 ”标记-整理“算法,会对垃圾回收导致的内存碎片进行整理
- 关注吞吐量的系统可以将Parallel Scavenge+Parallel Old组合使用
- GC日志关键字:ParOldGen
- 图示(Parallel Scavenge+Parallel Old)
6、CMS(Concurrent Mark Sweep)
- 年老代收集器,可以和Serial、ParNew组合使用
- 采用 ”标记-清除“算法,可以通过设置参数在垃圾回收时进行内存碎片的整理
- 1、UserCMSCompactAtFullCollection:默认开启,FullGC时进行内存碎片整理,整理时用户进程需停止,即发生Stop The World
- 2、CMSFullGCsBeforeCompaction:设置执行多少次不压缩的Full GC后,执行一个带压缩的(默认为0,表示每次进入Full GC时都进行碎片整理)
- CMS是并发算法,表示垃圾回收和用户进行同时进行,但是不是所有阶段都同时进行,在初始标记、重新标记阶段还是需要Stop the World。CMS垃圾回收分这四个阶段
- 1、初始标记(CMS Initial mark) Stop the World 仅仅标记一下GC Roots能直接关联到的对象,速度快
- 2、并发标记(CMS concurrent mark) 进行GC Roots Tracing,时间长,不发生用户进程停顿
- 3、重新标记(CMS remark) Stop the World 修正并发标记期间因用户程序继续运行导致标记变动的那一部分对象的标记记录,停顿时间较长,但远比并发标记时间短
- 4、并发清除(CMS concurrent sweep) 清除的同时用户进程会导致新的垃圾,时间长,不发生用户进程停顿
- CMS 收集器和 Serial Old 收集器配合使用
- CMS 收集器在并发清理阶段用户线程还在运行,自然还会有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉,需要到下一次 GC 时再清理掉,这一部分垃圾叫做“浮动垃圾”。因此,CMS 再收集时需要留一部分空间给这部分垃圾,不能等到老年代几乎填满再进行收集。在 JDK 1.6 中,CMS 收集的启动阈值为 92%,如果在 CMS 收集期间,预留的空间不能满足需要,就会出现“concurrent mode failure”失败,这时虚拟机将启动后备预案,临时启动 Serial Old 收集器来重新对老年代进行收集,这样停顿时间就会变得更长。所以,设置合适的启动阈值非常重要,通过 -XX:CMSInitiatingOccupancyFraction 来设置。
- 适合于对响应时间要求高的系统
- GC日志关键字:CMS-initial-mark、CMS-concurrent-mark-start、CMS-concurrent-mark、CMS-concurrent-preclean-start、CMS-concurrent-preclean、CMS-concurrent-sweep、CMS-concurrent-reset等等
- 缺点
- 1、对CPU资源非常敏感
- 2、CMS收集器无法处理浮动垃圾,即清除时用户进程同时产生的垃圾,只能等到下次GC时回收
- 3、因为是使用“标记-清除”算法,所以会产生大量碎片
- 图示
7、G1(Garbage-First)
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 对内存的布局与其他收集器有很大的差异,它将整个 Java 堆划分成多个大小相等的独立区域(Region),虽然还是保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。
- 并行与并发
- G1 能充分利用多CPU、多核环境的优势,缩短GC停顿时间;
- 分代收集
- 分代的概念在G1中保留,虽然G1不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果;
- 空间整合
- G1从整体上看是基于”标记-整理“算法实现的收集器,从局部上(两个Region之间)看是基于”复制“算法实现的,这意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存;
- 可预测的停顿
- 降低停顿时间是CMS和G1共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型:能让使用者明确指定在长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒;
- G1之所以能够建立可预测的时间停顿模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集;
- G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的Region;
总结
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | Both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
垃圾收集器参数总结
参数 | 描述 |
---|---|
-XX:PrintGCDetails | 发生GC时打印内存回收日志(详细) |
-XX:PrintGC | 发生GC时打印内存回收日志 |
-XX:UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收 |
-XX:UseParNewGC | 打开此开关后,使用ParNew+Serial Old的收集器组合进行内存回收 |
-XX:UseConcMarkSweepGC | 打开此开关后,使用ParNew+CMS+Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
-XX:UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收 |
-XX:UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
-XX:UseG1GC | 打开此开关后,使用G1垃圾收集器 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认值为8,代表Eden:Survivor=8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数时就进入老年代 一般应该使用的值为32 |
-XX:UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
-XX:HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 JDK1.6 Update24 之后不再使用 |
-XX:ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
-XX:GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效 |
-XX:CMSInitingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
-XX:UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用CMS收集器时生效 |
-XX:CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效 |