hotspot虚拟机
1、默认的设置有:
垃圾优先(G1)收集器
GC线程的最大数量受堆大小和可用CPU资源限制
初始堆大小为物理内存的1/64
最大堆大小为物理内存的1/4
使用C1和C2的分层编译器
Java HotSpot VM垃圾收集器配置为优先满足以下两个目标之一:最大暂停时间和应用程序吞吐量
最大暂停时间目标:是通过命令行选项-XX:MaxGCPauseMillis=
吞吐量目标: -XX:GCTimeRatio=nnn指定,垃圾回收时间与应用程序时间之比为1 /(1+ nnn)。例如,-XX:GCTimeRatio=19将垃圾收集目标设置为目标1/20或总时间的5%。
垃圾收集所花费的时间是所有垃圾收集引起的暂停的总时间。如果没有达到吞吐量目标,那么垃圾收集器可能采取的一种措施是增加堆的大小,以便在两次收集暂停之间花费在应用程序中的时间更长。
如果达到了吞吐量和最大暂停时间目标,则垃圾收集器将减小堆的大小,直到不能满足其中一个目标(始终是吞吐量目标)
使用命令行记录gc的日志: -verbose:gc / -Xlog:gc
-------------------------------------------------------------
从启动到gc的时间点 gc的类型 gc发生的原因 “ GC之前使用”->“ GC之后使用”(“堆大小”) (gc开始时间,结束时间) gc持续的时间
[15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms
[16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms
[16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms
-------------------------------------------------------------
heap大小影响 :
影响垃圾收集性能的最重要因素是总可用内存。由于收集是在世代填满时发生的,因此吞吐量与可用内存量成反比。
参数:
–XX:NewRatio (默认值为:2,表示老一代对年轻一代的相对大小,设置Yong和Old的比例,比如值为2,则Old Generation是 Yong Generation的2倍,即Yong Generation占据内存的1/3)
-XX:NewSize (默认值为: 1310M)
-XX:MaxNewSize (默认值为:无限大,设置Yong Generation的最大值大小)
-XX:SurvivorRatio (默认值为8,SurvivorRatio=6将Eden和Survivo空间之间的比例设置为1:6)
-XX:MinHeapFreeRatio=
-XX:MaxHeapFreeRatio=
通常将-Xms和设置-Xmx为相同的值(由于从操作系统申请和释放内存可能会导致不必要的延迟),同时如果处理器增大了则对应的堆内存大小也要相应增大
以下是服务器应用程序的一般准则:
1>首先确定您可以负担得起的虚拟机的最大堆大小。然后,针对年轻一代绘制性能指标,以找到最佳设置。
2>请注意,最大堆大小应始终小于计算机上安装的内存量,以避免过多的页面错误和崩溃。
3>如果总堆大小是固定的,则增加年轻代大小需要减少旧代大小。保留足够大的旧一代以容纳应用程序在任何给定时间使用的所有实时数据,以及一定数量的空闲空间(10%到20%或更多)。
4>遵循先前对旧一代的约束:
给年轻一代留下很多空间。
随着处理器数量的增加,可以增加年轻代的大小,因为分配可以并行化。
其中线相连的表示可以组合使用,其中横线上方的是年轻代的收集器,下方是老年代的收集器,其中G1是混合收集器。
使用的方式: -XX:+UseSerialGC
特点:串行收集器使用单个线程来执行所有垃圾收集工作、适合单处理器计算机
是可以控制吞吐量的收集器,其中提供了两个参数:
-XX:MaxGCPauseMillis: 控制最大垃圾收集停顿时间
-XX:GCTimeRatio: 设置吞吐量大小的
-XX:+UseAdaptiveSizePolicy: 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时
间或者最大的吞吐量
CMS使用增量更新算法实现待清除对象的标记。
-XX:CMSInitiatingOccu-pancyFraction:该参数表示设置为老年代内存占用达到阈值就触发。
使用方式:-XX:+UseG1GC
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1收集的过程:
·初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器解决对象跨代引用的记忆集(Remember Set)和卡表(Card Table)实现,使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作;
G1使用原始快照(SATB)算法来实现对象标记。垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”
G1垃圾收集器的默认参数设置:
-XX:MaxGCPauseMillis=200 最大暂停时间的目标。
-XX:GCPauseTimeInterval= 最大暂停时间间隔的目标。默认情况下,G1不设置任何目标,允许G1在极端情况下连续执行垃圾收集。
-XX:ParallelGCThreads= 垃圾回收暂停期间用于并行工作的最大线程数。这是通过以下方式从运行VM的计算机的可用线程数中得出的:如果进程可用的CPU线程数少于或等于8,则使用该数量。否则,将线程数的八分之八增加到最终线程数。在每个暂停开始时,使用的最大线程数进一步受到最大总堆大小的限制:
-XX:HeapSizePerGCThread 对于Java堆容量,G1不会使用多个线程。
-XX:ConcGCThreads= 用于并发工作的最大线程数。默认情况下,此值
-XX:ParallelGCThreads 除以4。
-XX:+G1UseAdaptiveIHOP
-XX:InitiatingHeapOccupancyPercent=45 用于控制初始堆占用的默认值指示该值的自适应确定已打开,并且对于前几个收集周期,G1将使用旧一代的占用率的45%作为标记开始阈值。
-XX:G1HeapRegionSize= 基于初始和最大堆大小的堆区域大小集。因此该堆包含大约2048个堆区域。堆区域的大小可以从1到32 MB不等,并且必须为2的幂。
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60 总体上,年轻代的大小在这两个值之间变化,以当前使用的Java堆的百分比表示。
-XX:G1HeapWastePercent=5 集合中允许的未回收空间设置为候选百分比。如果收集集合候选中的可用空间低于该间隔,则G1停止空间回收阶段。
-XX:G1MixedGCCountTarget=8 多个集合中空间回收阶段的预期长度。
-XX:G1MixedGCLiveThresholdPercent=85 在此空间回收阶段,不会收集活动对象占用率高于此百分比的旧区域。
Shenandoah也是基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的Region。Shenandoah(目前)是默认不使用分代收集的,不存在基于年轻代的Region和老年代的Region。
Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题(见3.4.4节)的发生概率。连接矩阵可以简单理解为一张二维表格,如果Region N有
对象指向Region M,就在表格的N行M列中打上一个标记,如图3-15所示,如果Region 5中的对象Baz引用了Region 3的Foo,Foo又引用了Region 1的Bar,那连接矩阵中的5行3列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些Region之间产生了跨代引用
Shenandoah的工作工程:
·初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。
·并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
·最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
·并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
·并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小。
·初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指
向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未
做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收
集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的
停顿。
·并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
·最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
·并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。
Shenandoah使用了转发指针(Brooks Pointer)来实现并行整理。在之前要做类似的并发操作,通常是在被移动对象原有的内存上设置保护陷阱(Memory Protection Trap),一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中段,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上。虽然确实能够实现对象移动与用户线程并发,但是如果没有操作系统层面的直接支持,这种方案将导致用户态频繁切换到核心态,代价是非常大的,不能频繁使用。
Brooks提出的新方案不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。当对象被拷贝到其他的Region之后,只需要修改转发指针使旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作。由于并发更新可能导致问题所以使用了CAS。
覆盖全部对象访问操作,Shenandoah不得不同时设置读、写屏障去拦截Shenandoah在读、写屏障中都加入了额外的转发处理,尤其是使用读屏障的代价,这是比写屏障更大的。所以Shenandoah将基于内存屏障的模型改为基于引用访问屏障,只拦截访问引用对象的读写操作,而非引用类型的读写则不存在该限制。
使用方式:-XX:+UseZGC
ZGC的内存模型:
ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有如图3-19所示的大、中、小三类容量:
·小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
·中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
·大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。
ZGC的堆内存布局:
ZGC并发整理算法的实现:
ZGC收集器有一个标志性的设计是它采用的染色指针技术(Colored Pointer,其他类似的技术中可能将它称为Tag Pointer或者Version Pointer)。染色指针是一种直接将少量额外的信息存储在指针上的技术。
在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节。实际上,基于需求(用不到那么多存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶体管)的考虑,在AMD64架构中支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存有256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚至只支持44位(16TB)的物理地址空间。
尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)
ZGC运作过程:
·并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
·并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
·并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(SelfHealing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。
·并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图[9]的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
本文摘自<深入理解Java虚拟机:JVM高级特性和最佳实践第三版>