【JVM 学习笔记 04】:JVM 的场景模拟和优化案例

一、基于G1垃圾回收器的百万级用户在线教育平台的性能优化

1.1 系统背景

百万级注册用户的在线教育平台,主要目标用户群体是几岁到十几岁的孩子,注册用户大概是几百万的规模,日活用户规模大概在几十万。
系统的业务流程其实也不复杂,我们可以排除掉一些选课、排课、浏览课程详情以及付费购买之类的低频的行为。

这样的一个平台,使用人群是幼儿园的孩子到中小学的孩子。他们平时白天都要上学,一般也就是晚上放学之后到八九点钟的样子,是最活跃使用这个平台的时候,还有就是周末也是最活跃使用这个平台的时候。

这里尤为关键的需要注意的,就是每天晚上那两三小时的高峰时期,几乎可以认为每天几十万日活用户都会集中在这个时间段来平台上上在线课程。所以这个晚上两三小时的时间段里,将会是平台每天绝对的高峰期,而且白天几乎没什么流量,可能99%的流量都集中在晚上。

1.2 系统核心业务流程

这样的一个系统,在上课的时候主要高频使用主打的是互动环节。这个游戏互动功能,一定会承载用户高频率、大量的互动点击
比如在完成什么任务的时候必须要点击很多的按钮,频繁的进行互动,然后系统后台需要接收大量的互动请求,并且记录下来用户的互 动过程和互动结果。

1.3 系统的高峰运行压力

核心点就是搞明白在晚上两三小时高峰期内,每秒钟会有多少请求,每个请求会连带产生多少对象,占用多少内存,每个请求要处 理多长时间。

大致估算一下,比如说晚上3小时高峰期内有总共60万活跃用户,平均每个用户大概会使用1小时左右来上课,那么每小时大概会有20万活跃用户同时在线学习。这20万活跃用户因为需要进行大量的互动操作,所以大致可以认为是每分钟进行1次互动操作,一小时内会进行60次互动操作。

那么20万用户在1小时内会进行1200万次互动操作,平均到每秒钟大概是3000次左右的互动操作,这是一个很合理的数字。
那么每秒钟要承载3000并发请求,根据经验来看,一般系统的核心服务需要部署5台4核8G的机器来抗住是差不多的,每台机器每秒钟抗个600请求,这个压力可以接受,一般不会导致宕机的问题。

那么每个请求会产生多少个对象呢?
一次互动请求不会有太复杂的对象,他主要是记录一些用户互动过程的,可能会跟一些积分之类的东西有关联。
所有大致估算一下,一次互动请求大致会连带创建几个对象,占据几KB的内存,比如我们就认为是5KB吧那么一秒600请求会占用3MB 。

1.4 基于G1垃圾回收器的优化

1.4.1 G1垃圾回收器的默认内存布局

采用4核8G的机器来部署系统,每台机器每秒会有600个请求会 占用3MB左右的内存空间。

那么假设我们对机器上的JVM,分配4G给堆内存,其中新生代默认初始占比为5%,最大占比为60%,每个Java线程的栈内存为1MB, 元数据区域(永久代)的内存为256M,此时JVM参数如下:

“-Xms4096M -Xmx4096M  -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC“

-XX:G1NewSizePercent”参数是用来设置新生代初始占比的,不用设置,维持默认值为5%即可。
-XX:G1MaxNewSizePercent”参数是用来设置新生代最大占比的,也不用设置,维持默认值为60%即可。
此时堆内存共4G,那么此时会除以2048,计算出每个Region的大小,此时每个Region的大小就是2MB,刚开始新生代就占5%的Region,可以认为新生代就是只有100个Region,有200MB的内存空间,如下图所示。

1.4.2 GC停顿时间的设置

-XX:MaxGCPauseMills”,他的默认值是200 毫秒
也就是说咱们希望每次触发一次GC的时候导致的系统停顿时间(也就是“Stop the World”)不要超过200毫秒,避免系统因为GC长时间卡死。

1.4.3 到底多长时间会触发新生代GC?

