Java内存结构

如下图,JVM虚拟机主要内存结构:

  • 堆:存放对象的地方,负责管理对象,管理对象的存活周期
  • 栈:方法执行的地方,每个线程都有一个栈,所以又叫线程栈,负责运行,栈中每个方法叫栈帧
  • 本地方法栈:存放Navtive方法的地方
  • 方法区:1.8之前是永久代,之后是元空间,区别在于元空间的内存分配是在计算机内存中,而非JVM堆中,负责存放类信息,静态变量,常量等不变的数据
  • 程序计数器:负责存放Java字节码地址,即线程切换之后的恢复原位继续执行,负责控制

而调优主要是针对中的内存进行优化

在这里插入图片描述

下图是堆布局:

执行流程:

  1. 新对象会在Eden中分配空间(大对象会直接在Old中分配)
  2. 如果发现Eden空间不够了,则会发生一次MinorGC,如果空间够了就在Eden分配
  3. 如果还不够,就会发生一次FullGC,如果够了就分配,还不够就会OOM

其中:

  • MinorGC:收集Eden中的垃圾对象,然后将存活的对象存放到S0中,下一次MinorGC就会吧存活队像连同S0中的对象一起放到S1中,所有对象都有一个对象头,对象头中有一个标记是分代年龄,如果某次垃圾回收没有回收掉该对象,对应的分代年龄就会+1,如果该年龄超过15就会被移到Old中
  • FullGC:对Old中的对象进行回收,操作时间较长,尽量避免

在这里插入图片描述

GC机制的基本原则:分代收集

调优

整个堆的设计原则就是按照年龄(存活的时长)来确定对象所在的区域,包括1.8之前的永久代,是因为类信息常量这些信息几乎是永远不会变动永远不会被回收的,所以在1.8之后就被划分到堆之外了,因为堆中是可变的对象,所以在进行JVM调优的时候只需要记住一个原则,应用程序中的各个对象的存活时长决定了对中各个区域的大小,也就是说,每个应用程序都有最适合自己的堆模型,所以调优最本质的问题在于了解自己程序有多少端周期对象和并发量(对应的Eden的大小),有多少是活跃对象(对应老年区的大小),以及使用什么样的回收算法以达到系统稳定的目的

计算活跃数据大小(元空间不谈)

如前所述,活跃数据大小是应用程序运行于稳定态时,长期存活的对象在Java堆中占用的空间大小。换句话说,活跃数据大小是应用程序运行于稳定态, FullGC之后Java雄中老年代占用的空间大小。

所以如果是计算应用程序中的这些活跃对象大小有点难度的话,就可以将程序运行到稳定状态时查看old大小以确定活跃数据的大小

初始堆空间大小配置

根据活跃数据大小定义初始Java堆大小时,还需要考虑 Full GCI的影响,推荐的做法是基于最差延迟进行估算。

通用法则之一,将Java雄的初始值-Xms和最大值-Xmx设置为老年代活跃数据大小的3-4倍。Full GC中,如果老年代空间占用了29511KB(大约295MB),因此活饫数据约为295MB。所以,该应用程序建议堆的初始值和最大值应该介于885MB到1180MB(即4倍活跃数据大小)之间 Xms1180m-Xmx1180m。

补充法则,新生代空间应该为老年代空间活跃数据的1~15倍。Full GC中,新生代为295MB。因此新生代的建议大小为295MB-442MB;

如果Java的初始值及最大值为活跃数据大小的3-4倍、新生代为活跃数据的1-1.5倍时,老年代应设置为活跃数据大小的2~3倍

空间命令行选项占用倍数
Java堆-Xms和-Xmx3-4倍FullGC后的老年代空间占用量
方法区-XX:PermSIze和-XX:MaxPermSize1.2-1.5倍FullGC后的永久代空间占用量
新生代-Xmn1-1.5倍FullGC后的老年代占用量
老年代Java堆大小减新生代大小2-3倍FullGC后的老年代空间占用量

