JVM之垃圾回收和调优思路

文章目录

      • GC的基础知识
        • 1.什么是垃圾
        • 2.如何定位(找到)垃圾
        • 3.常见的垃圾回收算法
        • 4.JVM内存分代模型(用于分代垃圾回收算法)
        • 5.常见的垃圾回收器
      • 常见垃圾回收器组合参数设定:(1.8)
      • JVM调优第一步,了解JVM常用命令行参数
      • PS GC日志详解
      • 调优前的基础概念:
      • 什么是调优?
      • 调优,从规划开始
      • 优化环境
      • 解决JVM运行中的问题
        • 一个案例理解常用工具
        • jconsole远程连接
        • jvisualvm远程连接
        • jprofiler (收费)
        • arthas在线排查工具
      • GC算法的基础概念
      • CMS
        • CMS的问题
        • CMS日志分析
      • G1
        • G1日志详解
      • 案例汇总
      • GC常用参数
      • Parallel常用参数
      • CMS常用参数
      • G1常用参数
        • 常见问题
      • 参考资料

GC的基础知识

1.什么是垃圾

C语言申请内存:malloc free

C++: new delete

c/C++ 手动回收内存

开发效率低,执行效率高

Java: new ?

开发效率高,执行效率低

自动内存回收,编程上简单,系统不容易出错,手动释放内存,容易出两种类型的问题:

  1. 忘记回收
  2. 多次回收

垃圾:没有任何引用指向的一个对象或者多个对象(循环引用),称为垃圾

2.如何定位(找到)垃圾
  1. 引用计数(ReferenceCount)

    对象会有一个变量来对引用计数,变为0时就是成为垃圾了;引用计数不能解决循环引用

  2. 根可达算法(RootSearching)

    根对象:

    线程栈变量指向的对象

    静态变量指向的对象

    运行时常量池中用到了其他类的对象,类对象

    JNI指针(c c++)native method stack本地方法中用到的对象

3.常见的垃圾回收算法
  1. 标记清除(mark sweep) - 位置不连续 产生碎片 效率偏低(两遍扫描,第一次扫描找出不可回收的对象,也就是标记存活对象,第二次是把没有标记的对象清理掉)

    算法相对简单,存活对象比较多的情况(适用于老年代)下效率较高

  2. 拷贝算法 (copying) - 没有碎片,浪费空间

    内存一分为二,适用于存活对象较少的情况(适用于Eden区),只扫描一次,效率较高

    移动复制对象,需要调整对象引用

  3. 标记压缩 标记整理(mark compact) - 没有碎片,不会产生内存减半,效率偏低(两遍扫描,第一次找到有用的对象,第二次是移动,需要移动对象,指针需要调整)

    将存活对象往前移动

4.JVM内存分代模型(用于分代垃圾回收算法)
  1. 部分垃圾回收器使用的模型

    除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型

    Epsilon ZGC Shenandoah逻辑物理不分代

    G1是逻辑分代,物理不分代

    除此之外不仅逻辑分代,而且物理分代

  2. 新生代(1/3) + 老年代(2/3) + 永久代(1.7)Perm Generation/ 元数据区(1.8) Metaspace

    设置比例 -XX:NewRatio

    1. 永久代 元数据 - Class
    2. 永久代必须指定大小限制 ,元数据可以设置,也可以不设置,无上限(受限于物理内存)
    3. 字符串常量 1.7 - 永久代,1.8 - 堆
    4. MethodArea逻辑概念 - 永久代、元数据
  3. 新生代 = Eden + 2个suvivor幸存区 (适用copying算法)

    1. YGC回收之后,大多数的对象会被回收,活着的进入s0
    2. 再次YGC,活着的对象eden + s0 -> s1
    3. 再次YGC,eden + s1 -> s0
    4. 年龄足够 -> 老年代 (parallel scavenge 回收了15次 CMS 6 G1 15)年龄参数设置:-XX: MaxTenuringThreshold
    5. s区装不下 -> 老年代
  4. 老年代 (适用Mark Compact 或 Mark Sweep算法)

    1. 顽固分子
    2. 老年代满了FGC Full GC
    3. 大对象直接就放老年代
  5. GC Tuning (Generation)

    1. 尽量减少FGC
    2. MinorGC = YGC(Eden区空间不足,年轻代空间耗尽时触发)
    3. MajorGC = FGC (在老年代无法继续分配空间时触发,System.gc()也会触发,新生代老年代同时进行回收)
  6. 栈上分配

    线程私有小对象

    不发生逃逸

    支持标量替换

    一般这个地方不调优,如果其他地方的调优没效果,就可以尝试一下

  7. 线程本地分配TLAB (Thread Local Allocation Buffer)

    占用Eden,默认1%

    多线程的时候不用竞争Eden就可以申请空间,提高效率

    分配小对象

    一般这个地方不调优,如果其他地方的调优没效果,就可以尝试一下

  8. 对象分配过程图
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g7X7rHqt-1604390947687)(对象分配过程详解.png)]

  9. 动态年龄:(不重要)
    https://www.jianshu.com/p/989d3b06a49d

    YGC时,Eden + to -> from 超过from的50%,把年龄最大的放入老年代,不管到没到年龄

  10. 分配担保:(不重要)
    YGC期间 survivor区空间不够了 空间担保直接进入老年代
    参考:https://cloud.tencent.com/developer/article/1082730

