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


相关文章
|
2月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
68 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
36 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
2月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
2月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
46 1
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
52 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
3月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
122 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
4月前
|
C# 开发者 Windows
震撼发布:全面解析WPF中的打印功能——从基础设置到高级定制,带你一步步实现直接打印文档的完整流程,让你的WPF应用程序瞬间升级,掌握这一技能,轻松应对各种打印需求,彻底告别打印难题!
【8月更文挑战第31天】打印功能在许多WPF应用中不可或缺,尤其在需要生成纸质文档时。WPF提供了强大的打印支持,通过`PrintDialog`等类简化了打印集成。本文将详细介绍如何在WPF应用中实现直接打印文档的功能,并通过具体示例代码展示其实现过程。
376 0
|
4月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
63 0
|
4月前
|
缓存 前端开发 Java
浅析JVM invokedynamic指令与Java Lambda语法
【8月更文挑战第27天】在Java的演进历程中,invokedynamic指令的引入和Lambda表达式的出现无疑是两大重要里程碑。它们不仅深刻改变了Java的开发模式和性能表现,还极大地推动了Java在函数式编程和动态语言支持方面的进步。本文将从技术角度浅析JVM中的invokedynamic指令及其与Java Lambda语法的紧密联系。
62 0
|
4月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
169 0