常用的空间调优参数:

参数说明
-Xmx[g|m|k]堆最大内存大小
-Xms[g|m|k]堆初始化内存大小
-Xmn[g|m|k]年轻的代大小
-Xss[g|m|k]设置每个线程堆栈大小
-XX:SurvivorRatio=4新生代中eden和S0/S1空间的比例,Eden:S0:S1=4:1:1
-XX:NewRatio=4年轻代与老年代在堆结构的占比, 新生代占1,老年代占4,年轻代占整个堆的1/5
-XX:MetaspaceSize=[g|m|k]元空间初始值
-XX:MaxMetaspaceSize=[g|m|k]元空间最大值
-XX:NewSize=[g|m|k]新生代初始内存的大小,应该小于-Xms的值
-XX:MaxNewSize=[g|m|k]新生代最大内存的大小
-XX:MaxTenuringThreshold=分代年龄大小提升到老年区的阈值

调优延迟/响应性

定义好初始Java堆的大小,就需要观察垃圾收集器堆延迟性的影响。

  • 测量MinorGC的持续时间
  • 统计MinorGC的次数
  • 测量FullGC的最差持续时间
  • 统计最差情况下,FullGC的频率

测量GC的持续时间及频率对优化Java雌的大小至关重要。 Minor GC的持续时间及频率决定了优化后新生代的大小。最差情况下的 Full GC持续时间及顷率决定了老年代的大小以及垃圾收集器的切换:是否需要从 Throughput l收集器(通过-XX:+ UseParallelt0ldCC或-XX:+ UseParallelgc选项启用)转向CMS收集器(通过-XX:+ UseConcMarkSweepGC或-XX:+ UseParallelGC选项启用)。如果 Throughput收集器的 Full GC的最差垃圾收集持续时间和频率远远不能满足应用程序的
延迟性要求,那么就应该考虑切换到CMS。一旦发生切换,同样也需要针对CMS进行调优。

性能的需求:

  • 应用程序可接受的平均停滞时间
  • 可接受的MinorGC频率
  • 应用程序干系人接受的应用程序的最大停顿时间
  • 应用程序干系人可接受的最大停顿发生的频率

根据垃圾收集的统计数据、 Minor GC的持续时间和颖率可以确定新生代空间的大小。下面是通过垃圾收集统计数据设置新生代大小的一个示例。

Minor GCT需要的时间与新生代中可访问的对象数直接相关,通常情况下,新生代空间越小,Minor GC持续的时间越短。不考虑这对于 Minor GC持续时间的影响,减小新生代空间又会增大Minor GC的颖率。这是因为以同样的对象分配频率,较小的新生代空间在很短的时间内就会被填满,増大新生代空问可以减少 Minor GC的频率

分析GC数据时,如果发现 Minor GC的间隔时间过长,修正的方法是减少新生代空间。如果Minor GC颖率太高,修正的方法是増加新生代空间。

优化年轻代的大小

为了更清晰地阐明这个问题,我们看一个示例,Minor GC使用了下面的 Hotspot VM命令行选项

-Xms6144m -Xmx6144m -Xmn2048m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UseParalleloldGC

如下图:
Minor GC平均持续时间是0.54秒。 Minor GCI的平均频率为每2.147秒一次。计算平均持时间和頻率时, Minor GC的次数越多,平均持续时间及频率的估计越准确。另外,使用应用程序运行于稳定阶段时的 Minor GC值也是非常重要的。

如果应用程序的延迟性要求是40ms,而此时的54ms是不满足延迟要求的,所以可以将年轻代变小以缩短MinorGC时间。
在这里插入图片描述
使用下面调整过的参数:

-Xms5940m -Xmx5940m -Xmn1844m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UseParalleloldGC