系统运行起来之后,会不停的在新生代的Eden区域内分配对象,按照之前的推算是每秒分配3MB的对象。
G1里是动态灵活的,他会根据你设定的gc停顿时间给你的新生代不停 分配更多Region,然后到一定程度,感觉差不多了,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设范围内。G1它本身是这样的一个运行原理,他会根据你预设的gc停顿时间,给新生代分配一些 Region,然后到一定程度就触发gc,并且把gc时间控制在预设范围内,尽量避免一次性回收过多的Region导致gc停 顿时间超出预期。

G1到底会分配多少个Region给新生代,多久触发一次新生代gc,每次耗费多 长时间,这些都是不确定的,必须通过一些工具去查看系统实际情况才知道,这个提前是无法预知的。

1.4.4 新生代gc如何优化?

对于G1而言,我们首先应该给整个JVM的堆区域足够的内存,比如我们在这里就给了JVM超过5G的内存,其中堆内存有4G的内存。
接着就应该合理设置“-XX:MaxGCPauseMills”参数

如果这个参数设置的小了,那么说明每次gc停顿时间可能特别短,此时G1一旦发现你对几十个Region占满了就立即触发新生代gc,然后gc频率特别频繁,虽然每次gc时间很短。

如果这个参数设置大了,那么可能G1会允许你不停的在新生代理分配新的对象,然后积累了很多对象了,再一次性回收几百个Region
此时可能一次GC停顿时间就会达到几百毫秒,但是GC的频率很低。比如说30分钟才触发一次新生代GC,但是每次停顿500毫秒。

这个参数到底如何设置,需要结合后续给大家讲解的系统压测工具、gc日志、内存分析工具结合起来进行考虑, 尽量让系统的gc频率别太高,同时每次gc停顿时间也别太长,达到一个理想的合理值。

1.4.5 mixed gc如何优化?

老年代在堆内存里占比超过45%就会触发。核心的点,在于“-XX:MaxGCPauseMills”这个参数。

假设“-XX:MaxGCPauseMills”参数设置的值很大,导致系统运行很久,新生代可能都占用了堆内存的60%了,此时才触发新生代gc。
那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你新生代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor 区域的50%,也会快速导致一些对象进入老年代中。

所以这里核心还是在于调节“-XX:MaxGCPauseMills”这个参数的值,在保证他的新生代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc。至于到底如何优化这个参数,都要结合后续大量工具的使用。

二、每秒10万并发的BI系统的优化

2.1 案例背景

所谓BI系统,有数十万甚至上百万的商家在你的平台上做生意,会使用这个平台系统。此时一定会产生大量的数据,然后基于这些数据我们需要为商家提供一些数据报表,比如:每个商家每天有多少访客?有多少交易?付费转化率是多少?简单来说,就是把一些商家平时日常经营的数据收集起来进行分析,然后把各种数据报表展示给商家的一套系 统。

这样的一个BI系统,大致的运行逻辑如下所示,首先从我们提供给商家日常使用的一个平台上会采集出来很多商家日常经营的数据,接着就可以对这些经营数据依托各种大数据计算平台,比如Hadoop、Spark、Flink等技术进行海量数据的计算,计算出来各种各样的 数据报表。然后我们需要将计算好的各种数据分析报表都放入一些存储中,比如说MySQL、Elastcisearch、HBase都可以存放类似的数据。最后一步,就是基于MySQL、HBase、Elasticsearch中存储的数据报表,基于Java开发出来一个BI系统,通过这个系统把各种存储好 的数据暴露给前端,允许前端基于各种条件对存储好的数据进行复杂的筛选和分析,如下图所示。
在这里插入图片描述

2.2 技术痛点

在少数商家的量级之下,这个系统是没多大问题的,运行的非常良好,但是问题恰恰就出在突然使用系统的商家数量开始暴涨的时候。
当商家的数量级达到几万的时候。此类BI系统的特点,就是在BI系统中有一种数据报表,他是支持前端页面有一个JS脚本,自动每隔几秒钟就发送 请求到后台刷新一下数据的,这种报表称之为“实时数据报表”。

