【Java虚拟机】万字长文,搞定Java虚拟机方方面面!3

简介: 【Java虚拟机】万字长文,搞定Java虚拟机方方面面!

4.垃圾回收调优

4.1.调优简介

1、查看虚拟机运行参数

java -XX:PrintFlagsFinal -version | findstr "GC"

2、回收器选择问题

【低延迟】还是【高吞吐量】,选择合适的回收器

  • CMS、G1、ZGC
  • ParallelGC

3、垃圾回收频繁的问题分析?

查看Full GC前后的内存占用,考虑下面几个问题

  • 数据太多
  • 数据表示太臃肿
  • 是否存在内存泄露

4.2.新生代调优

1、新生代的特点

  • 所有的new操作的内存分配非常廉价
  • TLAB thread-local allocation buffer
  • TLAB 是每个线程都会在伊甸园中分配一块私有的区域,当new一个对象的时候,先去TLAB中查看有没有足够的内存空间,如果有就占用这块内存。
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • Minor GC的时间远远低于Full GC

2、调优参数

  • 晋升阈值配置得当,让长时间存活对象尽快晋升
  • -XX:MaxTenuringThreshold=threshold :最大晋升值
  • -XX:+PrintTenuringDistribution :晋升的日志

4.3.老年代调优

  • 以CMS为例
  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC那么就无需调优,否则现场时调优新生代
  • 观察发生FullGC时老年代的占比,将老年代内存预设调大1/4 ~ 1/3
  • -XX:CMSInitatingOccupancyFraction=percent
  • 参数为0,只要老年代有垃圾就回收
  • 一般设置在百分之75到80,也就是说,预留20的空间来进行回收

4.4.GC调优案例

1、Full GC和Minor GC频繁

如果GC发生频繁,说明空间紧张,如果是新生代的空间紧张,当业务高峰期来了,大量对象被创建,很快就会把新生代的空间塞满,塞满之后还会造成一个问题,幸存区空间紧张,导致对象的晋升阈值减小,导致一些周期很短的对象也会被晋生到老年代,老年代存了大量的生命周期少的对象,发生Full GC。

增大新生代内存,新生代内存充裕后,就不会频繁发生GC,这样老年代也不会存储大量周期短的对象。

2、请求高峰期发生Full GC,单次暂停时间特别长(CMS)

CMS,查看GC日志,CMS初始标记和并发标记都是比较快的,耗时主要发生在重新标记上,重新标记不但会扫描老年代的和新生代的垃圾,可以在重新标记之前进行新生代的内存。使用参数-

XX:+CMSScavengeBeforeRemark设置重新标记之前进行新生代的一次GC回收。

3、老年代充裕的情况下,发生Full GC(CMS jdk1.7)

在1.8版本之前永久代的空间不足,也会产生Full GC。

5.字节码技术

5.1.类文件结构

1、JVM规范下类文件结构

4f537f1084234fcb8798be8e7f637dde.jpg

2、二进制字节码文件

//Hello World 示例
public class Demo16 {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

od -t -xC Demo16.class

0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
0000020 00 15 00 16 08 00 17 0a 00 18 00 19 07 00 1a 07
0000040 00 1b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 14 4c 63 6f 6d 2f 6c 69
0000160 78 69 61 6e 67 2f 44 65 6d 6f 31 36 3b 01 00 04
0000200 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c
0000220 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04
0000240 61 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61
0000260 6e 67 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75
0000300 72 63 65 46 69 6c 65 01 00 0b 44 65 6d 6f 31 36
0000320 2e 6a 61 76 61 0c 00 07 00 08 07 00 1c 0c 00 1d
0000340 00 1e 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000360 07 00 1f 0c 00 20 00 21 01 00 12 63 6f 6d 2f 6c
0000400 69 78 69 61 6e 67 2f 44 65 6d 6f 31 36 01 00 10
0000420 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
0000440 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73
0000460 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 76
0000500 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000520 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e
0000540 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c
0000560 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f
0000600 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 00
0000620 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00
0000640 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1
0000660 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 04
0000700 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d
0000720 00 00 00 09 00 0e 00 0f 00 01 00 09 00 00 00 37
0000740 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04
0000760 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00
0001000 06 00 08 00 07 00 0b 00 00 00 0c 00 01 00 00 00
0001020 09 00 10 00 11 00 00 00 01 00 12 00 00 00 02 00
0001040 13
0001041

5.2.字节码指令

1、字节码指令入门

研究两组字节码指令

(1)public cn.itcast.jvm.t5.HelloWord();构造方法的字节码指令

2a b7 00 01 b1
  • 2a:aload_0加载slot 0的局部变量,即this作为下main的invokespecial构造方法调用的参数
  • b7:invokespecial预备调用构造方法
  • 00 01:引用常量池中#1项,即【Method java/lang/Object.“<init>”😦)V】
  • b1:表示返回

(2)public static void main(java.lang.String[]);之方法的字节码指令

b2 00 02 12 03 b6 00 04 b1
  • b2:getstatic用来加载静态变量
  • 00 02:引用常量池中#2项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  • 12:ldc加载参数
  • 03:引用常量池中#3项,即【String hello world】
  • b6:invokevirtuaf预备调用成员方法。
  • 00 04:引用常量池中#4项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  • b1:表示返回

5.3.图解方法执行流程

1、原始Java代码

public class Demo17 {
    /**
     * 演示字节码指令和操作数栈、常量池的关系
     * @param args
     */
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE+1;
        int c = a + b;
        System.out.println(c);
    }
}