堆大小从6144m减少到了5940m,新生代为1844m ,减少新生代大小10%的同时保持老年代不变(通过计算得知上面老年代大小也为6144-2048=4096)这样就可以通过减少新生代大小以减少MinorGC停顿时间。

而如果能容忍的停顿时间操作还比54ms大,则可以适当增加年轻代的大小。

无论扩展还是缩祓新生代堆的大小,都需要采集垃圾收集时的统计数据、根据应用程序的延迟性要求重新计算 Minor GC的平均持续时间。改变新生代的大小可能要经历几个迭代过程。

优化老年代的大小

这一步的目标是评估 Full GC引人的最差停滞时间以及 Full GC的频率

同前一节一样,老年代的优化也需要采集垃圾收集的统计数据。我们关注的内容是 Full GC持续的时同和顺率。发生于稳定态的 Full Gc的持续时间是应用程序的最差 Full GC停滯时间。如果多个 Full GC在稳定态发生,就按平均最差停滞时间计算。往往取样的数据越多,预测的结果越好。

如下图是一个示例,包含了2个FullGC持续时间:

可以了解到如下内容:

  • Java堆的大小是6291456K=6144M
  • 新生代的大小是2097152K=2048M
  • 老年代的大小就是两者之差4096M

可以看到活跃数据大小约为1401197K大概是1370M意味着老年代有2762M的空间

需要多长时间才能填满老年代2762的空间取决于新生代到老年代的提升率,可以通过每次MinorGC后新生代的空间计算得出。

每次MinorGC之后,老年代的占用空间分别为:

1588635K,第一次MinorGC
1611428K,第二次MinorGC
1632106K,第三次MinorGC
1653117K,第四次MinorGC

每次GC时候老年代的空间分别为:
22793K
20678K
21011K

每次MinorGC平均提升为21494K,约为21M

而MinorGC的频率大概是两秒一次,所以,每秒提升大概10M的空间。所以填充玩2762M可用的老年代空间时间约为4.5分钟。

如果预期或观测到 Full GC的频率已经远远不能达到应用程序的最差 Full GC频率要求,就应该增大老年代空间的大小。这个方法可以帮助降低 Full GC的频率。增加老年代空间的大小时注意保持新生代空间大小恒定。

在这里插入图片描述

下面是几次GC日志:

在这里插入图片描述

CMS调优延迟

使用CMS收集器时,老年代垃圾收集线程与应用程序线程能实现最大的并行度。这为我们同时降低最差延迟出现的频率以及最差延迟的持续时间,避免发生长时间的GC提供了机会。CMS并不进行压缩,所以这一效果主要是通过避免老年代空间发生Stop-The- World压缩式垃圾来收集实现的。一旦老年代溢出就会触发Sop-The-Word压缩式垃圾收集。

调优CMS收集器的目的是避免发生Sop-The- Worlde的压缩式GC。然而,这实际上是件“说易行难”的事情在某些应用部著下甚至是不可避免的,特別是内存占用有限制的情况,与其他Hot Spot VM垃圾收集器比较起来。CMS收集器需要更细粒度的调优,尤其是对新生代空间大小进行更细致地调整,以及在需要时对何时启动老年代并行垃圾收集周期进行调整。

CMS在老年代空间从空闲列表中分配内存。与之相反, Throughput收集器只需要在线程本地分配的提升缓存中移动指针即可另外,由于老年代垃圾收集线程能够与应用程序线程实现最大程度的并发执行。所以可以预期应用程序的吞吐量会更低。然而,发生这种最差延迟的几率并不是很大,因为应用程序运行时老年代中的不可达对象会进行垃圾收集,从而避免了老年代空间被填满。

