美文网首页
2021-01-17 JVM-性能调优

2021-01-17 JVM-性能调优

作者: 竹blue | 来源:发表于2021-01-17 23:53 被阅读0次

    垃圾收集器分类

    垃圾收集器分类.png
    • 串行收集器->Serial和Serial Old

      只能有一个垃圾回收线程执行,用户线程暂停适用于'内存比较小的嵌入式设备。
      
    • 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old

      多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于'科学计算、后台处理等若交互场景 。
      
    • 并发收集器[停顿时间优先]->CMS、G1

      用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的
      
      时候不会停顿用户线程的运行。 适用于'相对时间有要求的场景,比如Web 。
      
    • 优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选 如果允许停顿时间超过1秒,选择并行或JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器

    常见问题

    1. 吞吐量和停顿时间

    吞吐量和停顿时间

    • 停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间

    • 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)

      停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 
      高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
      

      小结 :这两个指标也是评价垃圾回收器好处的标准。

    2. 如何选择合适的垃圾收集器

    官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

    • 优先调整堆的大小让服务器自己来选择

    • 如果内存小于100M,使用串行收集器

    • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选

    • 如果允许停顿时间超过1秒,选择并行或JVM自己选

    • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

      case:G1 GC 默认最小内存是 2G,稳定运行内存是6G

      如果服务器是4C8G 且对响应时间要求不是特别高,可用使用G1;

      如果是CPU特别好且内存较小(PS:核数和内存占比较高,如8C4G),对响应时间要求最高,使用CMS。

    1. 内存泄漏与内存溢出的区别

      内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
      内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

    2. young gc会有stw吗?

      不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关 系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时 间相对来说就小很多了。

    3. major gc和full gc的区别

      Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触 发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大 时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7 永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。

    4. 什么是直接内存

      Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通 常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内 存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是 有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

    5. 垃圾判断的方式

      引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没 有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
      引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有 一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。

      GC ROOT:

      • Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需 要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好 是其它形式的GC root;

      • Thread 线程,激活状态的线程;

      • Stack Local 栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;

      • JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定 义

      • JNI Global JNI中的全局变量引用的对象;同上

      • Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。

      • Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些类,JVM也没有过多的信息。

    6. 不可达的对象一定要被回收吗?

      即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真 正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行 一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个 对象建立关联,否则就会被真的回收。

    7. 为什么要区分新生代和老年代?

      当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不 同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合 适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制 成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分 配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    8. G1与CMS的区别是什么

      CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的 产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

    9. 方法区中的无用类回收

      方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面 3 个条件才能算是 “无用的类” :
      a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
      b. 加载该类的 ClassLoader 已经被回收。
      c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    JVM性能优化指南

    JVM性能优化.png

    JVM的性能优化可以分为代码层面非代码层面。 在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码,提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。 在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。

    注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身 已经做了很多的内部优化操作,大家要注意的 是不要为了调优和调优

    JVM编译

    JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式, 追求更高的运行效率。

    解释器

    Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。

    刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就 会把这些代码认定为"热点代码"。
    

    即使编译器(JIT)-- JDK7各层次的优化。

    Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。

    即时编译器会把这些"热点代码"编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存
    中。
    

    执行引擎

    热点代码:被多次调用的方法、被多次执行的循环体。说明:如果是循环体的话,会对其所在的方法进行即时编译 -- OSR.

    如何判断是否为热点代码? -- 热点探测

    热点探测分为两种:

    • 基于采样分析-- 无法解决线程阻塞造成方法一直在栈中的问题

    • 基于计数器的热点探测

      1. 方法调用计数器 : C1模式下 默认1500次;C2模式是10000次 可通过参数:-XX:CompileThreadHold来设置

        解释:统计的是一定时间内方法的调用次数,如果一定时间内调用次数小于设置的次数如1500,那么下个周期,调用次数设置为700次 -- 此算法为热度衰减算法 可通过参数:-XX:CounterHalfLifeTime来设置

      2. 回边计数器::统计的是循环体调用的次数,此处和方法调用计数器的区别是:统计的是绝对次数

    热点探测方法计数器执行流程.png
    热点探测回边计数器执行流程.png

    JVM编译期自我优化--方法内联:如果static、final 修饰的方法a调用了方法b且方法b不是太大的情况下JVM在编译阶段会将方法b的内容直接合并进方法a中,方法b大小限制热点方法:325字节;,非热点方法35字节。

    JVM编译期自我优化--逃逸分析:确定我们的对象会不会被外部访问到,如果一个对象仅仅是在方法中,那么可以直接将对象分配到栈(栈帧是线程私有的)中,分析过程就是逃逸分析,逃逸分析的目的是:分析对象的作用域,减少对象创建、回收的时间,同步锁的锁消除

    逃逸分析命令:-XX:+DoEscapeAnalysis , 系统默认开启。

    C1 -- C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序; 适用于:带界面的方法调用。

    C2 -- C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序;适用于:服务层面的方法调用。

    JIT5各级别.png 方法JIT流程分类.png

    开启分层编译的命令:

    • -XX:+TieredCompilation 默认开启,关闭直接走C2;

    • -XX:TieredStopAtLevel = 1表示只走C1;

    内存

    内存分配

    正常情况下不需要设置,那如果是促销或者秒杀的场景呢? 每台机器配置2c4G,以每秒3000笔订单为例,整个过程持续60秒

    秒杀场景JVM调优之内存分配.png
    内存溢出(OOM)

    一般会有两个原因: (1)大并发情况下 (2)内存泄露导致内存溢出

    大并发[秒杀]

    浏览器缓存、本地缓存、验证码

    CDN静态资源服务器
    集群+负载均衡 动静态资源分离、限流[基于令牌桶、漏桶算法] 应用级别缓存、接口防刷限流、队列、Tomcat性能优化 异步消息中间件
    Redis热点数据对象缓存
    分布式锁、数据库锁 5分钟之内没有支付,取消订单、恢复库存等

    内存泄露导致内存溢出

    ThreadLocal引起的内存泄露,最终导致内存溢出

    public class TLController {
     @RequestMapping(value = "/tl")
     public String tl(HttpServletRequest request) {
         ThreadLocal<Byte[]> tl = new ThreadLocal<Byte[]>();
         // 1MB
         tl.set(new Byte[1024*1024]);
         return "ok";
    } }
    

    排查流程:

    1. 启动

    2. 使用jmeter模拟10000次并发

    3. top命令查看

      top
      top -Hp PID

    4. jstack查看线程情况,发现没有死锁或者IO阻塞的情况

      jstack PID
      java -jar arthas.jar ---> thread

    5. 查看堆内存的使用,发现堆内存的使用率已经高达88.95%

      jmap -heap PID
      java -jar arthas.jar ---> dashboard

    6. 此时可以大体判断出来,发生了内存泄露从而导致的内存溢出,那怎么排查呢?

      jmap -histo:live PID | more

      获取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io

    GC

    此处以G1垃圾收集器调优为例

    是否选用G1

    官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases

    1. 50%以上的堆被存活对象占用
    2. 对象分配和晋升的速度变化非常大
    3. 垃圾回收时间比较长
    G1调优
    1. 使用G1GC垃圾收集器: -XX:+UseG1GC

      修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

      Throughput       Min Pause       Max Pause      Avg Pause       GC count
        99.16%         0.00016s         0.0137s        0.00559s          12
      
    2. 调整内存大小再获取gc日志分析

      -XX:MetaspaceSize=100M
      -Xms300M
      -Xmx300M
      

      比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间

      Throughput       Min Pause       Max Pause      Avg Pause       GC count
        98.89%          0.00021s        0.01531s       0.00538s           12
      
    3. 调整最大停顿时间

      -XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
      

      比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间

      Throughput       Min Pause       Max Pause      Avg Pause       GC count
        98.96%          0.00015s        0.01737s       0.00574s          12
      
    1. 启动并发GC时堆内存占用百分比

      -XX:InitiatingHeapOccupancyPercent=45 G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行 GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
      

      比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

      Throughput       Min Pause       Max Pause      Avg Pause       GC count
        98.11%          0.00406s        0.00532s       0.00469s          12
      
    G1调优最佳实战

    官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations

    1. 不要手动设置新生代和老年代的大小,只要设置整个堆的大小
    G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 如果手动设置了大小就意味着放弃了G1的自动调优
      参考:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
    
    1. 不断调优暂停时间目标
    一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就 不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以 对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到 满足。
    
    1. 使用-XX:ConcGCThreads=n来增加标记线程的数量
    IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过 低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。 IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高 ConcGCThreads。
    
    1. MixedGC调优
    -XX:InitiatingHeapOccupancyPercent
    -XX:G1MixedGCLiveThresholdPercent
    -XX:G1MixedGCCountTarger
    -XX:G1OldCSetRegionThresholdPercent
    
    1. 适当增加堆内存大小

    2. 不正常的Full GC

    有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由 Metaspace区域引起的。可以通过MetaspaceSize适当增加其大家,比如256M。
    

    CPU占用率高

    排查流程:

    1. top
    2. top -Hp PID 查看进程中占用CPU高的线程id,即tid
    3. jstack PID | grep tid

    JVM常用命令

    jps --查看java进程

    jps.png

    jinfo

    1. 实时查看和调整JVM配置参数

    2. 查看用法

      jinfo -flag name PID 查看某个java进程的name属性的值

      jinfo -flag MaxHeapSize PID
      jinfo -flag UseG1GC PID
      
      jinfo查看java进行name值.png
    3. 修改

      注意:参数只有被标记为manageable的flags可以被实时修改

      jinfo -flag [+|-] PID
      jinfo -flag <name>=<value> PID
      
    4. 查看曾经赋过值的一些参数

      jinfo -flags PID
      
      jinfo查看历史值.png

    jstat

    1. 查看虚拟机性能统计信息

    2. 查看类装载信息

      jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
      
    jstat查看类装载信息.png
    1. 查看垃圾收集信息

      jstat -gc PID 1000 10
      

    jstack

    1. 查看线程堆栈信息

    2. 用法

      jstack PID
      
      jstack用法.png
    3. 排查死锁案例

      //运行主类
      public class DeadLockDemo {
          public static void main(String[] args) {
              DeadLock d1 = new DeadLock(true);
              DeadLock d2 = new DeadLock(false);
              Thread t1 = new Thread(d1);
              Thread t2 = new Thread(d2);
              t1.start();
              t2.start();
          }
      
      }
          //定义锁对象 class MyLock{
          public static Object obj1 = new Object();
          public static Object obj2 = new Object();
      }
      
      //死锁代码
      class DeadLock implements Runnable {
          private boolean flag;
      
          DeadLock(boolean flag) {
              this.flag = flag;
          }
      
          public void run() {
              if (flag) {
                  while (true) {
                      synchronized (MyLock.obj1) {
                          System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
      
                          synchronized (MyLock.obj2) {
                              System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
                          }
                      }
                  }
              } else {
                  while (true) {
                      synchronized (MyLock.obj2) {
                          System.out.println(Thread.currentThread().getName() + "---获得obj2锁");
      
                          synchronized (MyLock.obj1) {
                              System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
                          }
                      }
                  }
              }
          }
      }
      
      • 运行结果:
      案例死锁-运行结果.png
      • jstack分析

        jstack-死锁分析1.png

        把打印信息拉到最后可以发现

        jstack-死锁分析(2)

        (2).png)

    jmap

    1. 打印出堆内存相关信息

    2. dump出堆内存相关信息

      jmap -heap PID
      jinfo -flag UsePSAdaptiveSurvivorSizePolicy 35352
      -XX:SurvivorRatio=8
      
      jmap-打印堆栈信息
    3. 内存溢出自动dump出该文件。

      jmap -dump:format=b,file=heap.hprof PID
      
      jmapdump日志信息.png
    4. 要是在发生堆内存溢出的时候,能自动dump出该文件就好了

      一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件 
      -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
      
    5. 关于dump下来的文件

      一般dump下来的文件可以结合工具来分析,如内存分析工具--MAT

    JVM工具

    • 工具层面,会有非常多的场景,这时候首选不要嵌入的工具 -- 影响CPU算例,占用服务器资源
      提醒:Tprofiler可以使用一下,原因是:1. 可以随时开关,2. 可以精确分析创建了多少对象、哪行代码出现了问题。

    内存分析工具--MAT

    Java堆分析器,用于查找内存泄漏

    Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照

    它在触发快照的时候保存了很多信息:Java对象和类信息。

    通常在写Heap Dump文件前会触发一次Full GC。

    下载地址 :https://www.eclipse.org/mat/downloads.php

    1. 获取dump文件

      • 手动

        jmap -dump:format=b,file=heap.hprof 44808
        
      • 自动

        -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
        
    1. Dump的信息

      • All Objects
        Class, fields, primitive values and references
      • All Classes
        Classloader, name, super class, static fields
      • Garbage Collection Roots
        Objects defined to be reachable by the JVM
      • Thread Stacks and Local Variables
        The call-stacks of threads at the moment of the snapshot, and per-frame information about local
        objects
    2. 使用

      • Histogram:可以列出内存中的对象,对象的个数及其大小

        Class Name:类名称,java类名 Objects:类的对象的数量,这个对象被创建了多少个
        Shallow Heap:一个对象内存的消耗大小,不包含对其他对象的引用
        Retained Heap:是shallow Heap的总和,即该对象被GC之后所能回收到内存的总和

        右击类名--->List Objects--->with incoming references--->列出该类的实例

        右击Java对象名--->Merge Shortest Paths to GC Roots--->exclude all ...--->找到GC Root以及原因

      • Leak Suspects:查找并分析内存泄漏的可能原因

        Reports--->Leak Suspects--->Details

      • Top Consumers:列出大对象

    性能分析工具--Tprofiler

    官网:https://github.com/alibaba/TProfiler/wiki

    相关文章

      网友评论

          本文标题:2021-01-17 JVM-性能调优

          本文链接:https://www.haomeiwen.com/subject/qmctzktx.html