2、编译后的字节码文件

javap -v Demo17.class
Classfile /D:/ideaworkspace/jvm-demo/target/classes/com/lixiang/Demo17.class
  Last modified 2021-12-10; size 604 bytes
  MD5 checksum e36477501751b13feb4bebbef30bfa7b
  Compiled from "Demo17.java"
public class com.lixiang.Demo17
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // com/lixiang/Demo17
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/lixiang/Demo17;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo17.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               com/lixiang/Demo17
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public com.lixiang.Demo17();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1(栈的深度), locals=1(局部变量表的长度), args_size=1(参数的长度)
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lixiang/Demo17;
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
        line 13: 10
        line 14: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo17.java"

3、图解方法执行流程

(1)常量池载入运行时常量池

Java虚拟机的类加载器,会把main方法所在的类进行类加载操作,

会把类的字节码相关信息放入内存中,同时把常量池中的信息放入运行时常量池中,后续查找都在运行时常量池中。一些比较小的值,不会直接放到常量池中,而是存放在字节码指令中。常量池也属于方法区,只不过这里单独提出来了。

fd87062f0c694a91ae0e2aa72a4f3672.jpg

(2)方法字节码载入方法区,main线程开始运行,分配栈帧内存

(stack=2,locals=4),两个操作数栈,局部变量表中有四个槽位。


d3acbd2d2e984f999a5c32fd2ba110b8.jpg

(3)执行引擎开始执行字节码

  • bipush:将一个byte压入操作数栈(其长度会补齐4个字节),类似的指令还有
  • sipush:将一个short压入操作数栈(其余长度会补齐4个字节)
  • ldc:将一个int压入操作数栈
  • ldc2_w:将一个long压入操作数栈(分两次压入,因为long是8个字节)
  • 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入了常量池

bipush 10:将10这个数值,压入栈顶,执行引擎执行bipush 10

5ead40335637484a802803f5d086ef54.jpg

istore 1:将操作数栈中的元素弹出,放入局部变量表slot 1中,对应的代码a = 10

183b241b429e483284ba6abf0beb282f.jpg



0375e87964eb471c9e9f465cde3ef5ef.jpg

ldc #3:读取运行时常量池中#3位置,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中

注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的


47c63665cacf4b2087e6a9b74cb73044.jpg

istore 2:将操作数栈中的元素弹出,放到局部变量表的2号位置

3d66110b3cda439eac9be991ccdf8043.jpg

istore 3:将操作数栈中的元素弹出,放入局部变量表的3号位置


image.jpeg

image.jpeg

getstatic #4:在运行时常量池中找到#4,发现是一个对象,在堆内找到该对象,将其引用放入操作数栈中。


image.jpeg


image.jpeg

iload 3:将局部变量表中3号位置的元素压入操作数栈中



image.jpeg

invokevirtual 5:找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法,生成新的栈帧(分配 locals、stack等)

传递参数,执行新栈帧中的字节码。


0315d355dc1e440e8c23a5991f3e4e65.jpg

执行完毕,弹出栈帧

清除 main 操作数栈内容


4986986d2be143549fc8ebe7ce3010ab.jpg

return

完成 main 方法调用,弹出 main 栈帧,程序结束

5.4.字节码分析x=0

代码:

public class Demo2 {
  public static void main(String[] args) {
    int i=0;
    int x=0;
    while(i<10) {
      x = x++;
      i++;
    }
    System.out.println(x); //接过为0
  }
}

字节码解析:

Code:
     stack=2, locals=3, args_size=1   //操作数栈分为两个空间,局部变量表分为3个空间,参数的长度为1
        0: iconst_0           //准备一个常数0
        1: istore_1                   //将常数0放在局部变量表第一个槽位上
        2: iconst_0           //准备一个常数0
        3: istore_2           //将常数0放在局部变量表第二个槽位上
        4: iload_1                  //将局部变量表中1号槽位的数据放入操作数栈中
        5: bipush        10           //将10 放入操作数栈中,此时操作数栈中有10 和 0两个数
        7: if_icmpge     21           //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21步
       10: iload_2            //将局部变量表中2号槽位的数据放入操作数栈中,放入的数值为0
       11: iinc          2, 1       //局部变量表中2号槽位的数加1,自增后,槽位中的数值为1
       14: istore_2           //将操作数组栈中的数放入到局部变量表中的2号槽位,2号槽位有变成了0
       15: iinc          1, 1       //1号槽位进行+1,1号槽位的数值为1
       18: goto          4        //跳转到第四条指令
       21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       24: iload_2
       25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
       28: return

5.5.构造方法

1、<cinit()>V

public class Demo3 {
  static int i = 10;
  static {
    i = 20;
  }
  static {
    i = 30;
  }
  public static void main(String[] args) {
    System.out.println(i); //结果为30
  }
}