使用CMS时,如果老年代空间用尽,就会触发一个单线程Sop-The-Word压缩式的垃圾收集。相对于 Throughput收集器的 Full GC而言,CMS垃圾收集通常的持续时间更长。因此,采用CMS的绝对最差延迟要比 Throughput l收集器的最差延迟时间长。老年代空间耗尽并因此触发Stop-The- World压缩式垃圾收集时,由于应用程序长时间无法响应,会引起应用程序干系人的关注。因此,尽量避免用尽老年代空间是非常重要的。从 Throughput收集器迁移到CMS收集器时需要遵守的一个通用原则是,将老年代空间增大209%~30%,这样才能更有效地运行CMS收集器。

几方面的因素使得CMS收集器的调优非常具有挑战性。一个是对象从新生代提升至老年代的速率。另一个是并行老年代垃圾收集线程回收空间的速率。第三个是由于CMS收集器回收位于对象之间的垃圾对象而造成老年代空间的碎片化。回收操作会在老年代的可达对象之间形成空洞,从而引起可用空间的碎片化

有多种方法都可以解决碎片化问题。其中之一是压缩老年代空间。通过Stop-The- World压缩式GC对老年代空间进行压缩。如前所述,Stop- The-world压缩式GC耗时较长,是应该尽量避免的事件,因为对于应用程序的最差延迟时间,它很可能是最大也是最重要的贡献者。这个方法不能从根本上解决碎片化问题,但是它可以推迟老年代空间碎片化到必须进行压缩的时间。通常情况下,老年代空间的内存越多、处理碎片压缩的时间就越长。应用程序生命周期中努力达到的一个目标是,让老年代空间大到足以避免由雄内存碎片引起的Stop-The- World压缩。换句话说,就是“为GC申请最大内存原则”。处理碎片问题的另一个方法是减少对象从新生代提升至老年代的比率,即“ Minor GC回收原则”。

-XX:+PrintTenuringDistribution

打开可以监控晋升的分布或者对象年龄分布,并以此为依据确定最大晋升阈值值。

XX:+ PrintTenuringDistribution会输出每次 MinorGC时晋升分布的情况。它也可以和其他的垃圾收集命令行选项,例如-XX:+ PrintGCDateStamps、-XX:+PrintGCTimeStamps或-XX:+PrintGCDetails配合使用。对 Survivor空间的有效对象老化进行微调时,应该使用选项-XX+PrintTenuringDistribution在垃圾收集日志中包含晋升分布的统计信息。同样,如果需要在生产环境中判断一个应用程序事件是否源于一次Stop-The- World压缩式垃圾收集,往往也需要获取晋升分布的日志信息,使用该选项是非常有帮助的。下面是使用-Xx:+ Printtenuringdi stribution输出的一个例子:

Desired survivor size 8388608 bytes, new threshold 1(max 15)
-age 1: 16690480 bytes, 16690480 total

这个例子中,最大晋升國值设置为15,由(max15)标识。通过 new threshold1可以知道虚拟机内部计算出的晋升阈值为1. Desired survivor size8388608 bytes是 Survivor空间的大小乘以目标存活率得到的空间大小。目标存活率是 Hotspot VM预计目标空间在 Survivor’空间中占用的百分比。标题信息之下是对象年龄的列表。每个年龄的对象及其占用的空间大小单独列为一行,本例中,年龄为1的对象大小为16690480字节。同时,在每一行中也会列出对象总的大小(字节数)。如果出现多年龄行的情况,总大小是该年龄行及其之前所有行对象大小的累计之和。后面的例子中有若干个年龄行的输出示例。

前文的示例中,期望 Survivor空间大小(8388608)远小于总的存活对象大小(16690480),导致 Survivor。空间溢出,即最终 Minor GC。将一些对象提升至老年代 Survivor。空间溢出表明 Survivor空间过小。另外,由于最大晋升阈值为15,而 Hotspot VM内部计算出的晋升國值为1,这进一步验证了 Survivor空间过小的问题。