假设仅仅就几万商家作为你的系统用户,很可能同一时间打开那个实时报表的商家就有几千个,然后每个商家打开实时报表之后,前端页面都会每隔几秒钟发送请求到后台来加载最新数据,基本上会出现你BI系统部署的每台机器每 秒的请求会达到几百个,我们假设就是每秒500个请求吧。然后每个请求会加载出来一张报表需要的大量数据,因为BI系统可能还需要针对那些数据进行内存中的现场计算加工一下,才能返回给 前端页面展示。

根据测算,每个请求大概需要加载出来100kb的数据进行计算,因此每秒500个请求,就需要加载出来50MB的数据到内存中进行计算。

2.3 频繁Young GC 影响不大

在上述系统运行模型下,基本上每秒会加载50MB的数据到Eden区中,只要区区200s,也就 是3分钟左右的时间,就会迅速填满Eden区,然后触发一次Young GC对新生代进行垃圾回收。
当然1G左右的Eden进行Young GC其实速度相对是比较快的,可能也就几十ms的时间就可以搞定了。其实对系统性能影响并不大。而且上述BI系统场景下,基本上每次Young GC后存活对象可能就几十MB,甚至是 几MB。

所以如果仅仅只是这样的话,那么大家可能会看到如下场景,BI系统运行几分钟过后,就会突然卡顿个10ms,但是对终端用户和系统 性能几乎是没有影响的。

2.4 优化方式一:提升机器配置,运用大内存机器

随着越来越多的商家来使用,并发压力越来越大,甚至高峰期会有每秒10万的并发压力。

如果还是用4核8G的机器来支撑,那么可能需要部署上百台机器来抗住每秒10万的高并发压力。所以一般针对这种情况,我们会提升机器的配置,本身BI系统就是非常吃内存的系统,所以我们将部署的机器全面提升到了16核32G的 高配置机器上去。每台机器可以抗个每秒几千请求,此时只要部署比如二三十台机器就可以了。

但是此时问题就来了,大家可以想一下,如果要是用大内存机器的话,那么新生代至少会分配到20G的大内存,Eden区也会占据16G以 上的内存空间。此时每秒几千请求的话,每秒大概会加载到内存中几百MB的数据,那么大概可能几十秒,甚至1分钟左右就会填满Eden区,会就需要执行Young GC。
此时Young GC要回收那么大的内存,速度会慢很多,也许此时就会导致系统卡顿个几百毫秒,或者1秒钟。系统卡顿时间过长,必然会导致瞬间很多请求积压排队,严重的时候会导致线上系统时不时出现前端请求超时的问题,就是 前端请求之后发现一两秒后还没返回就超时报错了。

2.5 使用G1 优化

对这个系统的一个优化,就是采用G1垃圾回收器来应对大内存的Young GC过慢的问题。对G1设置一个预期的GC停顿时间,比如100ms,让G1保证每次Young GC的时候最多停顿100ms,避免影响终端用户的使用。此时效果是非常显著的,G1会自动控制好在每次Young GC的时候就回收一部分Region,确保GC停顿时间控制在100ms以内。

这样的话,也许Young GC的频率会更高一些,但是每次停顿时间很小,这样对系统影响就不大了。

三、模拟垃圾回收的场景

3.1 YoungGC 的演示

3.1.1示例代码

public class YoungGCTest {

    public static void main(String[] args) {
        byte[] array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1=null;

        byte[] array2=new byte[2*1024*1024];
    }

}

3.1.2 程序的JVM参数示范

我们用以下JVM参数来运行代码:

-XX:NewSize=5242880 
-XX:MaxNewSize=5242880 
-XX:InitialHeapSize=10485760 
-XX:MaxHeapSize=10485760 
-XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=10485760 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC

上述参数都是基于JDK 1.8版本来设置的,不同的JDK版本对应的参数名称是不太一样的,但是基本意思是类似的。
-XX:InitialHeapSize”和“-XX:MaxHeapSize”就是初始堆大小和最大堆大小,
-XX:NewSize”和“XX:MaxNewSize”是初始新生代大小和最大新生代大小,
-XX:PretenureSizeThreshold=10485760”指定了大对象阈值是10MB。
相当于给堆内存分配10MB内存空间,其中新生代是5MB内存空间,其中Eden区占4MB,每个Survivor区占0.5MB, 大对象必须超过10MB才会直接进入老年代,年轻代使用ParNew垃圾回收器,老年代使用CMS垃圾回收器,看下图图示。
在这里插入图片描述

