到底在优化啥

  • 由于内存分配、参数设置的不合理,导致对象频繁进入老年代,频繁触发老年代 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)

设置初始参数

  1. -XX:NewSize=5242880 // 新生代初始值
  2. -XX:MaxNewSize=5242880 // 新生代最大值
  3. -XX:InitialHeapSize=10485760 // 堆内存初始值
  4. -XX:MaxHeapSize=10485760 // 堆内存最大值
  5. -XX:SurvivorRatio=8 // eden占有新生代比例 N:1:1 (survivor 都看作1)
  6. -XX:PretenureSizeThreshold=10485760 // 大对象阈值
  7. -XX:+UseParNewGC // 新生代用 ParNew 垃圾回收器
  8. -XX:+UseConcMarkSweepGC // 老年代用 cms 垃圾回收器

打印日志

  1. -XX:+PrintGCDetails // 打印详细的gc日志
  2. -XX:+PrintGCTimeStamps // 这个参数可以打印出来每次GC发生的时间
  3. -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

代码示例

  1. public class JVMDemo {
  2. public static void main(String[] args) {
  3. byte[] array = new byte[1024 * 1024];
  4. array = new byte[1024 * 1024];
  5. array = new byte[1024 * 1024];
  6. array = null;
  7. array = new byte[2 * 1024 * 1024];
  8. }
  9. }