调整 Survivor空间容量一个应该谨记于心的重要原则:调整 Survivor空间容量时,如果新生代空间大小不变,增大Survivor空间会减少Eden空间;而减少Eden空间会增加 Minor GC的频率。因此,为了同时满足应用程序 Minor Gc频率的要求,就需要增大当前新生代空间的大小;即增大Survivor空间大小时,Eden空间的大小应该保持不变。换句话说,每当 Survivor空间増加时,新生代空间都应该增大。如果可以增大 Minor GC的频率,你可以选择用一部分Eden空间来增大 Survivor空间,或者直接增大新生代空间大小。如果内存足够,相对于减少Eden空间,增加新生代大小通常是更好的选择。保持Eden空间大小恒定, Minor GC的频率就不会由于 Survivor空间増大而发生变化。

一旦包含Eden空间和 Survivor空间在内的新生代空间优化完成, Minor GC引人的延迟达到应用程序的要求之后,我们就可以把精力转向CMS收集器的调优上,减小最差情况的延迟并最小化最差延迟发生的率。这一步的目标是维持空闲老年代空间的恒定,并由此避免发生Stop-The-Word压缩式垃圾收集。

Stop-The- World压缩式垃圾收集是引入延迟的最大的垃圾收集。在一些应用程序中,这可能是无法完全避免的,但是这一节介绍的方法至少能降低它们发生的频率成功的CMS收集器调优要能以对象从新生代提升到老年代的同等速度对老年代中的对象进行垃圾收集。达不到这个标准则称之为“失速”( Lost the Race)失速的结果就会发生Stop-The-Word压缩式垃圾收集。避免失速的关键是要结合足够大的老年代空问和足够快地初始化CMS垃圾收集周期,让它以比提升速率更快的速度回收空间

CMS周期的初始化基于老年代空同的占用情况。如果CMS周期开始得太晚,就会发生失速如果它无法以足够快的速度回收对象,就无法避免老年代空间用尽。但是CMS周期开始得过早又会引起无用的消耗,影响应用程序的春吐量。通常,早启动CMS周期要比晚启动CMS好,因为启动太晚的结果比启动过早的结果要恶劣得多。Hot Spot VM会尝试自适应地计算在空间占用多大时开启CMS收集周期。但它做得并不是很好,某些情況下,无法避免Sop-The- World缩式垃圾收集。

-XX:CMSInitiatingOccupancyFraction=<percent>

设定的值是CMS垃圾收集周期在老年代空间占用达到多少百分比时启动。例如,如果你希望CMS周期在老年代空间占用达到65%时开始,可以设置-Xx:Cmsinitiatingoccupancyfraction=65。另一个可以与-XX:Cmsinitlatingoccupancyfraction=一起使用另一个 Hotspot命令行选项是-XX:+UseCMSInitiatingOccupancyOnly

-XX:+UseCMSInitiatingOccupancyOnly告知 Hotspot VM总是使用-XX:CMSInitiatingOccupancyFraction设定的值作为启动CMS周期的老年代空间占用阈值。不使用-XX:+UseCMSInitiatingOccupancyOnly, Hotspot VM仅在启动的第一个CMS周期里使用-XX:CMSInitiatingOccupancyFraction设定的值作为占用比率,之后的周期中又转向自适应地启动CMS周期,即第次CMS周期之后就不再使用-XX:CMSInitiatingOccupancyFraction设定的值。

常用的垃圾收集器调优参数:

参数说明
-XX:PrintTenuringDistribution=晋升阈值
-XX:+UseParallelOldGC使用Throughput收集器
-XX:+UseParallelGC使用Throughput收集器
-XX:+UseConcMarkSweepGC使用CMS收集器
-XX:+ScavengeBeforeFullGCFullGC时是否老年代和新生代都收集
-XX:CMSInitiatingOccupancyFraction=通知HotSpot在更早的时间启动CMS垃圾收集周期
-XX:+UseCMSInitiatingOccupancyOnly告诉垃圾收集器总是使用CMSInitiatingOccupancyFraction阈值作为启动收集,即第一次使用,以后自适应
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