3.1.3 打印JVM GC日志

在系统的JVM参数中加入GC日志的打印选型,如下所示:
-XX:+PrintGCDetils:打印详细的gc日志 -XX:+PrintGCTimeStamps:这个参数可以打印出来每次GC发生的时间 -Xloggc:gc.log:这个参数可以设置将gc日志写入一个磁盘文件
加上这个参数之后,jvm参数如下所示:

-XX:NewSize=5242880 
-XX:MaxNewSize=5242880 
-XX:InitialHeapSize=10485760 
-XX:MaxHeapSize=10485760 
-XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=10485760 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:gc.log

运行即可,此时运行完毕后,会在工程目录中出现一个gc.log文件,里面就是本次程序运行的gc日志。

3.1.4 执行代码时的内存分析

  1. 执行第一行代码 byte[] array1 = new byte[1024 * 1024];
    这行代码运行,就会在JVM的Eden区内放入一个1MB的对象,同时在main线程的虚拟机栈中会压入一个main() 方法的栈帧,在main()方法的栈帧内部,会有一个“array1”变量,这个变量是指向堆内存Eden区的那个1MB的数组。
    在这里插入图片描述

  2. 执行第二行和第三行代码 array1 = new byte[1024 * 1024];
    此时前面两个数组都没有人引用了,就都成了垃圾对象。
    在这里插入图片描述

  3. 执行第四行代码 array1=null;
    这行代码一执行,就让array1这个变量什么都不指向了,此时会导致之前创建的3个数组全部变成垃圾对象
    在这里插入图片描述

  4. 执行第五行代码byte[] array2=new byte[2*1024*1024];
    此时会分配一个2MB大小的数组,尝试放入Eden区中。明显是不行的,因为Eden区总共就4MB大小,而且里面已经放入了3个1MB的数组了,所以剩余空间只有1MB了,此时你放一个2MB的数组是放不下的。

所以这个时候就会触发年轻代的Young GC。

3.1.5 gc日志结果详解

gc日志结果:

Java HotSpot(TM) 64-Bit Server VM (25.151-b12) for windows-amd64 JRE (1.8.0_151-b12), built on Sep  5 2017 19:33:46 by "java_re" with MS VC++ 10.0 (VS2010)

Memory: 4k page, physical 33450456k(25709200k free), swap 38431192k(29814656k free)

CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC

0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 
0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

par new generation   total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)

eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)

from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)

to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)

concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)

Metaspace  used 2782K, capacity 4486K, committed 4864K, reserved 1056768K

class space  used 300K, capacity 386K, committed 512K, reserved 1048576K
  1. 启动参数
    在GC日志中,可以看到如下内容:
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:NewSize=5242880....

表示这次运行程序采取的JVM参数是什么,基本都是我们设置的,同时还有一些参数默认就给设置了。
如果没设置JVM参数的话,显示的就是系统默认JVM参数。默认给的内存是很小的。

  1. 一次GC的概要说明
    如下日志信息:
0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 
0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

概要说明了本次GC的执行情况,GC (Allocation Failure),表明出现 GC 的原因,因为创建 array2 时要分配一个2MB的数组,此时Eden区内存不够,所以就出现了“Allocation Failure”,也就是对象分配失败。所以此时就要触发一次Young GC。

数字“0.268”,表示系统运行以后过了多少秒发生了本次GC,比如这里就是大概系统运行之后过了大概200多毫秒,发生了本次GC。

ParNew: 4030K->511K(4608K), 0.0012884 secs
“ParNew”的意思,表示指定的是ParNew垃圾回收器执行GC的。

4030K->512K(4608K)
这个代表的意思是,年轻代可用空间是4608KB,也就是4.5MB,Eden区是4MB,两个Survivor中只有一个是可以放存活对象的,另外一个是必须一致保持空闲的,所以他考虑年轻代的可用空间,就是Eden+1个Survivor的大小,也就是4.5MB。
4030K->512K,意思就是对年轻代执行了一次GC,GC之前都使用了4030KB了,但是GC之后只有512KB的对象 是存活下来的。
0.0015734 secs,这个就是本次gc耗费的时间,看这里来说大概耗费了1.5ms,仅仅是回收3MB的对象而已。

