【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调用,属于动态绑定,即支持多态,成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象。


相关文章
|
18天前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
1天前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
6天前
|
存储 算法 前端开发
JVM架构与主要组件:了解Java程序的运行环境
JVM的架构设计非常精妙,它确保了Java程序的跨平台性和高效执行。通过了解JVM的各个组件,我们可以更好地理解Java程序的运行机制,这对于编写高效且稳定的Java应用程序至关重要。
20 3
|
14天前
|
Java 编译器 测试技术
Java零基础教学(03):如何正确区别JDK、JRE和JVM??
【8月更文挑战第3天】Java零基础教学篇,手把手实践教学!
38 2
|
15天前
|
人工智能 Java 编译器
Java零基础(3) - 区别JDK、JRE和JVM
【8月更文挑战第3天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
36 1
|
20天前
|
Arthas 监控 算法
JVM成神路终章:深入死磕Java虚拟机序列总纲
JVM成神路终章:深入死磕Java虚拟机序列总纲
|
20天前
|
监控 Oracle Java
(一)JVM成神路之初识虚拟机 - 探寻Java虚拟机的前世今生之秘
JVM(Java Virtual Machine)Java虚拟机的概念大家都不陌生,Java之所以可以做到“一次编译,到处运行”的跨平台性,其根本原因就在于JVM。JVM是建立在操作系统(OS)之上的,Java虚拟机屏蔽了开发人员与操作系统的直接接触,我们在通过Java编写程序时,只需要负责编写Java代码即可,关于具体的执行则会由JVM加载字节码后翻译成机械指令交给OS执行。
|
12天前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优与性能提升
本文旨在为Java开发者提供一条清晰的路径,以深入掌握Java虚拟机(JVM)的内部机制和性能调优技巧。通过具体案例分析,我们将探讨如何识别性能瓶颈、选择合适的工具进行监控与调试,以及实施有效的优化策略,最终达到提高应用程序性能的目的。文章不仅关注理论,更注重实践应用,帮助读者在面对复杂的Java应用时能够游刃有余。
34 0
|
20天前
|
存储 Java 对象存储
Java虚拟机(JVM)中的栈(Stack)和堆(Heap)
在Java虚拟机(JVM)中,栈(Stack)和堆(Heap)是存储数据的两个关键区域。它们在内存管理中扮演着非常重要的角色,但各自的用途和特点有所不同。
30 0
|
20天前
|
存储 算法 Java
(四)JVM成神路之深入理解虚拟机运行时数据区与内存溢出、内存泄露剖析
前面的文章中重点是对于JVM的子系统进行分析,在之前已经详细的阐述了虚拟机的类加载子系统以及执行引擎子系统,而本篇则准备对于JVM运行时的内存区域以及JVM运行时的内存溢出与内存泄露问题进行全面剖析。