一、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 字节码举例分析
public class ByteCode {
private int a = 1;
public int add() {
int b = 2;
int c = a+b;
System.out.println(c);
return c;
}
}
public ByteCode();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 1: 0
line 3: 4
public int add();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1 //stack:最大栈深度,locals:局部变量数,args_size:方法参数长度
0: iconst_2
1: istore_1
2: aload_0
3: getfield #2 // Field a:I
6: iload_1
7: iadd
8: istore_2
9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_2
13: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
16: iload_2
17: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 9
line 9: 16
}
操作数栈以及本地变量表变化分析
三、类加载过程
3.1 java基本类型
在 Java 语言规范中,boolean 类型的值只有两种可能,它们分别用符号“true”和“false”来表示。
在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。具体来说,“true”被映射为整数 1,而“false”被映射为整数 0。这个编码规则约束了 Java 字节码的具体实现。
修改字节码中boolean的值
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}
public Foo();
descriptor: ()V
flags: (0x0001) 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 2: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #3 // String Hello, Java!
11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iload_1
15: iconst_1
16: if_icmpne 27
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #5 // String Hello, JVM!
24: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
LineNumberTable:
line 4: 0
line 5: 2
line 6: 14
line 7: 27
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 14
locals = [ int ]
frame_type = 12 /* same */
}
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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2 //将iconst_1替换为iconst_2
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #1 // String Hello, Java!
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iload_1
15: iconst_1
16: if_icmpne 27
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #2 // String Hello, JVM!
24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 14
locals = [ int ]
frame_type = 12 /* same */
}
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等。
public class ByteCode
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // ByteCode
super_class: #6 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#18 // ByteCode.a:I
#3 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #21.#22 // java/io/PrintStream.println:(I)V
#5 = Class #23 // ByteCode
#6 = Class #24 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 add
#14 = Utf8 (I)I
#15 = Utf8 SourceFile
#16 = Utf8 ByteCode.java
#17 = NameAndType #9:#10 // "<init>":()V
#18 = NameAndType #7:#8 // a:I
#19 = Class #25 // java/lang/System
#20 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(I)V
#23 = Utf8 ByteCode
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (I)V
3.3.4 初始化阶段
类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
主动初始化
●当虚拟机启动时,初始化用户指定的主类;
●当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
●当遇到访问或者调用静态方法的指令时,初始化该静态方法所在的类;
●子类的初始化会触发父类的初始化;
●如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
●使用反射 API 对某个类进行反射调用时,初始化这个类;
●当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
被动初始化:
●通过子类引用父类的的静态字段,不会导致子类初始化
●通过数组定义来引用类,不会触发此类的初始化。
●常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
问题:新建数组,会触发此类的链接吗
public class Singleton {
private Singleton() {}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
static {
System.out.println("LazyHolder.<clinit>");
}
}
public static Object getInstance(boolean flag) {
if (flag) return new LazyHolder[2];
return LazyHolder.INSTANCE;
}
public static void main(String[] args) {
getInstance(true);
System.out.println("----");
getInstance(false);
}
}
{
public static java.lang.Object getInstance(boolean);
descriptor: (Z)Ljava/lang/Object;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ifeq 9
4: iconst_2
5: anewarray #2 // class Singleton$LazyHolder
8: areturn
9: getstatic #3 // Field Singleton$LazyHolder.INSTANCE:LSingleton;
12: areturn
LineNumberTable:
line 10: 0
line 11: 9
StackMapTable: number_of_entries = 1
frame_type = 9 /* same */
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_1
1: invokestatic #4 // Method getInstance:(Z)Ljava/lang/Object;
4: pop
5: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #6 // String ----
10: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: iconst_0
14: invokestatic #4 // Method getInstance:(Z)Ljava/lang/Object;
17: pop
18: return
LineNumberTable:
line 14: 0
line 15: 5
line 16: 13
line 17: 18
}
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.jasmjava -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Singleton\$LazyHolder.jasm
java -verbose:class Singleton
[0.101s][info][class,load] java.lang.PublicMethods$MethodList source: jrt:/java.base
[0.101s][info][class,load] java.lang.PublicMethods$Key source: jrt:/java.base
[0.102s][info][class,load] java.lang.Void source: jrt:/java.base
[0.102s][info][class,load] Singleton$LazyHolder source: file:/Users/wiam/project/jvm-learn/
[0.102s][info][class,load] java.lang.Readable source: jrt:/java.base
[0.102s][info][class,load] java.nio.CharBuffer source: jrt:/java.base
[0.103s][info][class,load] java.nio.HeapCharBuffer source: jrt:/java.base
[0.103s][info][class,load] java.nio.charset.CoderResult source: jrt:/java.base
----
[0.103s][info][class,load] java.lang.VerifyError source: jrt:/java.base
Exception in thread "main" [0.103s][info][class,load] java.lang.Throwable$PrintStreamOrWriter source: jrt:/java.base
[0.103s][info][class,load] java.lang.Throwable$WrappedPrintStream source: jrt:/java.base
[0.104s][info][class,load] java.util.IdentityHashMap source: jrt:/java.base
[0.104s][info][class,load] java.util.IdentityHashMap$KeySet source: jrt:/java.base
java.lang.VerifyError: Operand stack overflow
Exception Details:
Location:
Singleton$LazyHolder.<init>()V @0: aload_0
Reason:
Exceeded max stack size.
Current Frame:
bci: @0
flags: { flagThisUninit }
locals: { uninitializedThis }
stack: { }
Bytecode:
0000000: 2ab7 0007 b1
at Singleton.getInstance(Singleton.java:11)
at Singleton.main(Singleton.java:16)
[0.104s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/java.base
[0.104s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[0.104s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base
Singleton\$LazyHolder.jasm内容
super class Singleton$LazyHolder
version 55:0
{
static final Field INSTANCE:"LSingleton;";
private Method "<init>":"()V"
stack 0 locals 1 //不符合jvm规范
{
aload_0;
invokespecial Method java/lang/Object."<init>":"()V";
return;
}
static Method "<clinit>":"()V"
stack 2 locals 0
{
new class Singleton;
dup;
invokespecial Method Singleton."<init>":"()V";
putstatic Field INSTANCE:"LSingleton;";
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "LazyHolder.<clinit>";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
return;
}
新建数组不会链接元素类LazHolder,在getInstance(false)时才真正链接和初始化。