编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法cinit()V:

stack=1, locals=0, args_size=0
         0: bipush        10         //将变量10从字节码中压入操作数栈
         2: putstatic     #3                  // Field i:I
         5: bipush        20         //将变量20从字节码中压入操作数栈
         7: putstatic     #3                  // Field i:I
        10: bipush        30           //将变量30从字节码中压入操作数栈
        12: putstatic     #3                  // Field i:I
        15: return               //最后返回,以最后一次的取值为准

2、<init>()V

public class Demo4 {
  private String a = "s1";
  {
    b = 20;
  }
  private int b = 10;
  {
    a = "s2";
  }
  public Demo4(String a, int b) {
    this.a = a;
    this.b = b;
  }
  public static void main(String[] args) {
    Demo4 d = new Demo4("s3", 30);
    System.out.println(d.a);
    System.out.println(d.b);
  }
}

编译器会按从上至下的顺序,收集所有{}(实例块)和成员变量复制的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

Code:
     stack=2, locals=3, args_size=3
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: aload_0
        5: ldc           #2                  // String s1
        7: putfield      #3                  // Field a:Ljava/lang/String;
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       //原始构造方法在最后执行
       28: aload_0
       29: aload_1
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2
       35: putfield      #4                  // Field b:I
       38: return

不同方法在调用时,对应的虚拟机指令有所区别。

私有、构造、被final修饰的方法,在调用时都使用invokespecial指令。

普通成员方法在调用的时候,使用invokevirtual指令。因为编译期间无法确定该方法的内容。只有在运行期间才能确定。

静态方法在调用时使用invokestatic指令。

Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/nyima/JVM/day5/Demo5 
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4                  // Method test1:()V
        12: aload_1
        13: invokespecial #5                  // Method test2:()V
        16: aload_1
        17: invokevirtual #6                  // Method test3:()V
        20: invokestatic  #7                  // Method test4:()V
        23: return

new是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。

dup是赋值操作数栈栈顶的内容,本例为【对象引用】,本例即为【对象引用】,为什么需要两份引用呢,一个是配合invokespecial调用该对象的构造方法“init:()V‘(会消耗掉栈顶一个引用),另一个要配合astore_1赋值给局部变量。

终方法(final),私有方法(private),构造方法都是由invokespecial指令来调用,属于静态绑定。

普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态,成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象。


相关文章
|
1天前
|
Java 程序员 C++
大牛程序员用Java手写JVM:刚好够运行 HelloWorld
大牛程序员用Java手写JVM:刚好够运行 HelloWorld
|
9天前
|
缓存 监控 Java
Java虚拟机(JVM)性能调优实战指南
在追求软件开发卓越的征途中,Java虚拟机(JVM)性能调优是一个不可或缺的环节。本文将通过具体的数据和案例,深入探讨JVM性能调优的理论基础与实践技巧,旨在为广大Java开发者提供一套系统化的性能优化方案。文章首先剖析了JVM内存管理机制的工作原理,然后通过对比分析不同垃圾收集器的适用场景及性能表现,为读者揭示了选择合适垃圾回收策略的数据支持。接下来,结合线程管理和JIT编译优化等高级话题,文章详细阐述了如何利用现代JVM提供的丰富工具进行问题诊断和性能监控。最后,通过实际案例分析,展示了性能调优过程中可能遇到的挑战及应对策略,确保读者能够将理论运用于实践,有效提升Java应用的性能。 【
43 10
|
7天前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优的实用策略
在Java应用开发中,性能优化常常成为提升系统响应速度和处理能力的关键。本文将探讨Java虚拟机(JVM)调优的核心概念,包括垃圾回收、内存管理和编译器优化等方面,并提供一系列经过验证的调优技巧。通过这些实践指导,开发人员可以有效减少延迟,提高吞吐量,确保应用稳定运行。 【7月更文挑战第16天】
|
4天前
|
JSON Java BI
一次Java性能调优实践【代码+JVM 性能提升70%】
这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Review环节就可以避免。
53 0
一次Java性能调优实践【代码+JVM 性能提升70%】
|
14天前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
37 10
|
14天前
|
存储 运维 Java
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
30 9
|
12天前
|
存储 监控 Java
揭秘Java虚拟机:探索JVM的工作原理与性能优化
本文深入探讨了Java虚拟机(JVM)的核心机制,从类加载到垃圾回收,再到即时编译技术,揭示了这些复杂过程如何共同作用于Java程序的性能表现。通过分析现代JVM的内存管理策略和性能监控工具,文章提供了实用的调优建议,帮助开发者有效提升Java应用的性能。
30 3
|
14天前
|
存储 安全 Java
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
24 3
|
14天前
|
存储 Java 编译器
Java面试题:描述方法区(Method Area)的作用以及它在JVM中的演变(从永久代到元空间)
Java面试题:描述方法区(Method Area)的作用以及它在JVM中的演变(从永久代到元空间)
20 3
|
14天前
|
算法 Java
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
Java面试题:列举并解释JVM中常见的垃圾收集器,并比较它们的优缺点
24 3