JVM之性能调优

561 阅读6分钟

1.什么是JVM?

  • Java虚拟机

  • 栈:线程私有,存放着每个方法的局部变量,执行Java方法

  • 堆:被所有线程共享的一块内存区域,存放着对象

  • 方法区(元空间):存放着静态变量、常量、类元信息(加载的类信息)

  • 程序计数器:线程私有,当前线程所执行的Java字节码的行号指示器

  • 本地方法栈:执行Native方法(通过JNI直接调用本地C/C++库)

  • 类的加载机制:将.class字节码文件加载到内存中,并将这些静态数据转化成方法区中可运行的数据结构(加载、校验、准备、解析、初始化、使用、卸载)

  • 运行代码

    public class Math {
        public int compute() {
            int a = 1;
            int b = 2;
            int c = (a + b) * 10;
            return c;
        }
    
        public static void main(String[] args) {
            Math math = new Math();
            math.compute();
            System.out.println("test");
        }
    }
    
  • 字节码反汇编

    Compiled from "Math.java"
    public class test.Math {
      public test.Math();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public int compute();
        Code:
           0: iconst_1
           1: istore_1
           2: iconst_2
           3: istore_2
           4: iload_1
           5: iload_2
           6: iadd
           7: bipush        10
           9: imul
          10: istore_3
          11: iload_3
          12: ireturn
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class test/Math
           3: dup
           4: invokespecial #3                  // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: invokevirtual #4                  // Method compute:()I
          12: pop
          13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          16: ldc           #6                  // String test
          18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          21: return
    }
    
  • GC Root对象

    Java虚拟机栈(局部变量表)中的引用的对象
    方法区中静态引用指向的对象
    仍处于存活中的线程对象
    Native方法中JNI引用的对象
  • 可达性分析算法

  • GC(Garbage Collection)算法

2.为什么要JVM调优?

  • 前提:已无代码再优化问题

    为了提升系统性能,JVM调优并不是我们的首选方案,因为“优化代码”所需要的成本更低,JVM层面的调优不光是对服务器性能有要求,不同的项目也需要制定相应的调优方案。

    • 设计高性能算法、使用相匹配的设计模式
    • 合理运用Java四大引用
    引用 GC回收时机 使用示例
    强引用 如果一个对象具有强引用,那垃圾回收器绝不会回收它 Object obj = new Object();
    软引用 在内存实在不足时(即将发生OOM),会对软引用进行回收 SoftReference softObj = new SoftReference();
    弱引用 第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收 WeakReference weakObj = new WeakReference();
    虚引用 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例 不会使用
  • 原因:减少Stop the World事件发生

    顾名思义“系统卡顿”,这种现象让用户体验变差,但它的出现也是为了保证系统JVM能按照一定机制正常运行。

  • JVM调优的目的:减少GC的频率和Full GC的次数(减少Stop the World事件)

3.怎样做JVM调优?

  • 参数说明

    命令参数 功能描述
    -verbose:gc 显示GC的操作内容
    -Xms20M 初始化堆大小为20M(如果不指定默认为物理内存的1/64)
    -Xmx20M 设置堆的最大分配内存20M
    -Xmn10M 设置新生代的内存大小为10M
    -XX:+PrintGCDetails 打印GC的详细log日志
    -XX:SurvivorRatio=8 新生代中Eden区域与Survivor区域的大小比值为8:1:1
    -XX:PretenureSizeThreshold 设置从Eden直接升入老年代的对象大小
    • 一般情况下-Xms-Xmx所设置的大小都是一样的,因为这样能够防止避免在GC后调整堆大小带来的压力。
    • -Xms 、-Xmx 、-Xmn 后接设置的内存大小,默认单位为Byte,也可以手动设置成M或者是K,表示MB或KB
  • GC log分析(IDEA)

    • 设置VM options

      (-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8)

  • 运行代码

    public class MinorGCTest {
        public static final int _1MB = 1024 * 1024;
        //1KB = 1024B(字节)
        //1MB = 1024KB
        public static void testAllocation() {
            byte[] a1, a2, a3, a4;
            a1 = new byte[2 * _1MB];
            a2 = new byte[2 * _1MB];
            a3 = new byte[2 * _1MB];
            a4 = new byte[2 * _1MB];
        }
    
        public static void main(String[] args) {
            testAllocation();
        }
        
    }
    
  • 运行你的程序得到GC log

    [GC (Allocation Failure) [PSYoungGen: 8160K->824K(9216K)] 8160K->6976K(19456K), 0.0028212 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC (Ergonomics) [PSYoungGen: 824K->0K(9216K)] [ParOldGen: 6152K->6802K(10240K)] 6976K->6802K(19456K), [Metaspace: 3157K->3157K(1056768K)], 0.0034982 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     PSYoungGen      total 9216K, used 2130K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff814930,0x00000000ffe00000)
      from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
      to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
     ParOldGen       total 10240K, used 6802K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 66% used [0x00000000fec00000,0x00000000ff2a4998,0x00000000ff600000)
     Metaspace       used 3164K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 345K, capacity 388K, committed 512K, reserved 1048576K
    
  • 打包成jar包输入带参数进行运行程序(类中没有一个对象的创建)

> 但问题来了,为什么没有类的创建为什么Eden区的使用率不是0,而是16%
  • 利用Java VisualVMJVM性能分析工具,查看堆中年轻代和老年代的变化

    打开dos窗口,输入jvisualvm、jconsole命令,即可打开Java VisualVm工具

    能够自动检测到当前电脑正在运行的Java程序,Visual GC窗口需要更新Java VisualVM插件中心URL,再进行下载。

  • 运行代码

    public class HeapTest {
        byte[] a = new byte[1024 * 100];
    
        public static void main(String[] args) throws InterruptedException {
            ArrayList<HeapTest> heapTests = new ArrayList<>();
            while (true) {
                heapTests.add(new HeapTest());
                Thread.sleep(10);
            }
        }
    }
    
  • 对于不同场景选择合适的垃圾收集器