从Jdk8到Jdk12的Java虚拟机垃圾回收(垃圾收集)相关论文和官方网站集锦
Posted 21aspnet
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从Jdk8到Jdk12的Java虚拟机垃圾回收(垃圾收集)相关论文和官方网站集锦相关的知识,希望对你有一定的参考价值。
JVM架构图
《Java Program Execution Process in Detail | Working of JUST-IT-TIME Compiler (JIT) in Detail》
----
这张图微观画的很好: https://www.javainterviewpoint.com/java-virtual-machine-architecture-in-java/
这张图更宏观一点: http://sunjava4all.blogspot.com/2012/01/jvm-architecture.html
这张图综合了上面:https://www.quora.com/What-is-the-Java-virtual-machine-JVM
有个老外的视频感兴趣可以看看:https://www.youtube.com/watch?v=dncpVFP1JeQ
JVM分代这张图画的很清晰
出自:http://jprante.github.io/2012/11/28/Elasticsearch-Java-Virtual-Machine-settings-explained.html
JDK12的JVM选项
Java虚拟机技术概述
本章介绍Java虚拟机(JVM)的实现以及Java HotSpot技术的主要功能:
- 自适应编译器:标准解释器用于启动应用程序。应用程序运行时,将分析代码以检测性能瓶颈或热点。Java HotSpot VM编译代码的性能关键部分以提高性能,但不编译很少使用的代码(大多数应用程序)。Java HotSpot VM使用自适应编译器来决定如何使用内联等技术优化编译代码。
- 快速内存分配和垃圾收集:Java HotSpot技术为对象和快速,高效,最先进的垃圾收集器提供快速内存分配。
- 线程同步:Java HotSpot技术提供了一种线程处理功能,旨在扩展以用于大型共享内存多处理器服务器。
在Oracle Java Runtime Environment(JRE)8及更早版本中,JVM的不同实现(客户端VM,服务器VM和最小VM)支持常用作客户端,服务器和嵌入式系统的配置。由于大多数系统现在可以利用服务器VM,因此Oracle Java运行时环境(JRE)9仅提供该VM实现。
Java虚拟机指南
垃圾收集调整
垃圾收集
Oracle的HotSpot VM包含几个垃圾收集器,可用于帮助优化应用程序的性能。如果您的应用程序处理大量数据(多个千兆字节),具有多个线程并具有高事务率,则垃圾收集器尤其有用。
有关可用垃圾收集器的说明,请参阅Java平台标准版HotSpot虚拟机垃圾收集调整指南中的垃圾收集实现。
G1与其他收集器(CMS)的比较
这是G1和其他收集器之间主要差异的总结:
- 并行GC只能作为一个整体压缩和回收旧一代的空间。G1逐步将这项工作分散到多个更短的集合中。这大大缩短了暂停时间,可能会降低吞吐量。
- 与CMS类似,G1同时执行旧一代空间回收的一部分。但是,CMS无法对旧一代堆进行碎片整理,最终会遇到长的Full GC。
- G1可能表现出比上述收集器更高的开销,由于其并发性质而影响吞吐量。
- ZGC针对非常大的堆,旨在以更高的吞吐量成本提供显着更小的暂停时间。
已经不推荐使用CMS推荐G1了:
Java虚拟机规范,Java SE 12 Edition
JDK8的JVM选项
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/toc.html
JVM按以下顺序搜索并加载类:
Bootstrap类,它们是构成Java平台的类,包括类rt.jar和其他几个重要的JAR文件。
扩展类,它使用Java扩展机制。这些类捆绑为JAR文件并位于extensions目录中。
用户类,是由开发人员和第三方定义的类,不利用扩展机制。您-classpath可以使用命令行(首选)上的选项或CLASSPATH环境变量来标识这些类的位置。请参阅设置类路径。
----------------------
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#CBBIJCHG
下面选择一些常用选项,更多完整参数建议看官方链接:
-XX:MaxGCPauseMillis = 时间
设置最大GC暂停时间的目标(以毫秒为单位)。这是一个软目标,JVM将尽最大努力实现它。默认情况下,没有最大暂停时间值。
以下示例显示如何将最大目标暂停时间设置为500毫秒:
-XX:MaxGCPauseMillis = 500
-XX:MaxMetaspaceSize = size
设置可以为类元数据分配的最大本机内存量。默认情况下,大小不受限制。应用程序的元数据量取决于应用程序本身,其他正在运行的应用程序以及系统上可用的内存量。
以下示例显示如何将最大类元数据大小设置为256 MB:
-XX:MaxMetaspaceSize =256M
-XX:ParallelGCThreads = threads
设置在年轻和老一代中用于并行垃圾收集的线程数。默认值取决于JVM可用的CPU数量。
例如,要将并行GC的线程数设置为2,请指定以下选项:
-XX:ParallelGCThreads = 2
-XX:+ ScavengeBeforeFullGC
在每个完整GC之前启用年轻代的GC。默认情况下启用此选项。Oracle建议您不要禁用它,因为在完整GC之前清除年轻代可以减少从旧代空间到年轻代空间可到达的对象数。要在每个完整GC之前禁用年轻代的GC,请指定-XX:-ScavengeBeforeFullGC
。
-XX:+ UseTLAB
允许在年轻代空间中使用线程局部分配块(TLAB)。默认情况下启用此选项。要禁用TLAB,请指定-XX:-UseTLAB
。
-XX:TLABSize = size
设置线程局部分配缓冲区(TLAB)的初始大小(以字节为单位)。附加字母k
或K
表示千字节,m
或M
指示兆字节,g
或G
指示千兆字节。如果此选项设置为0,则JVM会自动选择初始大小。
以下示例显示如何将初始TLAB大小设置为512 KB:
-XX:TLABSize = 512K
-XX:+ UseG1GC
允许使用垃圾优先(G1)垃圾收集器。它是一个服务器式垃圾收集器,针对具有大量RAM的多处理器计算机。它以高概率满足GC暂停时间目标,同时保持良好的吞吐量。G1收集器推荐用于需要大堆(大小约为6 GB或更大)且GC延迟要求有限的应用(稳定且可预测的暂停时间低于0.5秒)。
默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器。
XX:+ UseConcMarkSweepGC
允许为旧一代使用CMS垃圾收集器。Oracle建议您在spam(-XX:+UseParallelGC
)垃圾收集器无法满足应用程序延迟要求时使用CMS垃圾收集器。G1垃圾收集器(-XX:+UseG1GC
)是另一种选择。
默认情况下,禁用此选项,并根据计算机的配置和JVM的类型自动选择收集器。启用此选项后,将-XX:+UseParNewGC
自动设置该选项,您不应禁用该选项,因为JDK 8中已弃用以下选项组合:-XX:+UseConcMarkSweepGC -XX:-UseParNewGC
。
-XX:+ PrintGC
允许在每个GC上打印消息。默认情况下,禁用此选项。
======================
JDK7的JVM选项
https://www.oracle.com/technetwork/articles/java/vmoptions-jsp-140102.html
-------------------------------
------------------------------
如果还看不明白这个说的比较清晰准确可以入门看看:
上文同《JVM中的G1垃圾回收器》
G1虽然保留了CMS关于代的概念,但是代已经不是物理上连续区域,而是一个逻辑的概念。在标记过程中,每个区域的对象活性都被计算,在回收时候,就可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Remark阶段新算法的运用,以及收集过程中的压缩,都弥补了CMS不足。引用Oracle官网的一句话:“G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS)”。
-----
----------------------------
G1收集器与CMS收集器的对比与实战
CMS(Concurrent Mark and Sweep)是以牺牲吞吐量为代价来获得最短停顿时间的垃圾回收器,主要适用于对响应时间的侧重性大于吞吐量的场景。仅针对老年代(Tenured Generation)的回收。
为求达到该目标主要是因为以下两个原因:
- 没有采取compact操作,而是简单的mark and sweep,同时维护了一个free list来管理内存空间,所以也产生了大量的内存碎片。
- mark and sweep分为多个阶段,其中大部分的阶段的GC线程是和用户线程并发执行,默认的GC线程数为物理CPU核心数的1/4。
因为是并发地进行清理,所以必须预留部分堆空间给正在运行的应用程序,默认情况下在老年代使用了68%及以上的内存的时候就开始CMS。
Promotion Failed
由于CMS没有任何的碎片整理机制,所以会产生大量的堆碎片。因此可能会发生即使堆的大小没有耗尽,但是从新生代晋升至老年代却失败的情况。此时会fallback为Serial Old从而引起一次full GC(会进行碎片整理)。可以增加老年代的大小和Survivor区的大小以减少full GC的发生。
Concurrent Mode Failed
如果对象分配率高于CMS回收的效率,将导致在CMS完成之前老年代就被填满,这种状况成为“并发模式失败”,同样也会引起full GC。可以调节-XX:CMSInitiatingOccupancyFraction和新生代的堆大小。
JVM: CMS过程中的promotion failure和concurrent mode failure有何区别,如何应对
Garbage First - G1垃圾收集器
G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆时产生的停顿。在回收的时候将对象从一个小堆区复制到另一个小堆区,这意味着G1在回收垃圾的时候同时完成了堆的部分内存压缩,相对于CMS的优势而言就是内存碎片的产生率大大降低。
heap被划分为一系列大小相等的“小堆区”,也称为region。每个小堆区(region)的大小为1~32MB,整个堆大致要划分出2048个小堆区。
与上一代的垃圾收集器一样在逻辑上被划分Eden、Survivor和老年代,但是各种角色的region个数都不是固定的。
young GC
young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间存活的对象会被**撤离**(代表复制或者移动)到另外一个或是多个Survivor小堆区,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
本阶段如同老一代的垃圾收集器一般,会有Stop The World暂停。同时计算出Eden的大小和Survivor的大小,为下一次young GC做准备。Accounting信息会被保存用于计算大小。如目标暂停时间的东西会被纳入考虑范围当中。这种方法使得resize小堆区更加容易,使小堆区可以根据需要变得更大或者更小。
最后,存活的对象会被撤离至Survivor小堆区和老年代小堆区,如下图所示。
最近被晋升至老年代的对象用深蓝色表示,Survivor小堆区用绿色表示。
简而言之,G1的young GC规范如下:
- 堆从一个单一的内存空间被划分为众多的小堆区(region)。
- 新生代的内存由一系列不连续的小堆区所组成。这使得在需要的时候更加容易进行resize。
- young GC是一个STW事件,所有应用程序线程都会被暂停。
- young GC会使用多线程并行执行。
- 存活的对象将会复制到新的Survivor小堆区或者老年代小堆区。
总结一下G1收集器对老年代的收集
- 并发标记阶段
- 在应用程序运行时并发地计算活跃度信息
- 活跃度信息甄别出哪个小堆区是在撤离暂停时最适合回收的
- 重新标记阶段
- 使用Snapshot-at-the-Beginning (SATB) 算法,这个算法比CMS所使用的要快得多
- 回收空的小堆区
- 复制/清除阶段
- 新生代和老年代同时被回收
- 老年代的小堆区会根据活跃度而进行部分的选定
总结
如果是使用旧的老年代收集器,在面临超大堆的时候会显得力不从心,通常一次full GC就会暂停3s以上。CMS的出现真的是救星,如今G1也在蓬勃发展,在JDK9中成为默认的垃圾收集器。我们在上面对老年代收集进行测试时,的确发现了G1收集器相对于CMS是有优势的。但是如果我们并不是运行超大堆的Java程序,或者线上的CMS收集器已经运行得很好,我们就不必再迁移到G1了。
------------------------------
深入理解 Java G1 垃圾收集器(优秀)
第四阶段,G1(并发)收集器
G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低。
其次,G1将新生代,老年代的物理空间划分取消了。
这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC 阶段:
- 阶段1:根扫描
静态和本地对象被扫描 - 阶段2:更新RS
处理dirty card队列更新RS - 阶段3:处理RS
检测从年轻代指向年老代的对象 - 阶段4:对象拷贝
拷贝存活的对象到survivor/old区域 - 阶段5:处理引用队列
软引用,弱引用,虚引用处理
G1 Mix GC(新生代垃圾收集+回收老年代分区)
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
- 全局并发标记(global concurrent marking)
- 拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
- 初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。 - 根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。 - 并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断 - 最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。 - 清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
------------------------------
使用CMS垃圾收集器产生的问题和解决方案
首先我们经常遇到promotion failed问题,这也确实是个很头痛的问题,一般是进行Minor GC的时候,发现救助空间不够,所以,需要移动一些新生带的对象到老年带,然而,有些时候尽管老年带有足够的空间,但是由于CMS采用标记清除算法,默认并不使用标记整理算法,可能会产生很多碎片,因此,这些碎片无法完成大对象向老年带转移,因此需要进行CMS在老年带的Full GC来合并碎片。
这个问题的直接影响就是它会导致提前进行CMS Full GC, 尽管这个时候CMS的老年带并没有填满,只不过有过多的碎片而已,但是Full GC导致的stop-the-wold是难以接受的。
解决这个问题的办法就是可以让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法,CMS提供了以下参数来控制:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
也就是CMS在进行5次Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内,甚至可以配置CMS在每次Full GC的时候都进行内存的整理。
另外,有些应用存在比较大的对象朝生熄灭,这些对象在救助空间无法容纳,因此,会提早进入老年带,老年带如果有碎片,也会产生promotion failed, 因此我们应该控制这样的对象在新生代,然后在下次Minor GC的时候就被回收掉,这样避免了过早的进行CMS Full GC操作,下面的一个配置样例就通过增加救助空间的大小来解决这个问题:
-Xmx4000M -Xms4000M -Xmn600M -XXmSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled eCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log
上面讨论了promotion failed引起的原因以及解决方案,除了promotion failed还有一个情况会引起CMS回收失败,从而退回到Serial Old收集器进行回收,我们在线上尤其要注意的是concurrent mode failure出现的频率,这可以通过-XX:+PrintGCDetails来观察,当出现concurrent mode failure的现象时,就意味着此时JVM将继续采用Stop-The-World的方式来进行Full GC,这种情况下,CMS就没什么意义了,造成concurrent mode failure的原因是当minor GC进行时,旧生代所剩下的空间小于Eden区域+From区域的空间,或者在CMS执行老年带的回收时有业务线程试图将大的对象放入老年带,导致CMS在老年带的回收慢于业务对象对老年带内存的分配。
解决这个问题的通用方法是调低触发CMS GC执行的阀值,CMS GC触发主要由CMSInitiatingOccupancyFraction值决定,默认情况是当旧生代已用空间为68%时,即触发CMS GC,在出现concurrent mode failure的情况下,可考虑调小这个值,提前CMS GC的触发,以保证旧生代有足够的空间。
总结:
1. promotion failed – concurrent mode failure
Minor GC后, 救助空间容纳不了剩余对象,将要放入老年带,老年带有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者 调大新生代或者救助空间
2. concurrent mode failure
CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年带直接分配,例如大对象,但是老年带没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
----------------------------------------------------------------
JVM性能优化,第4部分:针对低延迟的C4垃圾收集......
http://ssw.jku.at/General/Staff/TW/igv.html
IdealGraphVisualizer研究C2的实用工具之一
使用 Ideal Graph Visualizer 分析编译代码过程
-------------------
https://www.cs.kent.ac.uk/people/staff/rej/gcbib/
-----------------
https://hllvm-group.iteye.com/group/topic/21468
RednaxelaFX 2010-09-11 |
------
Vladimir Ivanov讲解JIT编译器:
JIT-compiler in JVM seen by a Java developer, Vladimir Ivanov, JavaOne 2013 Moscow, 2013
https://wiki.openjdk.java.net/display/HotSpot/Garbage+Collection
至于G1的算法⋯大体概念其实还挺直观的?到底是哪里没明白?
从最高层看,G1的collector一侧其实就是两个大部分:
* 全局并发标记(global concurrent marking)
* 拷贝存活对象(evacuation)
而这两部分可以相对独立的执行。
Global concurrent marking基于SATB形式的并发标记。它具体分为下面几个阶段:
1、初始标记(initial marking):暂停阶段。扫描根集合,标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不使用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用young GC的暂停,因而没有额外的、单独的暂停阶段。
2、并发标记(concurrent marking):并发阶段。不断从扫描栈取出引用递归扫描整个堆里的对象图。每扫描到一个对象就会对其标记,并将其字段压入扫描栈。重复扫描过程直到扫描栈清空。过程中还会扫描SATB write barrier所记录下的引用。
3、最终标记(final marking,在实现中也叫remarking):暂停阶段。在完成并发标记后,每个Java线程还会有一些剩下的SATB write barrier记录的引用尚未处理。这个阶段就负责把剩下的引用处理完。同时这个阶段也进行弱引用处理(reference processing)。
注意这个暂停与CMS的remark有一个本质上的区别,那就是这个暂停只需要扫描SATB buffer,而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,而此时整个young gen(不管对象死活)都会被当作根集合的一部分,因而CMS remark有可能会非常慢。
4、清理(cleanup):暂停阶段。清点和重置标记状态。这个阶段有点像mark-sweep中的sweep阶段,不过不是在堆上sweep实际对象,而是在marking bitmap里统计每个region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的region就会将其整体回收到可分配region列表中。
G1需要暂停来拷贝对象,而CMS在暂停中只需要扫描(mark)对象,那算法上G1的暂停时间会比CMS短么?
其实CMS在较小的堆、合适的workload的条件下暂停时间可以很轻松的短于G1。在2011年的时候Ramki告诉我堆大小的分水岭大概在10GB~15GB左右:以下的-Xmx更适合CMS,以上的才适合试用G1。现在到了2014年,G1的实现经过一定调优,大概在6GB~8GB也可以跟CMS有一比,我之前见过有在-Xmx4g的环境里G1比CMS的暂停时间更短的案例。
合适的workload:CMS最严重的暂停通常发生在remark阶段,因为它要扫描整个根集合,其中包括整个young gen。如果在CMS的并发标记阶段,mutator仍然在高速分配内存使得young gen里有很多对象的话,那remark阶段就可能会有很长时间的暂停。Young gen越大,CMS remark暂停时间就有可能越长。所以这是不适合CMS的workload。相反,如果mutator的分配速率比较温和,然后给足时间让并发的precleaning做好remark的前期工作,这样CMS就只需要较短的remark暂停,这种条件下G1的暂停时间很难低于CMS。
要在拷贝对象的前提下实现真正的低延迟就需要做并发拷贝(concurrent compaction)。但是现在已知的实现concurrent compaction的GC算法无一例外需要使用某种形式的read barrier,例如Azul的C4和Red Hat的Shenendoah。不用read barrier的话,没办法安全的实现一边移动对象一边修正指向这些对象的引用,因为mutator也可以会并发的访问到这些引用。
而G1则坚持只用write barrier不用read barrier,所以无法实现concurrent compaction。
[资料] 名词链接帖[占位] https://rednaxelafx.iteye.com/
选择合适的GC
G1垃圾收集器
Java生态系统的好消息是,从JDK 9开始,默认情况下启用现代缩小的G1垃圾收集器。如果使用较低版本的JDK,则可以使用该-XX:+UseG1GC
参数启用G1 。
G1的主要优势之一是能够在没有冗长的暂停时间的情况下压缩可用内存空间。它也可以取消使用未使用的堆。我们发现此GC是在OpenJDK或HotSpot JDK上运行的Java应用程序的垂直扩展的最佳选择。
垃圾收集器 - 串行与并行与CMS与G1(以及Java 8中的新功能)
Java 8和PermGen
Java 8中最大的变化之一是 删除传统上为类元数据,实体字符串和静态变量分配的堆的permgen部分。传统上,这需要开发人员使用能够加载大量类的应用程序(使用企业容器的应用程序常见的东西)来专门优化和调整堆的这一部分。多年来,这已成为许多OutOfMemory异常的来源,因此如果JVM(大多数情况下)是一个非常好的补充,那么让它(大多数情况下)要小心。即便如此,这本身可能不会减少开发人员将他们的应用程序分离到多个JVM的浪潮。
---------------
这个老外讲的很好《Java Generics》
《Java Stack + Heap with Reference & Instance Variables》
垃圾收集器 - 串行与并行与CMS垃圾收集器 - 串行与并行与CMS与G1(以及Java 8中的新功能)与G1(以及Java 8中的新
JVM GC 之「AdaptiveSizePolicy」实战
AdaptiveSizePolicy(自适应大小策略) 是 JVM GC Ergonomics(自适应调节策略) 的一部分。
如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。
使用 jinfo -flags pid 即可查看默认配置的 GC 算法。
上文提到,该算法默认开启 AdaptiveSizePolicy。
即使 SurvivorRatio 的默认值是 8,但年轻代三个区域之间的比例仍会变动。
这个问题,可以参考来自R大的回答
vivorRatio没用?
-XX:SurvivorRatio = <N>
每个幸存者空间的大小与伊甸园空间大小的比率,其中<n>是比率。以下等式可用于确定使用-XX指定的比率的幸存者空间大小:SurvivorRatio = <n>:幸存者大小= -Xmn <n> /( - XX:SurvivorRatio = <n> + 2)其中-Xmn <n>是年轻代空间的大小,-XX:SurvivorRatio = <n>是指定为比率的值。等式中+ 2的原因是存在两个幸存者空间。指定为比率的值越大,幸存者空间大小越小。-XX:当你想要使用并发垃圾收集器显式调整幸存者空间大小来操纵对象老化时,应该使用SurvivorRatio = <n>
-XX:SurvivorRatio = <n>不应与启用了自适应大小调整的吞吐量收集器一起使用。默认情况下,通过-XX:+ UseParallelGC或-XX:+ UseParallelOldGC使用吞吐量垃圾收集器启用自适应大小调整。如果需要初始幸存者比率来开始吞吐量垃圾收集器的自适应大小调整,则应使用-XX:InitialSurvivorRatio = <n>。
----
=================
经典著作《深入java虚拟机第二版》
Java虚拟机内部的第5章
数据类型
Java虚拟机通过对某些类型的数据执行操作来计算。数据类型和操作都由Java虚拟机规范严格定义。数据类型可以分为一组基本类型和引用类型。基本类型的变量包含原始值,引用类型的变量保存参考值。引用值是指对象,但不是对象本身。相反,原始值不是指任何东西。它们本身就是实际数据。您可以在图5-4中看到Java虚拟机的数据类型系列的图形描述。
Java编程语言的所有原始类型都是Java虚拟机的原始类型。尽管boolean
有资格作为Java虚拟机的原始类型,但指令集对它的支持非常有限。当编译器将Java源代码转换为字节码时,它使用int
s或byte
s来表示boolean
s。在Java虚拟机中,false
由整数零和 true
任何非零整数表示。涉及boolean
值的操作使用int
s。数组boolean
作为数组访问 byte
,但它们可以在堆上表示为数组 byte
或作为位字段。
Java编程语言的原始类型,而不是Java虚拟机boolean
的数字类型。数字类型之间划分 整数类型:byte
,short
, int
,long
,和char
,以及浮点类型:float
和double
。与Java编程语言一样,Java虚拟机的原始类型在任何地方都具有相同的范围。long
Java虚拟机中的A 总是充当64位带符号的二进制补码数,独立于底层主机平台。
Java虚拟机使用Java程序员不可用的另一种基本类型:returnAddress
类型。该原始类型用于实现 finally
Java程序的子句。returnAddress
第18章“最后条款”中详细描述了该类型的使用 。
Java虚拟机的引用类型巧妙地命名reference
。类型的值有reference
三种形式:类类型, 接口类型和数组类型。所有三种类型都具有对动态创建的对象的引用的值。类类型的值是对类实例的引用。数组类型的值是对数组的引用,数组是Java虚拟机中的完整对象。接口类型的值是对实现接口的类实例的引用。另一个参考值是 null
值,表示reference
变量不引用任何对象。
Java虚拟机规范定义了每种数据类型的值范围,但未定义其大小。用于存储每个数据类型值的比特数是各个实现的设计者的决定。Java虚拟机数据类型的范围如表5-1所示。有关浮点范围的更多信息,请参见第14章“浮点运算”。
类加载器子系统涉及Java虚拟机的许多其他部分以及java.lang
库中的几个类。例如,用户定义的类加载器是其类来自的常规Java对象java.lang.ClassLoader
。类的方法ClassLoader
允许Java应用程序访问虚拟机的类加载机制。此外,对于Java虚拟机加载的每种类型,它都会创建一个类的实例 java.lang.Class
来表示该类型。与所有对象一样,用户定义的类加载器和类实例Class
驻留在堆上。加载类型的数据驻留在方法区中。
方法区
有关已加载类型的信息存储在称为方法区域的内存的逻辑区域中。当Java虚拟机加载类型时,它使用类加载器来定位相应的类文件。类加载器读入类文件 - 二进制数据的线性流 - 并将其传递给虚拟机。虚拟机从二进制数据中提取有关类型的信息,并将信息存储在方法区域中。类中声明的类(静态)变量的内存也取自方法区域。
堆
只要在正在运行的Java应用程序中创建类实例或数组,就会从单个堆中分配新对象的内存。由于Java虚拟机实例中只有一个堆,因此所有线程都共享它。因为Java应用程序在其“自己的”独占Java虚拟机实例中运行,所以每个运行的应用程序都有一个单独的堆。两个不同的Java应用程序无法在彼此的堆数据上进行践踏。但是,同一应用程序的两个不同线程可以互相压缩彼此的堆数据。这就是您必须关注Java程序中对对象(堆数据)的多线程访问的正确同步的原因。
Java虚拟机有一条指令,用于在堆上为新对象分配内存,但没有释放该内存的指令。正如您无法在Java源代码中显式释放对象一样,您无法在Java字节码中显式释放对象。虚拟机本身负责决定是否以及何时释放正在运行的应用程序不再引用的对象占用的内存。通常,Java虚拟机实现使用垃圾收集器来管理堆。
对象表示
Java虚拟机规范没有说明如何在堆上表示对象。对象表示 - 堆和垃圾收集器的整体设计的一个不可或缺的方面 - 是实现设计者的决定
必须以某种方式为每个对象表示的主要数据是在对象的类及其所有超类中声明的实例变量。给定对象引用,虚拟机必须能够快速定位对象的实例数据。此外,在给定对象的引用的情况下,必须有某种方法来访问对象的类数据(存储在方法区域中)。因此,为对象分配的内存通常包括某种指向方法区域的指针。
一种可能的堆设计将堆分为两部分:句柄池和对象池。对象引用是指向句柄池条目的本机指针。句柄池条目有两个组件:指向对象池中实例数据的指针和指向方法区域中类数据的指针。这种方案的优点是它使虚拟机可以轻松地对抗堆碎片。当虚拟机移动对象池中的对象时,它只需要使用对象的新地址更新一个指针:句柄池中的相关指针。这种方法的缺点是每次访问对象的实例数据都需要解除引用两个指针。这种对象表示方法如图5-5所示。HeapOfFish applet以交互方式演示这种堆,
另一种设计使对象引用指向包含对象实例数据和指向对象类数据的指针的数据包的本机指针。此方法只需要解除引用一个指针来访问对象的实例数据,但会使移动对象更复杂。当虚拟机移动对象以对抗此类堆的碎片时,它必须更新运行时数据区域中任何位置的对该对象的每个引用。这种对象表示方法如图5-6所示。
Java堆栈
启动新线程时,Java虚拟机会为该线程创建新的Java堆栈。如前所述,Java堆栈将线程的状态存储在离散帧中。Java虚拟机只在Java Stacks上直接执行两个操作:它推送和弹出帧。
线程当前正在执行的方法是线程的当前方法。当前方法的堆栈帧是当前帧。定义当前方法的类称为当前类,当前类的常量池是 当前常量池。在执行方法时,Java虚拟机会跟踪当前类和当前常量池。当虚拟机遇到对存储在堆栈帧中的数据进行操作的指令时,它会在当前帧上执行这些操作。
当线程调用Java方法时,虚拟机会创建一个新帧并将其推送到线程的Java堆栈上。然后,这个新帧成为当前帧。当该方法执行时,它使用该帧来存储参数,局部变量,中间计算和其他数据。
方法可以以两种方式之一完成。如果方法通过返回完成,则称其具有 正常完成。如果它通过抛出异常完成,则说它突然完成。当方法完成时,无论是正常还是突然,Java虚拟机都会弹出并丢弃方法的堆栈帧。然后,先前方法的帧成为当前帧。
线程的Java堆栈上的所有数据都是该线程专用的。线程无法访问或更改另一个线程的Java堆栈。因此,您无需担心在Java程序中同步多线程访问本地变量。当线程调用方法时,方法的局部变量存储在调用线程的Java堆栈的框架中。只有一个线程可以访问这些局部变量:调用该方法的线程。
与方法区域和堆一样,Java堆栈和堆栈帧在内存中不需要是连续的。帧可以在连续堆栈上分配,也可以在堆上分配,或者两者的某种组合。用于表示Java堆栈和堆栈帧的实际数据结构是实现设计者的决定。实现可以允许用户或程序员指定Java堆栈的初始大小,以及最大或最小大小。
本机方法堆栈
除了由Java虚拟机规范定义并且如前所述的所有运行时数据区域之外,正在运行的Java应用程序可以使用由本机方法创建的或用于本机方法的其他数据区域。当线程调用本机方法时,它进入一个新的世界,其中Java虚拟机的结构和安全限制不再妨碍其自由。本机方法可能会访问虚拟机的运行时数据区域(它取决于本机方法接口),但也可以执行其他任何需要的操作。它可以使用本机处理器内的寄存 以上是关于从Jdk8到Jdk12的Java虚拟机垃圾回收(垃圾收集)相关论文和官方网站集锦的主要内容,如果未能解决你的问题,请参考以下文章 从 JDK 8 到 JDK 18,Java 垃圾回收的十次进化