到底在优化啥
- 由于内存分配、参数设置的不合理,导致对象频繁进入老年代,频繁触发老年代 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>### 解读```shell0.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