打印日志

  • 做了点对齐美化 ```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

  1. <a name="lF6KF"></a>
  2. ### 解读
  3. ```shell
  4. 0.666: [
  5. GC (Allocation Failure) 0.666: [ParNew: 4056K->512K(4608K), 0.0030116 secs]
  6. 4056K->2057K(9728K), 0.0032908 secs]
  7. [Times: user=0.00 sys=0.02, real=0.00 secs
  8. ]
  • 第一个和第二个 0.666: 系统运行了多久触发的 gc
  • GC (Allocation Failure): 对象分配失败,发生了 gc
  • [ParNew: 4056K->512K(4608K), 0.0030116 secs]
    • ParNew: : 发生了 minor gc
    • 4608K: 新生代可用空间 (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 ?
  1. 代码中的对象,除了对象本身实际的内存,还有一些标识性的信息
  2. 除了代码中的对象,还有一些未知对象

一些注意点

  1. gc 是先清理完垃圾再创建对象
  2. 如果 ygc 时对象放不进 survivor,并不会全部扔老年代,可能会部分扔 suvivor,部分扔老年代
  3. 如果是 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刷新一次> <刷新多少次>

    内容

    1. SOC:这是From Survivor区的大小
    2. S1C:这是To Survivor区的大小
    3. SOU:这是From Survivor区当前使用的内存大小
    4. S1U:这是To Survivor区当前使用的内存大小
    5. EC:这是Eden区的大小
    6. EU:这是Eden区当前使用的内存大小
    7. OC:这是老年代的大小
    8. OU:这是老年代当前使用的内存大小
    9. MC:这是方法区(永久代、元数据区)的大小
    10. MU:这是方法区(永久代、元数据区)的当前使用的内存大小
    11. YGC:这是系统运行迄今为止的Young GC次数
    12. YGCT:这是Young GC的耗时
    13. FGC:这是系统运行迄今为止的Full GC次数
    14. FGCT:这是Full GC的耗时
    15. GCT:这是所有GC的总耗时

jmap

  • jmap -histo <pid> 查看占用内存多的类
  • jmap -dump:live,format=b,file=dump.hprof <pid> 将当前内存快照以二进制方式 dump 到一个文件里去

jhat

  • jhat -port 7000 dump.hprof 启动图形化界面查看 jmap dump 出来的快照

场景

  1. redis 做读请求,如果 qps 过高, eden 会存在大量对象存活,导致 survivor 区无法存放,从而进入老年代,引发大量 full gc
    1. 除了调整 survivor 的大小,还需要调整 cms内存碎片问题
    2. full gc 是标记清除算法,过多得 full gc 会导致内存碎片过多,full gc 得频率会更高
    3. 参数说明: -XX:+UseCMSCompactAtFullCollection : cms gc 进行标记清除操作后启用压缩操作; -XX:CMSFullGCsBeforeCompaction=0 两个参数的含义: 就是在每次Full GC之后会触发一次Compaction操作,也就是压缩操作
    4. -XX:+CMSParallelInitialMarkEnabled: cms 初始标记的时候多线程并发执行,加快标记速度
    5. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,降低 stw 时间
    6. -XX:+CMSScavengeBeforeRemark: 在重新标记之前,尽量执行一次 minor gc
      1. 重新标记会 stw
      2. 重新标记会扫描年轻代,因为要判断年轻代是否会引用到老年代的对象
      3. minor gc 是为了减少重新标记所需扫描的时间
  2. jdk8场景下,没有设置 metaspace 对应的空间大小,然后莫名其妙的因为反复加载某个类而导致 metaspace 空间不足而频繁的 full gc

    • -XX:+TraceClassLoading -XX:+TraceClassUnloading: 打印出来 jvm 加载和卸载了哪些类
    • 打印了以下内容

      1. [Loaded sun.reflect.GeneratedMethodAccessor19 from __JVM_DefineClass__]
      2. [Unloading class sun.reflect.GeneratedMethodAccessor19 0x0000000100073028]
    • 这是因为大量使用反射时候jvm底层会动态生成一些类信息放入 metaspace 区域中, 而他们的 class 对象是放入堆中,例如

      1. private static void test2() throws Exception {
      2. Demo demo = new Demo();
      3. while (true) {
      4. Method method = Demo.class.getDeclaredMethod("test");
      5. method.setAccessible(true);
      6. method.invoke(demo);
      7. TimeUnit.MILLISECONDS.sleep(100);
      8. }
      9. }
      10. private static class Demo {
      11. public Demo() {
      12. }
      13. private void test() {
      14. }
      15. }
    • 这些动态生成的类的 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 设置大一点
  3. 老年代突然有大量对象产生,可能是 survivor 触发了动态年龄判断,也可能是产生了大对象
    1. 有可能是因为sql查询进行了全表查询,查了一堆东西出来,直接进入了老年代
    2. -XX:CMSInitiatingOccupancyFraction=92-XX:+UseCMSInitiatingOccupancyOnly配套使用,如果不设置后者,jvm第一次会采用92%但是后续jvm会根据运行时采集的数据来进行GC周期,如果设置后者则jvm每次都会在92%的时候进行gc
  4. 如果显示调用了 System.gc(); ,在并发高的时候,系统会卡死
    1. 要么不要写
    2. 要么设置 -XX:+DisableExplicitGC, 禁止显式调用 gc,代码调用不会触发 gc
  5. tomcat 工作线程全部堵塞导致内存一直占用
    1. tomcat 的配置 max-http-header-size 设置请求头大小,会导致内存占用 double 大小
    2. 调用等待重试太长,会导致这段时间内这些请求一直在内存中占用
    3. 如果出现类似 100 qps 有 400工作线程调用,说明是 100 请求堵塞住,导致 4s内 不能处理
      1. 经典的微服务调用问题(超时熔断处理,配合请求队列异步,只给几个固定的线程资源)
  6. jetty 服务器在并发高、请求时间长情况下会出现堆外内存oom,java nio 源码中表明,每次调用堆外内存的时候,都会调用 System.gc(); 通知垃圾回收器回收掉没有引用的 DirectByteBuffer 对象
    1. 但是如果设置了 -XX:+DisableExplicitGC, 那么 System.gc() 将无效
    2. 可以放开该设置

对策

触发排查 cpu 负载过高的原因

  1. 创建了大量线程
  2. 频繁触发了 full gc

    初步排查频繁 Full GC 的问题

  3. 内存分配不合理,导致对象频繁进入老年代,频繁触发 full gc

  4. 内存泄露,内存驻留大量对象进入老年代,导致少量对象进入老年代就触发 full gc
  5. 永久代/元空间类太多,触发 full gc
  6. 显式调用了 System.gc();, 不过可以参数禁用

OOM

区域

metaspace

  • 存放类的元信息等,大部分情况下,当对应的类没有引用后(没有对应实例对象,没有对应的堆中的 class 对象引用,没有加载它的 classloader 引用),gc 才会干掉它
  1. 默认大小只有几十m,稍微大型的系统本身有很多类,依赖很多外部 jar 包,在启动的时候会加载很多类,导致 metaspace 空间不足
  2. 动态生成类,如果代码没有足够的控制,生成的类过多,很容易塞满 metaspace.

    1. 比如生成多个 Enhancer 实例来代理同一个类, jvm 会为这些代理类在 metaspace 中生成太多元信息, 如果他们被一直引用,那么 full gc 将无法清理他们,造成 oom

      虚拟机栈

  3. 出现了递归调用,出现 stackoverflow

  • 新生代 ygc 啥的将对象扔进老年代,老年代满了之后,如果 full gc 后老年代空间仍然不足,此时有新对象进来,老年代无法存放,就会引发 oom
  1. 系统高并发请求,请求了太大了,大量对象存活,ygc 后大量对象进入老年代,老年代 full gc 时这些对象如果还存活,请求再进来就会引发 oom 系统崩溃
  2. 系统内存泄漏,导致很多对象长期存活,即使触发了 gc 依然无法回收,导致 OOM 系统崩塌

oom 时 自动 dump

  • 加入参数
    1. -XX:+HeapDumpOnOutOfMemoryError
    2. -XX:HeapDumpPath=E:/dumps

常见参数

  1. -Xms4096M
  2. -Xmx4096M
  3. -Xmn3072M
  4. -Xss1M
  5. -XX:MetaspaceSize=256M
  6. -XX:MaxMetaspaceSize=256M
  7. -XX:+UseParNewGC
  8. -XX:+UseConcMarkSweepGC
  9. -XX:CMSInitiatingOccupancyFaction=92
  10. -XX:+UseCMSInitiatingOccupancyOnly
  11. -XX:+UseCMSCompactAtFullCollection
  12. -XX:CMSFullGCsBeforeCompaction=0
  13. -XX:+CMSParallellnitialMarkEnabled
  14. -XX:+CMSScavengeBeforeRemark
  15. -XX:+DisableExplicitGC
  16. -XX:+PrintGCDetails
  17. -Xloggc:gc.log
  18. -XX:+HeapDumpOnOutOfMemoryError
  19. -XX:HeapDumpPath=E:/dumps
  • 一行
  1. -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
  • 解释

    1. -Xms4096M // 设置初始 Java 堆大小
    2. -Xmx4096M // 设置最大 Java 堆大小
    3. -Xmn3072M // 设置 Java 年轻代大小
    4. -Xss1M // 设置每个线程的栈大小
    5. -XX:MetaspaceSize=256M // 设置 metaspace 初始值
    6. -XX:MaxMetaspaceSize=256M // 设置 metaspace 最大值
    7. -XX:+UseParNewGC // 使用 ParNew 作为新生代垃圾回收器
    8. -XX:+UseConcMarkSweepGC // 设置年老代为并发收集
    9. -XX:CMSInitiatingOccupancyFaction=92 // 设置老年代触发 full gc 的阈值
    10. -XX:+UseCMSInitiatingOccupancyOnly // 要求设置老年代触发 full gc 的阈值
    11. -XX:+UseCMSCompactAtFullCollection // 使用 cms 作为老年代垃圾回收器
    12. -XX:CMSFullGCsBeforeCompaction=0 // 设置 cms 进行标记-清除的 gc 操作后,进行内存整理的间隔
    13. -XX:+CMSParallellnitialMarkEnabled // cms gc 时初始标记阶段并发进行
    14. -XX:+CMSScavengeBeforeRemark // cms gc 时重新标记阶段前尽量进行 ygc
    15. -XX:+DisableExplicitGC // 显式调用 System.gc() 无效
    16. -XX:+PrintGCDetails // 打印 gc 详细信息
    17. -Xloggc:gc.log // 保存 gc 的详细信息的文件
    18. -XX:+HeapDumpOnOutOfMemoryError // 当发生 oom 时自动 dump
    19. -XX:HeapDumpPath=E:/dumps // 自动 dump 的文件位置
    • -XX:CMSInitiatingOccupancyFraction=92-XX:+UseCMSInitiatingOccupancyOnly配套使用,如果不设置后者,jvm第一次会采用92%但是后续jvm会根据运行时采集的数据来进行GC周期,如果设置后者则jvm每次都会在92%的时候进行gc