5.常见的垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Acz5YdX-1604390947690)(常用垃圾回收器.png)]

  1. 垃圾回收会stw,在safepoint安全点, java 工作线程停止,GC线程开始工作

  2. 目前没有不出现STW的垃圾回收器

  3. 串行是指一个GC线程,并行是多个GC线程,并发是指工作线程和GC线程同时运行。

  4. JDK诞生 Serial追随 提高效率,诞生了PS,为了配合CMS,诞生了PN,CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS
    并发垃圾回收是因为无法忍受STW

  5. Serial 年轻代 串行回收

    单个GC线程进行垃圾回收

  6. PS 年轻代 并行回收

    多个GC线程,默认线程数为CPU的核数

  7. ParNew 年轻代 配合CMS的并行回收,在ps的基础上增强来配合CMS

  8. SerialOld 老年代

    单个GC线程,标记清除压缩

  9. ParallelOld 老年代

    多个GC线程,标记压缩算法

  10. ConcurrentMarkSweep 老年代 并发的, 垃圾回收和应用程序同时运行,降低STW的时间(200ms)
    CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定
    CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收
    想象一下:
    PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW)
    几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC
    算法:三色标记 + Incremental Update

CMS的4个阶段:

初始标记:

并发标记:

重新标记:

并发清理:

  1. G1(10ms)
    算法:三色标记 + SATB

  2. ZGC (1ms) PK C++
    算法:ColoredPointers + LoadBarrier

  3. Shenandoah
    算法:ColoredPointers + WriteBarrier

  4. Eplison

  5. PS 和 PN区别的延伸阅读:
    ▪https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-3D0BB91E-9BFF-4EBB-B523-14493A860E73

  6. 垃圾收集器跟内存大小的关系

    1. Serial 几十兆
    2. PS 上百兆 - 几个G
    3. CMS - 20G
    4. G1 - 上百G 逻辑上分代,物理上不分
    5. ZGC - 4T - 16T(JDK13)

1.8默认的垃圾回收:PS + ParallelOld (多个GC线程)

常见垃圾回收器组合参数设定:(1.8)

  • -XX:+UseSerialGC = Serial New (DefNew) + Serial Old

    • 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
  • -XX:+UseParNewGC = ParNew + SerialOld

    • 这个组合已经很少用(在某些版本中已经废弃)
    • https://stackoverflow.com/questions/34962257/why-remove-support-for-parnewserialold-anddefnewcms-in-the-future
  • -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old

  • -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】

  • -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old

  • -XX:+UseG1GC = G1

  • Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC

    • java +XX:+PrintCommandLineFlags -version
    • 通过GC的日志来分辨
  • Linux下1.8版本默认的垃圾回收器到底是什么?

    • 1.8.0_181 默认(看不出来)Copy MarkCompact
    • 1.8.0_222 默认 PS + PO

JVM调优第一步,了解JVM常用命令行参数

  • JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • HotSpot参数分类

    标准: - 开头,所有的HotSpot都支持

    非标准:-X 开头,特定版本HotSpot支持特定命令

    不稳定:-XX 开头,下个版本可能取消

    java -version

    java -X

    试验用程序:

    import java.util.List;
    import java.util.LinkedList;
    
    public class HelloGC {
         
      public static void main(String[] args) {
         
        System.out.println("HelloGC!");
        List list = new LinkedList();
        for(;;) {
         
          byte[] b = new byte[1024*1024];
          list.add(b);
        }
      }
    }
    
    1. 区分概念:内存泄漏memory leak,内存溢出out of memory
    2. java -XX:+PrintCommandLineFlags HelloGC
    3. java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
      PrintGCDetails PrintGCTimeStamps PrintGCCauses
    4. java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags HelloGC
    5. java -XX:+PrintFlagsInitial 默认参数值
    6. java -XX:+PrintFlagsFinal 最终参数值
    7. java -XX:+PrintFlagsFinal | grep xxx 找到对应的参数
    8. java -XX:+PrintFlagsFinal -version |grep GC