4030K->574K(9728K), 0.0017518 secs,这段话指的是整个Java堆内存的情况
意思是整个Java堆内存是总可用空间9728KB(9.5MB),其实就是年轻代4.5MB+老年代5M,然后GC前整个Java堆内存里使用了4030KB,GC之后Java堆内存使用了574KB。

[Times: user=0.00 sys=0.00, real=0.00 secs]
这个意思就是本次gc消耗的时间,也就是说本次gc就耗费了几毫秒,从秒为单位来看,几乎是0。

gc回收之后,从4030KB内存使用降低到了512KB的内存使用。
也就是说这次gc日志有512KB的对象存活了下来,从Eden区转移到了Survivor1区,其实我们可以把称呼改改,叫做 Survivor From区,另外一个Survivor叫做Survivor To区。

在这里插入图片描述

  1. GC过后的堆内存使用情况
Heap

par new generation   total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)

上述日志中 par new generation total 4608K, used 2601K,这就是说“ParNew”垃圾回收器负责的年轻代总共有 4608KB(4.5MB)可用内存,目前是使用了2601KB(2.5MB)。array2数组和512KB的未知对象的总大小(包含数组额外使用的内存空间)。

eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)

from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)

to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)

通过上述GC日志就能验证我们的推测是完全准确的,Eden区此时4MB的内存被使用了51%,就是因为有一个2MB的数组在里面。
然后From Survivor区,512KB是100%的使用率,此时被之前gc后存活下来的512KB的未知对象给占据了。

concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)

Concurrent Mark-Sweep 垃圾回收器,也就是 CMS垃圾回收器,管理的老年代内存空间一共是5MB,此时使用了62KB的空间,可以通过内存分析工具查看。

