结合oracle关于GC系列官方文档(HotSpot Virtual Machine Garbage Collection Tuning Guide ,Java12)以及周志明老师的《深入理解Java虚拟机》(通过这本书收益挺大的,厚着脸皮称呼为老师吧!),这半个月来趁着业余时间断断续续的再次把GC机制回顾了一遍。在阅读了Oracle官方关于java12 GC机制的系列文章后,相对几年前周老师关于java7的著述发现:GC技术体系在几个支撑性的理论层面上变化并不大;如分代回收、内存分配与回收策略,不过在具体实现上确实是发生了较大的改变。也发现G1(Garbage-First)在C位出道的基础上已经大放异彩了(CMS(Concurrent Mark Sweep )被干下去了);在java7中G1刚移除“experimental”标志投入商用Hotspot VM中,HotSpot开发团队旨在以用G1替代CMS(为什么会被替代请见后文),终于在java9开始CMS被标记为deprecated,在java12官方文档中对于CMS介绍的模块直接在开头部分码上:
The CMS collector is deprecated. Strongly consider using the Garbage-First collector instead.
此外在发布的java11中新添加了ZGC收集器。当然,周老师提到,我们没法去论述比较出最好的collector,不同的收集器对应了不用的运用需求场景。高级用户自可以根据实际的部署需求结合实际运行情况选择不同的GC策略。而实际上,对于运行在JVM上的程序,GC导致的“stop the world”常成为高性能运用的优化瓶颈所在。当从架构角度去思考部署在JVM上的运用时,我想我们应该要知道程序的瓶颈在哪里,应该如何去调优。
以下对GC相关机制进行渐进讨论。
(1)操作系统分配给虚拟机的资源是有限的(即使在服务器上虚拟机完全占有节点资源也要受限于物理机的内存)
我们的程序每创建一个对象都需要在内存中申请空间,在程序不断地运行中即意味着有源源不断的对象被创建。那如何在内存空间有限的情况下保证Java程序正常运行呢?此时内存回收自然就出现了。在java程序运行中大多数的对象创建后都只是局限在某个代码段内执行(局部性原理),用完后就不需要了,比如在循环遍历中的Iterator,虚拟机会及时对这部分“不再需要”的对象进行回收。实际上在java程序中有90%的对象都是“朝生夕死”的。关于对象生命周期分布见下图:(来自Oracle官方文档,3 Garbage Collector Implementation)
Description of figure:
The x-axis of this graph, “Bytes allocated,” represents object lifetimes measured in bytes allocated. The y-axis, “Bytes surviving,” is the total bytes on objects with the corresponding lifetime. The left third of the graph is labeled “Minor collections.” The right two-thirds of the graph is labeled “Major collections.” The area below the plotted line is solid and colored blue. This area represents a typical distribution for the lifetime of objects. The area peaks sharply on the left and stretches out to the right. The graph is described further in the text that surrounds it.
(2)再论java堆内存分布
通过上一篇博客(JVM学习笔记(二)JVM运行时内存模型)我们知道java堆分为新生代(Eden、From Survivor、To Survivor)、老年代、永久代(java7及之前是永久代,java8开始为元空间)
(3)关于java堆内存的伸缩问题,以及默认分配内存大小
直接看oracle官方文档对于HotSpot JVM 相关默认参数设定说明:
① Garbage-First (G1) collector
② The maximum number of GC threads is limited by heap size and available CPU resources
③ Initial heap size of 1/64 of physical memory
④ Maximum heap size of 1/4 of physical memory
⑤ Tiered compiler, using both C1 and C2
以及范围自定义堆内存:
The following are general guidelines regarding heap sizes for server applications:
Unless you have problems with pauses, try granting as much memory as possible to the virtual machine. The default size is often too small.
Setting -Xms and -Xmx to the same value increases predictability by removing the most important sizing decision from the virtual machine. However, the virtual machine is then unable to compensate if you make a poor choice.
In general, increase the memory as you increase the number of processors, because allocation can be made parallel.
(4)引用计数算法与可达性分析算法
二者皆是判断对象是否存活的策略
引用计数算法:
对于一个对象空间,判断其是否存活的方法为当前是否有引用变量指向这个对象空间。考虑一种情况:两个不被需要的对象在互相引用情况下该回收还是不该回收呢?根据周志明老师的实验结果过来看,两个互相引用的对象也被JVM回收了,说明虚拟机并不使用这种算法。
可达性分析算法:
利用树型引用链的方法判断对象是否从根节点引用集可达(不妨看看图论相关概念);即从root引用节点集出发能否直接或间接的引用这个对象节点,以此来判断对象是否需要被回收,HotSpot VM便是运用这种方法。
(1)标记清除
对于判定可回收对象将会进行标记,在一定时间段内进行标记后进行统一回收,因为被回收的对象们大多数情况下不会再一片连续内存空间内吗,故而在进行了一次内存回收后将会出现大量不连续的可用空间,在此运用周志明老师的著述示意图:(个人认为原理和操作系统内存回收的过程中一个阶段是类似的)
该算法主要有两点不足:
首先是效率问题,标记和清除的算法效率不高。此外内存回收后产生大量了不连续内存碎片,在下一进行大对象创建时可能会由于找不到足够大的连续空间而提前引发GC,进而进一步降低程序吞吐。CMS便是运用这种方法,现已被高版本JVM淘汰。
(2)复制
在堆内存中另外划分出Survivor区域,利用这部分区域复制存放Eden区经过标记后还能存活下来的对象,而后对整个Eden进行回收,如此在内存回收后提供了一块连续的可用内存,当然也增加了额外的内存损耗。其实在堆内存中的对象只有不超过10%的对象能够活到老年代,也就是说相对标记清除算法所增加的额外内存只需要占虚拟机年轻代堆内存的10%(在两块survivor区域交换使用的情况下需占用20%年轻代堆内存)左右的空间就够了(这个参数可以根据实际需求自行调节)。在年轻代中大部分的对象存在于Eden区,Eden区占有了大部分年轻代堆内存(再复习一下堆内存,年轻代(young)主要包含了Eden区以及Survivor区,或说是由这两个区域组成;同样的,JVM堆内存主要由年轻代(young)与老年代(old)组成,默认情况下年轻代占有大部分的堆内存),在执行GC时Eden区的内存波动是最大的。如下图执行GC后Eden区的内存波动:(截自本人机器上对hotSpot8 VM程序监控,监控工具jconsole,下同)
这种算法下还是要面临内存分配问题,假设在某次复制算法执行中需要移动复制到survivor区域的对象所占空间超过了其所能划拨分配的上限该怎么办呢(毕竟survivor所占空间相对还是比较小的)?这就涉及到分配担保了(详见后文),分配是挺麻烦的,还记得前面所说的大对象直接进入老年代吗?这是为什么呢?
(3)标记整理
上面所述的复制算法需要占用额外的空间,比较适用于Eden区的对象回收中。但在对老年代的回收中却不再使用,因为在老年代中的对象很可能在经过一次GC后80%以上的对象都活下来了,见下图:在经过手动GC后老年代占用内存缩减并不大,即在执行GC后大部分的老年代对象都活下来了。
标记整理算法对可以活下来的对象进行向前移动,在此过程中直接覆盖可回收对象(这和数据结构中的非端数组元素删除过程有点类似,不妨结合理解)。在这种情况下使用基于标记整理的算法可以避免不必要的大空间分配。引用周志明老师著述的图解:
(4)分代回收
经过前面三种回收算法的讨论,我想分代回收算法就可以出场了。实际上分代回收算法并没有本质上的创新,其根据对象所处的不同年龄段(存活时间或说是熬过的GC次数)进而将其分配到不同的内存区域,针对不同的区域运用不同的算法;如在新生代使用复制算法,在老年代使用标记整理或标记清除算法。具体情况具体分析,进而运用不同的策略。
(1)Serial
最基本也是最古老的单线程收集器,在java1.3.1之前的client模式下针对新生代几乎采用Series收集器。Series在年轻代采用复制算法,在老年代采用标记整理算法(series old),如图:(引用周志明老师的深入理解Java虚拟机)
此外在进行内存回收时会执行“stop the word”动作暂停JVM内所有线程,由此在一定程度上造成了程序卡顿问题,实际上JVM团队一直致力于降低“stop the word”时间(截止到java12的G1中还是无法消除,只能尽可能的降低)。
(2)ParNew
作为Series的多线程版本,并没有太多的创新,工作在Server模式下。关于JVM的运行模式详见JVM client模式和Server模式的区别:
最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。
JVM工作在Server模式可以大大提高性能,但应用的启动会比client模式慢大概10%。当该参数不指定时,虚拟机启动检测主机是否为服务器,如果是,则以Server模式启动,否则以client模式启动,J2SE5.0检测的根据是至少2个CPU和最低2GB内存。
当JVM用于启动GUI界面的交互应用时适合于使用client模式,当JVM用于运行服务器后台程序时建议用Server模式。JVM在client模式默认-Xms是1M,-Xmx是64M;JVM在Server模式默认-Xms是128M,-Xmx是1024M。我们可以通过运行:java -version来查看jvm默认工作在什么模式。
(3)Parallel Scavenger
作为一个新生代收集器,相对于ParNew,Parallel Scavenger更加关注吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)),其可以根据用户自定义期待吞吐量进行自适应调节堆内存分区,以达到目标吞吐量(不会总是100%的满足);我们知道堆内存的区域(Eden、survivor、old)划分是影响内存回收效率的重要因素。其对应老年代收集器Parallel old(各个系列的收集器并不能很好的配合工作)。
(4)CMS(Concurrent Mark Sweep,@deprecated from java9)
采用标记清除算法,容易产生内存碎片进而导致提前触发Full GC(关于GC分类详见这位大佬的译作:Minor GC、Major GC和Full GC之间的区别,原文:Minor GC vs Major GC vs Full GC)。CMS注重程序响应时间的优化,旨在于获取最短回收停顿时间。运行过程示意图如下:(引自周志明老师的深入理解java虚拟机)
(5)G1(Garbage First),(用于取代CMS)
G1同样是采用分代回收、标记整理策略,并且将堆内存等分为一个个region,此时新生代和老年代不再是物理隔离的了。G1维护了一个关于这些region的回收优先级队列(以回收region获得的内存收益结合所花费时间成本为衡量,以最少的时间回收最大的内存空间为旨),在进行回收时可以根据这个优先级队列结合暂停时间限制选取某个region进行回收(类似贪心算法思想,亦可参考动态规划),这也是G1(Garbage First)命名的由来。当然Eden阶段对象大部分会集中分配在堆内存区域的一端(half)。有的对象会占用一个region区域,有的对象会连续占用多个region区域,对于占用多个region的对象会直接进入老年代。
对于离散分布的region也面临着一个问题,在GC阶段如何准确的判断各个对象的年龄呢?难不成要进行全表扫描?周志明老师在著述《深入理解Java虚拟机》中(截止到java7)提出了上述问题并给出了解答,截图如下:
关于region,在此根据oracle官方文档(HotSpot Virtual Machine Garbage Collection Tuning Guide:9 Garbage-First Garbage Collector)如下:
The young generation contains eden regions (red) and survivor regions (red with “S”). These regions provide the same function as the respective contiguou
s spaces in other collectors, with the difference that in G1 these regions are typically laid out in a noncontiguous pattern in memory. Old regions (light blue) make up the old generation. Old generation regions may be humongous (light blue with “H”) for objects that span multiple regions.
An application always allocates into a young generation, that is, eden regions, with the exception of humongous objects that are directly allocated as belonging to the old generation.
还是有一个就是循环标记回收过程:
主要分为了标记阶段和清除阶段,圈圈代表暂停(pause,标记暂停与收集暂停),此处的暂停可以简单理解为虚拟机的对外服务卡顿(与“stop the world”有什么区别呢?)。两个箭头代表了两个不同阶段的内存回收过程及其可能发生的暂停,上半部分为young-only阶段的初始标记暂停(大圈圈)以及内存回收暂停(小圈圈);下半部分为混合回收阶段及其暂停(小圈圈)。
关于这幅图的oracle官方描述如下:
This figure shows the sequence of G1 phases with the pauses that may occur during these phases. There are filled circles, every circle represents a garbage collection pause: blue circles represent young-only collection pauses, orange ones represent pauses induced by marking, and red circles represent mixed collection pauses. The pauses are threaded on two arrows forming a circle: one each for the pauses occurring during the young-only phase, another one representing the mixed collection phase. The young-only phase starts with a few young-only garbage collections represented by small blue circles. After object occupancy in the old generation reaches the threshold defined by InitiatingHeapOccupancyPercent, the next garbage collection pause will be an initial mark garbage collection pause, shown as larger blue circle. In addition to the same work as in other young-only pauses, it prepares concurrent marking.
While concurrent marking is running, other young-only pauses may occur, until the Remark pause (the first large orange circle), where G1 completes marking. Additional young-only garbage collections may occur until the Cleanup pause. After the Cleanup pause, there will be a final young-only garbage collection that finishes the young-only phase. During the space-reclamation phase a sequence of mixed collections will occur, indicated as red filled circles. Typically there are fewer mixed garbage collection pauses than young-only pauses in the young-only phase as G1 strives to make the space reclamations as efficient as possible.
G1虽然极力减少内存回收的暂停时间,但还是会有暂停;主要分为Remark阶段暂停以及Cleanup阶段暂停。Oracle官方详细描述如下:
The following list describes the phases, their pauses and the transition between the phases of the G1 garbage collection cycle in detail:
1、Young-only phase: This phase starts with a few Normal young collections that promote objects into the old generation. The transition between the young-only phase and the space-reclamation phase starts when the old generation occupancy reaches a certain threshold, the Initiating Heap Occupancy threshold. At this time, G1 schedules a Concurrent Start young collection instead of a Normal young collection.
(1)、Concurrent Start : This type of collection starts the marking process in addition to performing a Normal young collection. Concurrent marking determines all currently reachable (live) objects in the old generation regions to be kept for the following space-reclamation phase. While collection marking hasn’t completely finished, Normal young collections may occur. Marking finishes with two special stop-the-world pauses: Remark and Cleanup.
(2)、Remark: This pause finalizes the marking itself, performs global reference processing and class unloading, reclaims completely empty regions and cleans up internal data structures. Between Remark and Cleanup G1 calculates information to later be able to reclaim free space in selected old generation regions concurrently, which will be finalized in the Cleanup pause.
(3)、Cleanup: This pause determines whether a space-reclamation phase will actually follow. If a space-reclamation phase follows, the young-only phase completes with a single Prepare Mixed young collection.
2、Space-reclamation phase: This phase consists of multiple Mixed collections that in addition to young generation regions, also evacuate live objects of sets of old generation regions. The space-reclamation phase ends when G1 determines that evacuating more old generation regions wouldn’t yield enough free space worth the effort.
After space-reclamation, the collection cycle restarts with another young-only phase. As backup, if the application runs out of memory while gathering liveness information, G1 performs an in-place stop-the-world full heap compaction (Full GC) like other collectors.
(6)ZGC(available from java11)
Java11开始加入的一个以低延迟为目标的多线程收集器,需要以大量堆内存空间作为支撑(个人感觉有点用空间损耗换时间效率的策略,当然集结了整个JVM团队努力的作品肯定没这么简单)。除此之外,通过用户设定最大堆内存空间以及GC收集并发线程数量,其同样可以参考用户给定期待延迟结合实际资源自适应调节GC时间。适用于对实时性要求较高的运用程序。
详见官方文档:(11 The Z Garbage Collector)
(1)对象优先在Eden区分配
(2)大对象直接进入老年代
(3)长期存活的对象将进入老年代
(4)动态对象年龄判定
如果在survivor空间中相同年龄的所有对象大小的中和大于survivor空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代,而无须达到对象晋升老年代年龄阀值。如此可以减少minor GC的压力。
(5)空间分配担保
空间分配担保主要发生在minor GC时,我们知道minor GC主要回收Eden阶段对象,采用复制算法。Survivor空间是比较小的(多数为Eden区的八分之一),因为一般情况下,能够挺过minor GC活下来的对象比较少。但在某些情况下可能会出现活下来的对象较多,所占空间大于survivor所能分配的空间,此时就需要启动空间分配担保机制了;首先检查JVM是否允许分配担保以及查看老年代空间是否有足够的可用空间进行分配担保(即简单理解为老年代空余空间是否大于能够活下来的对象所占空间,然而在执行minor GC之前是无法提前得知有多少对象能够活下来的,故而在此取得以往GC对应本阶段能够活下来的对象所占空间大小的平均值(HRAVG)进行比较),如果老年代剩余空间小于HRAVG或不允许担保则执行Full GC,然后就没有然后了。而若条件允许,则执行minor GC。但在此依旧有可能出现活下来的对象大于平均值的情况而造成担保失败,在担保失败后将进行Full GC。上述过程就像是信用贷款中的征信以及担保一样,故而称之为空间分配担保。
主要针对元数据而言,在java7及之前会有一个永久代的存在。再java8开始有了一个元空间的存在,其默认不受JVM限制直接受限于系统所持有的可分配内存。Oracle官方描述如下(详见12 Other Considerations):
Java classes have an internal representation within Java Hotspot VM and are referred to as class metadata.
In previous releases of Java Hotspot VM, the class metadata was allocated in the so-called permanent generation. Starting with JDK 8, the permanent generation was removed and the class metadata is allocated in native memory. The amount of native memory that can be used for class metadata is by default unlimited. Use the option -XX:MaxMetaspaceSize to put an upper limit on the amount of native memory used for class metadata.
Java Hotspot VM explicitly manages the space used for metadata. Space is requested from the OS and then divided into chunks. A class loader allocates space for metadata from its chunks (a chunk is bound to a specific class loader). When classes are unloaded for a class loader, its chunks are recycled for reuse or returned to the OS. Metadata uses space allocated by mmap, not by malloc.