到底在优化啥
- 由于内存分配、参数设置的不合理,导致对象频繁进入老年代,频繁触发老年代 gc,导致系统频繁的卡死
- 减少 minor gc 后存活对象小于 survivor 区 50% 的频率,减少对象进入老年代的频率,减少 full gc 的频率
选用合适的垃圾回收器
- 一般来说用 ParseNew + CMS
- minor gc 频繁一般没啥问题,因为 stw 时间并不会很长
- 但是如果 eden 区非常大,比如 16g 啥的,minor gc 会造成 stw 接近 1s 多
- 如果 minor gc 频率多,会造成频繁的、明显的卡顿
- 此时要选用 g1 因为是基于 Region 的,g1 会在预设时间内对具有回收价值的 Region 进行回收,减少明显的卡顿
- 所以大内存 + 面向用户,用 g1 ( jdk9 后用比较好)
- g1 会消耗部分堆内存来存储 Region 收集树之类的信息,会造成额外的内存消耗
- 而且有 85% 以上存活对象的 Region 不回收的原则,可能每次 gc 并不会回收太多的垃圾
full gc 的优化
- 尽可能的避免full gc
- 尽可能避免空间担保导致存活对象放进去老年代
- 或者因为空间担保导致存活对象放不进 survivor 而放进老年代
自测(jdk8)
设置初始参数
-XX:NewSize=5242880 // 新生代初始值
-XX:MaxNewSize=5242880 // 新生代最大值
-XX:InitialHeapSize=10485760 // 堆内存初始值
-XX:MaxHeapSize=10485760 // 堆内存最大值
-XX:SurvivorRatio=8 // eden占有新生代比例 N:1:1 (survivor 都看作1)
-XX:PretenureSizeThreshold=10485760 // 大对象阈值
-XX:+UseParNewGC // 新生代用 ParNew 垃圾回收器
-XX:+UseConcMarkSweepGC // 老年代用 cms 垃圾回收器
打印日志
-XX:+PrintGCDetails // 打印详细的gc日志
-XX:+PrintGCTimeStamps // 这个参数可以打印出来每次GC发生的时间
-Xloggc:gc.log // 这个参数可以设置将gc日志写入一个磁盘文件
完整参数
-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
代码示例
public class JVMDemo {
public static void main(String[] args) {
byte[] array = new byte[1024 * 1024];
array = new byte[1024 * 1024];
array = new byte[1024 * 1024];
array = null;
array = new byte[2 * 1024 * 1024];
}
}
打印日志
- 做了点对齐美化 ```shell Java HotSpot(TM) 64-Bit Server VM (25.102-b14) for windows-amd64 JRE (1.8.0_102-b14), built on Jun 22 2016 13:15:21 by “java_re” with MS VC++ 10.0 (VS2010) Memory: 4k page, physical 8277272k(2762620k free), swap 15879448k(6289664k free)
— 这里是说明使用到的 jvm 参数 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
— 发生的 gc 情况 0.666: [GC (Allocation Failure) 0.666: [ParNew: 4056K->512K(4608K), 0.0030116 secs] 4056K->2057K(9728K), 0.0032908 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 0.670: [GC (Allocation Failure) 0.670: [ParNew: 2637K->0K(4608K), 0.0014145 secs] 4182K->2054K(9728K), 0.0014698 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
— 堆内存分布 Heap — parNew 垃圾收集器能管理的内存 par new generation total 4608K, used 2089K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000) eden space 4096K, 51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000) from space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000) to space 512K, 0% used [0x00000000ffa80000, 0x00000000ffa80000, 0x00000000ffb00000)
— cms 垃圾收集器能管理的内存 concurrent mark-sweep generation total 5120K, used 2054K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
— 元空间内存分布 Metaspace used 3339K, capacity 4496K, committed 4864K, reserved 1056768K — class 空间内存分布 class space used 366K, capacity 388K, committed 512K, reserved 1048576K
<a name="lF6KF"></a>
### 解读
```shell
0.666: [
GC (Allocation Failure) 0.666: [ParNew: 4056K->512K(4608K), 0.0030116 secs]
4056K->2057K(9728K), 0.0032908 secs]
[Times: user=0.00 sys=0.02, real=0.00 secs
]
- 第一个和第二个 0.666: 系统运行了多久触发的 gc
GC (Allocation Failure)
: 对象分配失败,发生了 gc[ParNew: 4056K->512K(4608K), 0.0030116 secs]
ParNew
: : 发生了 minor gc4608K
: 新生代可用空间 (eden + s1)4056K->512K
: 新生代可用空间 (eden + s1) gc 前 有 4056 k 的对象,gc 后存活 512k ,扔到了 to-survivor 区0.0030116 secs
: 本次 gc 花费时间
4056K->2057K(9728K), 0.0032908 secs
- 表明 新生代可用空间 (eden + s1) + 老年代有 9728k 的大小,gc 前总空间有 4056k 的对象,gc 后存活 2057k 的对象,花费了一些时间
[Times: user=0.00 sys=0.02, real=0.00 secs]
: gc 在各个系统态消耗时间
为什么 gc 前 eden + s1 有 4m 的对象
- 代码中明明只有 3m ?
- 代码中的对象,除了对象本身实际的内存,还有一些标识性的信息
- 除了代码中的对象,还有一些未知对象
一些注意点
- gc 是先清理完垃圾再创建对象
- 如果 ygc 时对象放不进 survivor,并不会全部扔老年代,可能会部分扔 suvivor,部分扔老年代
- 如果是 ygc 触发 full gc ,那么 full gc 是 old gc + metaspace gc; 如果是 metaspace 触发 full gc,那么 full gc 是 ygc + old gc + metaspace gc
工具
Jstat
- 首先通过
jps
查看要追踪的 java 进程 jstat -gc <java_pid> <多少mills刷新一次> <刷新多少次>
内容
SOC:这是From Survivor区的大小
S1C:这是To Survivor区的大小
SOU:这是From Survivor区当前使用的内存大小
S1U:这是To Survivor区当前使用的内存大小
EC:这是Eden区的大小
EU:这是Eden区当前使用的内存大小
OC:这是老年代的大小
OU:这是老年代当前使用的内存大小
MC:这是方法区(永久代、元数据区)的大小
MU:这是方法区(永久代、元数据区)的当前使用的内存大小
YGC:这是系统运行迄今为止的Young GC次数
YGCT:这是Young GC的耗时
FGC:这是系统运行迄今为止的Full GC次数
FGCT:这是Full GC的耗时
GCT:这是所有GC的总耗时
jmap
jmap -histo <pid>
查看占用内存多的类jmap -dump:live,format=b,file=dump.hprof <pid>
将当前内存快照以二进制方式 dump 到一个文件里去
jhat
jhat -port 7000 dump.hprof
启动图形化界面查看 jmap dump 出来的快照
场景
- redis 做读请求,如果 qps 过高, eden 会存在大量对象存活,导致 survivor 区无法存放,从而进入老年代,引发大量 full gc
- 除了调整 survivor 的大小,还需要调整 cms内存碎片问题
- full gc 是标记清除算法,过多得 full gc 会导致内存碎片过多,full gc 得频率会更高
- 参数说明:
-XX:+UseCMSCompactAtFullCollection
: cms gc 进行标记清除操作后启用压缩操作;-XX:CMSFullGCsBeforeCompaction=0
两个参数的含义: 就是在每次Full GC之后会触发一次Compaction操作,也就是压缩操作 -XX:+CMSParallelInitialMarkEnabled
: cms 初始标记的时候多线程并发执行,加快标记速度-XX:+CMSParallelRemarkEnabled
:在重新标记的时候多线程执行,降低 stw 时间-XX:+CMSScavengeBeforeRemark
: 在重新标记之前,尽量执行一次 minor gc- 重新标记会 stw
- 重新标记会扫描年轻代,因为要判断年轻代是否会引用到老年代的对象
- minor gc 是为了减少重新标记所需扫描的时间
jdk8场景下,没有设置 metaspace 对应的空间大小,然后莫名其妙的因为反复加载某个类而导致 metaspace 空间不足而频繁的 full gc
-XX:+TraceClassLoading -XX:+TraceClassUnloading
: 打印出来 jvm 加载和卸载了哪些类打印了以下内容
[Loaded sun.reflect.GeneratedMethodAccessor19 from __JVM_DefineClass__]
[Unloading class sun.reflect.GeneratedMethodAccessor19 0x0000000100073028]
这是因为大量使用反射时候jvm底层会动态生成一些类信息放入 metaspace 区域中, 而他们的 class 对象是放入堆中,例如
private static void test2() throws Exception {
Demo demo = new Demo();
while (true) {
Method method = Demo.class.getDeclaredMethod("test");
method.setAccessible(true);
method.invoke(demo);
TimeUnit.MILLISECONDS.sleep(100);
}
}
private static class Demo {
public Demo() {
}
private void test() {
}
}
这些动态生成的类的 class 对象都是 软引用
- 软引用的存活时间是根据公式
clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB
进行判断clock - timestamp
代表一个软引用对象有多久没被访问freespace
表示当前 jvm 空闲时间SoftRefLRUPolicyMSPerMB
表示每一MB空间内存空间可以允许 软引用 对象存活多久
- 软引用的存活时间是根据公式
- 如果设置了
-XX:SoftRefLRUPolicyMSPerMB=0
那么一旦发生了 gc,会立刻回收这些动态生成的 class 对象,下一次要利用反射时,metaspace 的元信息在堆中对应的 class 对象找不到,那么又会重新生成 class 对象进堆中、生成新的元信息占据 metaspace 空间,从而导致 metaspace 空间不足的 gc - 一方面不要
-XX:SoftRefLRUPolicyMSPerMB
要设置大一点,一方面要主动将 metaspace 设置大一点
- 老年代突然有大量对象产生,可能是 survivor 触发了动态年龄判断,也可能是产生了大对象
- 有可能是因为sql查询进行了全表查询,查了一堆东西出来,直接进入了老年代
-XX:CMSInitiatingOccupancyFraction=92
和-XX:+UseCMSInitiatingOccupancyOnly
配套使用,如果不设置后者,jvm第一次会采用92%但是后续jvm会根据运行时采集的数据来进行GC周期,如果设置后者则jvm每次都会在92%的时候进行gc
- 如果显示调用了
System.gc();
,在并发高的时候,系统会卡死- 要么不要写
- 要么设置
-XX:+DisableExplicitGC
, 禁止显式调用 gc,代码调用不会触发 gc
- tomcat 工作线程全部堵塞导致内存一直占用
- tomcat 的配置
max-http-header-size
设置请求头大小,会导致内存占用 double 大小 - 调用等待重试太长,会导致这段时间内这些请求一直在内存中占用
- 如果出现类似 100 qps 有 400工作线程调用,说明是 100 请求堵塞住,导致 4s内 不能处理
- 经典的微服务调用问题(超时熔断处理,配合请求队列异步,只给几个固定的线程资源)
- tomcat 的配置
- jetty 服务器在并发高、请求时间长情况下会出现堆外内存oom,java nio 源码中表明,每次调用堆外内存的时候,都会调用
System.gc();
通知垃圾回收器回收掉没有引用的DirectByteBuffer
对象- 但是如果设置了
-XX:+DisableExplicitGC
, 那么System.gc()
将无效 - 可以放开该设置
- 但是如果设置了
对策
触发排查 cpu 负载过高的原因
- 创建了大量线程
-
初步排查频繁 Full GC 的问题
内存分配不合理,导致对象频繁进入老年代,频繁触发 full gc
- 内存泄露,内存驻留大量对象进入老年代,导致少量对象进入老年代就触发 full gc
- 永久代/元空间类太多,触发 full gc
- 显式调用了
System.gc();
, 不过可以参数禁用
OOM
区域
metaspace
- 存放类的元信息等,大部分情况下,当对应的类没有引用后(没有对应实例对象,没有对应的堆中的 class 对象引用,没有加载它的 classloader 引用),gc 才会干掉它
- 默认大小只有几十m,稍微大型的系统本身有很多类,依赖很多外部 jar 包,在启动的时候会加载很多类,导致 metaspace 空间不足
动态生成类,如果代码没有足够的控制,生成的类过多,很容易塞满 metaspace.
-
堆
- 新生代 ygc 啥的将对象扔进老年代,老年代满了之后,如果 full gc 后老年代空间仍然不足,此时有新对象进来,老年代无法存放,就会引发 oom
- 系统高并发请求,请求了太大了,大量对象存活,ygc 后大量对象进入老年代,老年代 full gc 时这些对象如果还存活,请求再进来就会引发 oom 系统崩溃
- 系统内存泄漏,导致很多对象长期存活,即使触发了 gc 依然无法回收,导致 OOM 系统崩塌
oom 时 自动 dump
- 加入参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:/dumps
常见参数
-Xms4096M
-Xmx4096M
-Xmn3072M
-Xss1M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFaction=92
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallellnitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
-XX:+PrintGCDetails
-Xloggc:gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:/dumps
- 一行
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallellnitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/dumps
解释
-Xms4096M // 设置初始 Java 堆大小
-Xmx4096M // 设置最大 Java 堆大小
-Xmn3072M // 设置 Java 年轻代大小
-Xss1M // 设置每个线程的栈大小
-XX:MetaspaceSize=256M // 设置 metaspace 初始值
-XX:MaxMetaspaceSize=256M // 设置 metaspace 最大值
-XX:+UseParNewGC // 使用 ParNew 作为新生代垃圾回收器
-XX:+UseConcMarkSweepGC // 设置年老代为并发收集
-XX:CMSInitiatingOccupancyFaction=92 // 设置老年代触发 full gc 的阈值
-XX:+UseCMSInitiatingOccupancyOnly // 要求设置老年代触发 full gc 的阈值
-XX:+UseCMSCompactAtFullCollection // 使用 cms 作为老年代垃圾回收器
-XX:CMSFullGCsBeforeCompaction=0 // 设置 cms 进行标记-清除的 gc 操作后,进行内存整理的间隔
-XX:+CMSParallellnitialMarkEnabled // cms gc 时初始标记阶段并发进行
-XX:+CMSScavengeBeforeRemark // cms gc 时重新标记阶段前尽量进行 ygc
-XX:+DisableExplicitGC // 显式调用 System.gc() 无效
-XX:+PrintGCDetails // 打印 gc 详细信息
-Xloggc:gc.log // 保存 gc 的详细信息的文件
-XX:+HeapDumpOnOutOfMemoryError // 当发生 oom 时自动 dump
-XX:HeapDumpPath=E:/dumps // 自动 dump 的文件位置
-XX:CMSInitiatingOccupancyFraction=92
和-XX:+UseCMSInitiatingOccupancyOnly
配套使用,如果不设置后者,jvm第一次会采用92%但是后续jvm会根据运行时采集的数据来进行GC周期,如果设置后者则jvm每次都会在92%的时候进行gc