```powershell
Metaspace  used 2782K, capacity 4486K, committed 4864K, reserved 1056768K

class space  used 300K, capacity 386K, committed 512K, reserved 1048576K

上述两段日志也很简单,意思就是Metaspace元数据空间和Class空间,存放一些类信息、常量池之类的东西,此时他 们的总容量,使用内存,等等。

used capacity commited 和reserved,MetaSpace由一个或多个Virtual Space(虚拟空间)组成。虚拟空间是操作系统的连续存储空间,虚拟空间是按需分配的。当被分配时,虚拟空间会向操作系统预留(reserve)空间,但还没有被提交(committed)。
MetaSpace的预留空间(reserved)是全部虚拟空间的大小。 虚拟空间的最小分配单元是MetaChunk(也可以说是 Chunk)。
当新的Chunk被分配至虚拟空间时,与Chunk相关的内存空间被提交了(committed)。MetaSpace的committed 指的是所有Chunk占有的空间。
每个Chunk占据空间不同,当一个类加载器(Class Loader)被gc时,所有与之关联的Chunk被释放(freed)。这些被释放的Chunk被维护在一个全局的释放数组里。
MetaSpace的capacity指的是所有未被释放的Chunk占据的空间。 这么看gc日志发现自己committed是4864K, capacity4486K。有一部分的Chunk已经被释放了,代表有类加载器被回收了

3.2 模拟对象从新生代进入老年代的场景

3.2.1 动态年龄判定规则

如果Survivor区域内年龄1+年龄2+年龄3+年龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入 老年代。这就是所谓的动态年龄判定规则。

代码示例:

public class GCTest2 {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = null;

        byte[] array2 = new byte[128 * 1024];

        byte[] array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[128 * 1024];
        array3 = null;

        byte[] array4 = new byte[2 * 1024 * 1024];
    }
}

JVM 启动参数设置如下:

-XX:InitialHeapSize=20971520
-XX:MaxHeapSize=20971520
-XX:NewSize=10485760
-XX:MaxNewSize=10485760
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=10485760
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:gcTest2.log

此处,新生代通过“-XX:NewSize”设置为10MB,然后其中Eden区是8MB,每个Survivor区是1MB,Java堆总大小是20MB,老年代是10MB,大对象必须超过10MB才会直接进入老年代。
通过“-XX:MaxTenuringThreshold=15”设置了,只要对象年龄达到15岁才会直接进入老年代。
在这里插入图片描述
执行代码后的gc日志如下:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)

Memory: 4k page, physical 8274996k(1681152k free), swap 13780020k(3192280k free)

CommandLine flags: 
-XX:InitialHeapSize=20971520 
-XX:MaxHeapSize=20971520 
-XX:MaxNewSize=10485760 
-XX:MaxTenuringThreshold=15 
-XX:NewSize=10485760 
-XX:OldPLABSize=16 
-XX:PretenureSizeThreshold=10485760 
-XX:+PrintGC -XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:SurvivorRatio=8 
-XX:+UseConcMarkSweepGC 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParNewGC 

0.152: [GC (Allocation Failure) 0.153: [ParNew: 8129K->705K(9216K), 0.0007779 secs] 8129K->705K(19456K), 
0.0009952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

0.154: [GC (Allocation Failure) 0.154: [ParNew: 7006K->0K(9216K), 0.0017488 secs] 7006K->695K(19456K), 
0.0018044 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 
Heap
 par new generation   total 9216K, used 2130K [0x04c00000, 0x05600000, 0x05600000)
  eden space 8192K,  26% used [0x04c00000, 0x04e14938, 0x05400000)
  from space 1024K,   0% used [0x05400000, 0x05400000, 0x05500000)
  to   space 1024K,   0% used [0x05500000, 0x05500000, 0x05600000)
  
 concurrent mark-sweep generation total 10240K, used 695K [0x05600000, 0x06000000, 0x06000000)
 
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K

过程分析:

当执行下面的代码:byte[] array3 = new byte[2 * 1024 * 1024]; 时,Eden 区总10MB 已经存放了 3 * 2MB + 128KB ,此时创建 array3 会触发一次 YoungGC。
即日志中的以下部分:

0.152: [GC (Allocation Failure) 0.153: [ParNew: 8129K->705K(9216K), 0.0007779 secs] 8129K->705K(19456K), 
0.0009952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

系统运行 0.152s 后发生了第一次YoungGC,原因是Allocation Failure分配对象内存失败,采用了 ParNew 垃圾回收器,年轻代的可用内存(Eden区+1个Survivor区)为9MB(9216K),YoungGC 之前占用了8129K,GC之后有 705K 的对象(年轻代刚开始会有512KB的未知对象+128KB+使用数组的内存)存活下来,本次GC耗费时间0.99ms。

整个Java堆内存是总可用空间19456KB(19MB),其实就是年轻代9MB+老年代10M,然后GC前整个Java堆内存里使用了8129KB,GC之后Java堆内存使用了705KB。

[Times: user=0.00 sys=0.00, real=0.00 secs]
表示本次gc消耗的时间,也就是说本次gc就耗费了几毫秒,从秒为单位来看,几乎是0。
在这里插入图片描述
此时Survivor From区里的那700kb的对象,熬过一次gc,年龄就会增长1岁。而且此时Survivor区域总大小是1MB,此时Survivor区域中的存活对象已经有700KB了,超过了50%。

当执行代码byte[] array4 = new byte[2 * 1024 * 1024];时,Eden 区总10MB 已经存放了 3 * 2MB + 128KB ,此时创建 array4 会触发第二次 YoungGC。
即:

0.154: [GC (Allocation Failure) 0.154: [ParNew: 7006K->0K(9216K), 0.0017488 secs] 7006K->695K(19456K), 
0.0018044 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

系统运行 0.154s 后发生了第二次YoungGC,原因是Allocation Failure分配对象内存失败,采用了 ParNew 垃圾回收器,年轻代的可用内存为9MB(9216K),YoungGC 之前占用了7006K,GC之后为0,没有对象在年轻代,本次GC耗费时间1.7ms。

整个Java堆内存是总可用空间19456KB(19MB),年轻代9MB+老年代10M,然后GC前整个Java堆内存里使用了70,06KB,GC之后Java堆内存使用了695KB。

此时在Eden区里有3个2MB的数组和1个128KB的数组,会被回收掉,而根据动态年龄判断规则:年龄1+年龄2+年龄n的对象总大小超过了Survivor区域的50%,年龄n以上的对象进入老年代。Survivor From 这里的对象都是年龄1的,所以直接全部进入老年代了。
从日志可以看出:

  concurrent mark-sweep generation total 10240K, used 695K [0x05600000, 0x06000000, 0x06000000)

CMS管理的老年代,此时使用空间刚好是695KB(array2引用的128KB数组和512KB未知对象),证明此时Survivor里的对象触发了动态年龄判定规则进入老年代了。

然后array4变量引用的那个2MB的数组,此时就会分配到Eden区域中。
在这里插入图片描述
Eden区当前就是有一个2MB的数组。日志如下:

Heap
 par new generation   total 9216K, used 2130K [0x04c00000, 0x05600000, 0x05600000)
  eden space 8192K,  26% used [0x04c00000, 0x04e14938, 0x05400000)
  from space 1024K,   0% used [0x05400000, 0x05400000, 0x05500000)
  to   space 1024K,   0% used [0x05500000, 0x05500000, 0x05600000)

两个Survivor区域都是空的,因为之前存活的700KB的对象都进入老年代了,所以当然现在Survivor里都是空的了。

3.2.2 Young GC过后存活对象放不下Survivor区域,直接进入老年代

代码示例:

public class GCTest3 {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];

        byte[] array2 = new byte[128 * 1024];
        array2=null;

        byte[] array3 = new byte[2 * 1024 * 1024];

    }
}

执行代码`byte[] array3 = new byte[2 * 1024 * 1024];``,Eden 区内存不够触发YoungGC时的内存分析:
在这里插入图片描述
此次GC会回收掉上图中的2个2MB的数组和1个128KB的数组,然后留下 一个2MB的数组和1个未知的500KB的对象。

