• jvm虚拟机浅谈(一)


    一、jvm内存运行时区域

    1.1 程序计数器

    程序计数器作为当前线程所执行的字节码的行号指示器,保存当前线程的执行位置。当字节码解释器工作时,就是通过改变这个计算器的值来选取下一条要执行的字节码指令。每条线程都有一个独立的程序计数器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的字节码指令地址。如果线程正在执行的是Native方法,这个计数器值为空。

    1.2 本地方法栈

    本地方法栈就是执行本地native方法的栈,native方法由虚拟机实现

    1.3 java虚拟机栈

    java虚拟机栈描述的是该线程执行java方法时的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。栈帧中的局部变量表存储了方法中的基本数据类型变量、对象引用变量。栈内存默认最大1M。

    1.4 堆

    Java堆是被所有线程共享的一块内存区域。此内存区域用于存放类实例和数组。

    1.5 方法区

    方法区用于存储JVM加载的类信息、final常量、static静态变量等数据,还包含了运行时常量池,主要存放编译期生成的字面量和符号引用(在类加载后存入运行时常量池)。

    二、java字节码指令

    2.1 加载和存储指令

    加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。

    load和store

    ●load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶

    ●store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置

    指令名称

    描述

    iload_0

    将局部变量表的第1个int型变量入栈

    istore_0

    将栈顶int数值存入局部变量表的第一个位置

    astore_0

    栈顶ref对象存入第1局部变量

    aload_0

    第1个ref型变量进栈

    const、push和ldc

    ●const、push:将相应类型的常量放入栈顶

    ●ldc:则是从常量池中将常量放入栈顶

    指令名称

    描述

    iconst_1

    int型常量1进栈

    bipush

    byte型常量进栈

    ldc

    int、float或String型常量从常量池推送至栈顶

    2.2 对象创建以及访问指令

    字段调用

    指令名称

    描述

    getstatic

    获取类的静态字段,将其值压入栈顶

    putstatic

    给类的静态字段赋值

    getfield

    获取对象的字段,将其值压入栈顶

    putfield

    给对象的字段赋值

    方法调用

    指令名称

    描述

    invokestatic

    用于调用静态方法

    invokespecial

    用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法

    invokevirtual

    用于调用非私有实例方法

    invokeinterface

    用于调用接口方法

    invokedynamic

    用于调用动态方法

    2.3 流程控制指令

    指令名称

    描述

    ifeq

    当栈顶int类型数值等于0时跳转

    ifne

    当栈顶int类型数值不等于0时跳转

    if_icmpeq

    比较栈顶两int类型数值大小,当前者等于后者时跳转

    if_icmpne

    比较栈顶两int类型数值大小,当前者不等于后者时跳转

    2.4 字节码举例分析

    1. public class ByteCode {
    2. private int a = 1;
    3. public int add() {
    4. int b = 2;
    5. int c = a+b;
    6. System.out.println(c);
    7. return c;
    8. }
    9. }

    1. public ByteCode();
    2. descriptor: ()V
    3. flags: (0x0001) ACC_PUBLIC
    4. Code:
    5. stack=2, locals=1, args_size=1
    6. 0: aload_0
    7. 1: invokespecial #1 // Method java/lang/Object."":()V
    8. 4: aload_0
    9. 5: iconst_1
    10. 6: putfield #2 // Field a:I
    11. 9: return
    12. LineNumberTable:
    13. line 1: 0
    14. line 3: 4
    15. public int add();
    16. descriptor: ()I
    17. flags: (0x0001) ACC_PUBLIC
    18. Code:
    19. stack=2, locals=3, args_size=1 //stack:最大栈深度,locals:局部变量数,args_size:方法参数长度
    20. 0: iconst_2
    21. 1: istore_1
    22. 2: aload_0
    23. 3: getfield #2 // Field a:I
    24. 6: iload_1
    25. 7: iadd
    26. 8: istore_2
    27. 9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    28. 12: iload_2
    29. 13: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
    30. 16: iload_2
    31. 17: ireturn
    32. LineNumberTable:
    33. line 6: 0
    34. line 7: 2
    35. line 8: 9
    36. line 9: 16
    37. }

    操作数栈以及本地变量表变化分析

     

    三、类加载过程

    3.1 java基本类型

     在 Java 语言规范中,boolean 类型的值只有两种可能,它们分别用符号“true”和“false”来表示。

    在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。具体来说,“true”被映射为整数 1,而“false”被映射为整数 0。这个编码规则约束了 Java 字节码的具体实现。

    修改字节码中boolean的值

    1. public class Foo {
    2. public static void main(String[] args) {
    3. boolean flag = true;
    4. if (flag) System.out.println("Hello, Java!");
    5. if (flag == true) System.out.println("Hello, JVM!");
    6. }
    7. }
    1. public Foo();
    2. descriptor: ()V
    3. flags: (0x0001) ACC_PUBLIC
    4. Code:
    5. stack=1, locals=1, args_size=1
    6. 0: aload_0
    7. 1: invokespecial #1 // Method java/lang/Object."":()V
    8. 4: return
    9. LineNumberTable:
    10. line 2: 0
    11. public static void main(java.lang.String[]);
    12. descriptor: ([Ljava/lang/String;)V
    13. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    14. Code:
    15. stack=2, locals=2, args_size=1
    16. 0: iconst_1
    17. 1: istore_1
    18. 2: iload_1
    19. 3: ifeq 14
    20. 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    21. 9: ldc #3 // String Hello, Java!
    22. 11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    23. 14: iload_1
    24. 15: iconst_1
    25. 16: if_icmpne 27
    26. 19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    27. 22: ldc #5 // String Hello, JVM!
    28. 24: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    29. 27: return
    30. LineNumberTable:
    31. line 4: 0
    32. line 5: 2
    33. line 6: 14
    34. line 7: 27
    35. StackMapTable: number_of_entries = 2
    36. frame_type = 252 /* append */
    37. offset_delta = 14
    38. locals = [ int ]
    39. frame_type = 12 /* same */
    40. }

    boolean字节码修改

    java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
     awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
     java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

    1. public static void main(java.lang.String[]);
    2. descriptor: ([Ljava/lang/String;)V
    3. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    4. Code:
    5. stack=2, locals=2, args_size=1
    6. 0: iconst_2 //将iconst_1替换为iconst_2
    7. 1: istore_1
    8. 2: iload_1
    9. 3: ifeq 14
    10. 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    11. 9: ldc #1 // String Hello, Java!
    12. 11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    13. 14: iload_1
    14. 15: iconst_1
    15. 16: if_icmpne 27
    16. 19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    17. 22: ldc #2 // String Hello, JVM!
    18. 24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    19. 27: return
    20. StackMapTable: number_of_entries = 2
    21. frame_type = 252 /* append */
    22. offset_delta = 14
    23. locals = [ int ]
    24. frame_type = 12 /* same */
    25. }

    3.2 类加载

    加载,是指查找字节流,并且据此创建类的过程。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。

    类加载器双亲委派机制

     

    反向委派加载机制

     

    三方sqlDriver通过线程上下文类加载器进行加载(Mysql Driver、Oracle Driver等)

     

     

     

    3.3 链接

    3.3.1 验证阶段

    确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全

    3.3.2 准备阶段

    为类中的静态字段分配内存,并设置默认的初始值,同时还会构造与该类相关的方法表,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译期就将其结果放入了调用它的类的常量池中。

    3.3.3 解析阶段

    符号引用

    在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。

    解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

    解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

    1. public class ByteCode
    2. minor version: 0
    3. major version: 55
    4. flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    5. this_class: #5 // ByteCode
    6. super_class: #6 // java/lang/Object
    7. interfaces: 0, fields: 1, methods: 2, attributes: 1
    8. Constant pool:
    9. #1 = Methodref #6.#17 // java/lang/Object."":()V
    10. #2 = Fieldref #5.#18 // ByteCode.a:I
    11. #3 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
    12. #4 = Methodref #21.#22 // java/io/PrintStream.println:(I)V
    13. #5 = Class #23 // ByteCode
    14. #6 = Class #24 // java/lang/Object
    15. #7 = Utf8 a
    16. #8 = Utf8 I
    17. #9 = Utf8
    18. #10 = Utf8 ()V
    19. #11 = Utf8 Code
    20. #12 = Utf8 LineNumberTable
    21. #13 = Utf8 add
    22. #14 = Utf8 (I)I
    23. #15 = Utf8 SourceFile
    24. #16 = Utf8 ByteCode.java
    25. #17 = NameAndType #9:#10 // "":()V
    26. #18 = NameAndType #7:#8 // a:I
    27. #19 = Class #25 // java/lang/System
    28. #20 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
    29. #21 = Class #28 // java/io/PrintStream
    30. #22 = NameAndType #29:#30 // println:(I)V
    31. #23 = Utf8 ByteCode
    32. #24 = Utf8 java/lang/Object
    33. #25 = Utf8 java/lang/System
    34. #26 = Utf8 out
    35. #27 = Utf8 Ljava/io/PrintStream;
    36. #28 = Utf8 java/io/PrintStream
    37. #29 = Utf8 println
    38. #30 = Utf8 (I)V

    3.3.4 初始化阶段

    类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

    主动初始化

    ●当虚拟机启动时,初始化用户指定的主类;

    ●当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

    ●当遇到访问或者调用静态方法的指令时,初始化该静态方法所在的类;

    ●子类的初始化会触发父类的初始化;

    ●如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

    ●使用反射 API 对某个类进行反射调用时,初始化这个类;

    ●当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

    被动初始化

    ●通过子类引用父类的的静态字段,不会导致子类初始化

    ●通过数组定义来引用类,不会触发此类的初始化。

    ●常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

    问题:新建数组,会触发此类的链接吗

    1. public class Singleton {
    2. private Singleton() {}
    3. private static class LazyHolder {
    4. static final Singleton INSTANCE = new Singleton();
    5. static {
    6. System.out.println("LazyHolder.");
    7. }
    8. }
    9. public static Object getInstance(boolean flag) {
    10. if (flag) return new LazyHolder[2];
    11. return LazyHolder.INSTANCE;
    12. }
    13. public static void main(String[] args) {
    14. getInstance(true);
    15. System.out.println("----");
    16. getInstance(false);
    17. }
    18. }
    1. {
    2. public static java.lang.Object getInstance(boolean);
    3. descriptor: (Z)Ljava/lang/Object;
    4. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    5. Code:
    6. stack=1, locals=1, args_size=1
    7. 0: iload_0
    8. 1: ifeq 9
    9. 4: iconst_2
    10. 5: anewarray #2 // class Singleton$LazyHolder
    11. 8: areturn
    12. 9: getstatic #3 // Field Singleton$LazyHolder.INSTANCE:LSingleton;
    13. 12: areturn
    14. LineNumberTable:
    15. line 10: 0
    16. line 11: 9
    17. StackMapTable: number_of_entries = 1
    18. frame_type = 9 /* same */
    19. public static void main(java.lang.String[]);
    20. descriptor: ([Ljava/lang/String;)V
    21. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    22. Code:
    23. stack=2, locals=1, args_size=1
    24. 0: iconst_1
    25. 1: invokestatic #4 // Method getInstance:(Z)Ljava/lang/Object;
    26. 4: pop
    27. 5: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
    28. 8: ldc #6 // String ----
    29. 10: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    30. 13: iconst_0
    31. 14: invokestatic #4 // Method getInstance:(Z)Ljava/lang/Object;
    32. 17: pop
    33. 18: return
    34. LineNumberTable:
    35. line 14: 0
    36. line 15: 5
    37. line 16: 13
    38. line 17: 18
    39. }

    java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Singleton\$LazyHolder.class > Singleton\$LazyHolder.jasm.1
      
    awk 'NR==1,/stack 1/{sub(/stack 1/, "stack 0")} 1' Singleton\$LazyHolder.jasm.1 > Singleton\$LazyHolder.jasm

    java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Singleton\$LazyHolder.jasm

    java -verbose:class Singleton

    1. [0.101s][info][class,load] java.lang.PublicMethods$MethodList source: jrt:/java.base
    2. [0.101s][info][class,load] java.lang.PublicMethods$Key source: jrt:/java.base
    3. [0.102s][info][class,load] java.lang.Void source: jrt:/java.base
    4. [0.102s][info][class,load] Singleton$LazyHolder source: file:/Users/wiam/project/jvm-learn/
    5. [0.102s][info][class,load] java.lang.Readable source: jrt:/java.base
    6. [0.102s][info][class,load] java.nio.CharBuffer source: jrt:/java.base
    7. [0.103s][info][class,load] java.nio.HeapCharBuffer source: jrt:/java.base
    8. [0.103s][info][class,load] java.nio.charset.CoderResult source: jrt:/java.base
    9. ----
    10. [0.103s][info][class,load] java.lang.VerifyError source: jrt:/java.base
    11. Exception in thread "main" [0.103s][info][class,load] java.lang.Throwable$PrintStreamOrWriter source: jrt:/java.base
    12. [0.103s][info][class,load] java.lang.Throwable$WrappedPrintStream source: jrt:/java.base
    13. [0.104s][info][class,load] java.util.IdentityHashMap source: jrt:/java.base
    14. [0.104s][info][class,load] java.util.IdentityHashMap$KeySet source: jrt:/java.base
    15. java.lang.VerifyError: Operand stack overflow
    16. Exception Details:
    17. Location:
    18. Singleton$LazyHolder.()V @0: aload_0
    19. Reason:
    20. Exceeded max stack size.
    21. Current Frame:
    22. bci: @0
    23. flags: { flagThisUninit }
    24. locals: { uninitializedThis }
    25. stack: { }
    26. Bytecode:
    27. 0000000: 2ab7 0007 b1
    28. at Singleton.getInstance(Singleton.java:11)
    29. at Singleton.main(Singleton.java:16)
    30. [0.104s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/java.base
    31. [0.104s][info][class,load] java.lang.Shutdown source: jrt:/java.base
    32. [0.104s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

    Singleton\$LazyHolder.jasm内容

    1. super class Singleton$LazyHolder
    2. version 55:0
    3. {
    4. static final Field INSTANCE:"LSingleton;";
    5. private Method "":"()V"
    6. stack 0 locals 1 //不符合jvm规范
    7. {
    8. aload_0;
    9. invokespecial Method java/lang/Object."":"()V";
    10. return;
    11. }
    12. static Method "":"()V"
    13. stack 2 locals 0
    14. {
    15. new class Singleton;
    16. dup;
    17. invokespecial Method Singleton."":"()V";
    18. putstatic Field INSTANCE:"LSingleton;";
    19. getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
    20. ldc String "LazyHolder.";
    21. invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
    22. return;
    23. }

    新建数组不会链接元素类LazHolder,在getInstance(false)时才真正链接和初始化。

  • 相关阅读:
    FFmpeg命令行工具-实用命令
    Acrel-1200分布式光伏运维平台
    你觉得ACID别扭吗?已经习惯了的ACID,原来是为了凑单词缩写?
    大模型时代的机器人研究
    Docker搭建Redis cluster集群
    @Bean注解详解
    IEEE投稿模板下载
    Redis安装,性能优化
    365天挑战LeetCode1000题——Day 035 每日一题 + 二分查找 13
    (数字图像处理MATLAB+Python)第十章图像分割-第四,五节:分水岭分割和综合案例
  • 原文地址:https://blog.csdn.net/wuweiwoshishei/article/details/126379979