PS GC日志详解

每种垃圾回收器的日志格式是不同的!

PS日志格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lF16xdFr-1604390947692)(./GC日志详解.png)]

heap dump部分:

eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)
                            后面的内存地址指的是,起始地址,使用空间结束地址,整体空间结束地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mg27677d-1604390947694)(GCHeapDump.png)]

total = eden + 1个survivor

调优前的基础概念:

  1. 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间)
  2. 响应时间:STW越短,响应时间越好

所谓调优,首先确定,追求啥?吞吐量优先,还是响应时间优先?还是在满足一定的响应时间的情况下,要求达到多大的吞吐量…

问题:

科学计算,吞吐量。数据挖掘,thrput。吞吐量优先的一般:(PS + PO)

响应时间:网站 GUI API (1.8 G1)

什么是调优?

  1. 根据需求进行JVM规划和预调优
  2. 优化运行JVM运行环境(慢,卡顿)
  3. 解决JVM运行过程中出现的各种问题(OOM)

调优,从规划开始

  • 调优,从业务场景开始,没有业务场景的调优都是耍流氓

  • 无监控(压力测试,能看到结果),不调优

  • 步骤:

    1. 熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器)
      1. 响应时间、停顿时间 [CMS G1 ZGC] (需要给用户作响应)
      2. 吞吐量 = 用户时间 /( 用户时间 + GC时间) [PS]
    2. 选择回收器组合
    3. 计算内存需求(经验值 1.5G 16G)
    4. 选定CPU(越高越好)
    5. 设定年代大小、升级年龄
    6. 设定日志参数
      1. -Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
      2. 或者每天产生一个日志文件
    7. 观察日志情况
  • 案例1:垂直电商,最高每日百万订单,处理订单系统需要什么样的服务器配置?

    这个问题比较业余,因为很多不同的服务器配置都能支撑(1.5G 16G)

    1小时360000集中时间段, 100个订单/秒,(找一小时内的高峰期,1000订单/秒)

    经验值,

    非要计算:一个订单产生需要多少内存?512K * 1000 500M内存

    专业一点儿问法:要求响应时间100ms

    压测!

  • 案例2:12306遭遇春节大规模抢票应该如何支撑?

    12306应该是中国并发量最大的秒杀网站:

    号称并发量100W最高

    CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(10K问题) 100台机器

    普通电商订单 -> 下单 ->订单系统(IO)减库存 ->等待用户付款

    12306的一种可能的模型: 下单 -> 减库存 和 订单(redis kafka) 同时异步进行 ->等付款

    减库存最后还会把压力压到一台服务器

    可以做分布式本地库存 + 单独服务器做库存均衡

    大流量的处理方法:分而治之

  • 怎么得到一个事务会消耗多少内存?

    1. 弄台机器,看能承受多少TPS?是不是达到目标?扩容或调优,让它达到

    2. 用压测来确定

优化环境

  1. 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G
    的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G
    的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了
    1. 为什么原网站慢?
      很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢
    2. 为什么会更卡顿?
      内存越大,FGC时间越长
    3. 咋办?
      PS -> PN + CMS 或者 G1
  2. 系统CPU经常100%,如何调优?(面试高频)
    CPU100%那么一定有线程在占用系统资源,
    1. 找出哪个进程cpu高(top)
    2. 该进程中的哪个线程cpu高(top -Hp)
    3. 导出该线程的堆栈 (jstack)
    4. 查找哪个方法(栈帧)消耗时间 (jstack)
    5. 工作线程占比高 | 垃圾回收线程占比高
  3. 系统内存飙高,如何查找问题?(面试高频)
    1. 导出堆内存 (jmap)
    2. 分析 (jhat jvisualvm mat jprofiler … )
  4. 如何监控JVM
    1. jstat jvisualvm jprofiler arthas top…

解决JVM运行中的问题

一个案例理解常用工具
  1. 测试代码:

    package com.mls.jvm.gc;
    
    import java.math.BigDecimal;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 从数据库中读取信用数据,套用模型,并把结果进行记录和传输
     */
    
    public class T15_FullGC_Problem01 {
         
    
        private static class CardInfo {
         
            BigDecimal price = new 

你可能感兴趣的:(Java虚拟机,jvm)