gc日志如下:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8274996k(1580276k free), swap 13780020k(3646816k free)

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 

0.175: [GC (Allocation Failure) 0.175: [ParNew: 8129K->577K(9216K), 0.0014206 secs] 8129K->2626K(19456K), 
0.0015901 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

Heap
 par new generation   total 9216K, used 2707K [0x05200000, 0x05c00000, 0x05c00000)
  eden space 8192K,  26% used [0x05200000, 0x05414938, 0x05a00000)
  from space 1024K,  56% used [0x05b00000, 0x05b90660, 0x05c00000)
  to   space 1024K,   0% used [0x05a00000, 0x05a00000, 0x05b00000)
  
 concurrent mark-sweep generation total 10240K, used 2049K [0x05c00000, 0x06600000, 0x06600000)
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K

由以上日志可知,
系统运行 0.175s 后发生了一次YoungGC,原因是Allocation Failure分配对象内存失败,年轻代的可用内存为9MB(9216K),YoungGC 之前占用了8129K,GC之后为577。
此次gc时,因为Survivor区仅仅只有1MB,而存活的对象有2MB和512KB的未知对象。从日志看可知,gc过后,年轻代里剩下了500多KB的对象(512KB的未知对象),所以这些存活对象并不是全部放入老年代,在这种情况下,是会把部分对象放入Survivor区的。

整个Java堆内存是总可用空间19456KB(19MB),年轻代9MB+老年代10M,然后GC前整个Java堆内存里使用了8129KB,GC之后Java堆内存使用了2626KB。

此时老年代里有2MB的数组,因此可以认为,Young GC过后,发现存活下来的对象有2MB的数组和500KB的未知对象。
此时把500KB的未知对象放入Survivor中,然后2MB的数组直接放入老年代。
在这里插入图片描述

3.2.3 对象达到15岁年龄之后自然进入老年代的场景

待补充…
在这里插入图片描述

3.2.4 大对象直接进入老年代

待补充…

3.3 老年代 FullGC 的演示

代码示例:

public class FullGCTest {

    public static void main(String[] args) {
        byte[] array1 = new byte[4 * 1024 * 1024];
        array1 = null;

        byte[] array2 = new byte[2 * 1024 * 1024];
        byte[] array3 = new byte[2 * 1024 * 1024];
        byte[] array4 = new byte[2 * 1024 * 1024];
        byte[] array5 = new byte[128 * 1024];

        byte[] array6 = new byte[2 * 1024 * 1024];
    }
}

设置JVM启动参数:

-XX:NewSize=10485760 
-XX:MaxNewSize=10485760 
-XX:InitialHeapSize=20971520 
-XX:MaxHeapSize=20971520 
-XX:SurvivorRatio=8  
-XX:MaxTenuringThreshold=15 
-XX:PretenureSizeThreshold=3145728 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:gc.log

这里最关键一个参数,就是“-XX:PretenureSizeThreshold=3145728”这个参数要设置大对象阈值为3MB,也就是超过3MB,就直接进入老年代。

gc日志分析:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8274996k(1798160k free), swap 13780020k(3475820k free)

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 
-XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=3145728 
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC 
-XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
 
0.222: [GC (Allocation Failure) 0.222: [ParNew (promotion failed): 8129K->8835K(9216K), 0.0031393 secs]
0.225: [CMS: 8193K->6837K(10240K), 0.0045225 secs] 12225K->6837K(19456K), 
[Metaspace: 2093K->2093K(4480K)], 0.0079786 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

Heap
 par new generation   total 9216K, used 2130K [0x05200000, 0x05c00000, 0x05c00000)
  eden space 8192K,  26% used [0x05200000, 0x05414938, 0x05a00000)
  from space 1024K,   0% used [0x05b00000, 0x05b00000, 0x05c00000)
  to   space 1024K,   0% used [0x05a00000, 0x05a00000, 0x05b00000)
  
 concurrent mark-sweep generation total 10240K, used 6837K [0x05c00000, 0x06600000, 0x06600000)
 
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K

创建数组 array1 时,大小为4MB,直接进入老年代,然后不再引用。
接下来连续创建的 4个数组,3个大小为2Mb,1个是128KB ,进入年轻代的Eden区(总空间为8MB),当执行创建2MB的数组byte[] array6 = new byte[2 * 1024 * 1024];时,触发YoungGC。此时对象的引用情况如下:
在这里插入图片描述

GC日志:

0.222: [ParNew (promotion failed): 8129K->8835K(9216K), 0.0031393 secs]

这行日志显示了,Eden区原来是有8000多KB的对象,但是回收之后发现一个都回收不掉,因为上述几个数组都被变量引用了。

所以此时会把这些对象放入到老年代里去,但是此时老年代里已经有一个4MB的数组了,无法放下3个2MB的数组和1个128KB的数组,此时触发了CMS垃圾回收器的Full GC。(Full GC其实就是会对老年代进行Old GC, 同时一般会跟一次Young GC关联,还会触发一次元数据区(永久代)的GC。)

0.225: [CMS: 8193K->6837K(10240K), 0.0045225 secs] 12225K->6837K(19456K), 
[Metaspace: 2093K->2093K(4480K)], 0.0079786 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

此时就会回收掉其中的一个4MB的数组,因为他已经没人引用了。老年代的内存占用从回收前的8MB变成了 6MB。
在这里插入图片描述

四、JVM 优化思路总结

  1. 项目上线初期:
    ①、上线前,根据预期的并发量、平均每个任务的内存需求大小等,然后评估需要使用几台机器来承载,每台机器需要 什么样的配置。
    ②、根据系统的任务处理速度,评估内存使用情况,然后合理分配Eden、Survivor,老年代的内存大小。

总体原则就是,让短命对象在YoungGC就被回收,不要进入老年代,长期存活的对象,尽早进入老年代,不要在新生 代复制来复制去。对系统响应时间敏感且内存需求大的,建议采用G1回收器
如何合理分配各个区域: 根据内存增速来评估多久进行Young GC 根据每次Young GC的存活,评估一下Survivor区的大小设置是否合理评估多久进行一次FullGC,产生的STW,是否可以接受?

  1. 公司的运营效果很佳,过了一段时间,系统负载增加了10倍,100倍:
    方案1:增加服务器数量 根据系统负载的增比,同比增加机器数量,机器配置,和jvm的配置可以保持不变。
    方案2:使用更高配置的机器 更高的配置,意味着更快速的处理速度和更大的内存。响应时间敏感且内存需求大的使 用G1回收器这时候需要和‘项目上线初期’一样,合理的使用配置和分配内存。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值