Java代码编写到执行流程为:
- 编写Java程序,这些代码会保存到xxx.java文件里。
- 通过javac Test 将java文件编译为.class文件(字节码(ByteCode)文件).
- 通过虚拟机运行字节码文件,这一步是不分平台的,只要你电脑上有jre,就可以运行字节码文件,执行java程序。
JAVA是一个面向对象,静态类型,编译执行,有VM/GC的运行时,跨平台的高级语言。
什么是字节码?
字节码其实是一系列指令的组合,jvm在执行程序的时候会根据字节码去一条条执行对应的指令,直到遇到return语句。
所以字节码其实是给机器看的二进制语言,所有java程序都需要转换为字节码才能被JVM执行。
JVM内存结构
JVM是基于栈的虚拟机。
每个线程会有一个独属自己的栈,用于创建栈帧。
每一个方法会创建一个栈帧,栈帧由操作数栈,局部变量表(数组)以及一个class引用组成。
实例解析
我们知道JAVA里面String类其实是做过处理的,它可以用new String("a")
去创建对象,也可以用="a"
创建,这两者的创建出来的对象栈地址是否一致呢?它们具体是怎么构建的呢?我们今天来从字节码的层面来看下整个代码执行过程。
源代码很简单,就是分别用两种方式去实例了两个对象,然后比较两者地址是否一致:
public class JvmDemo {
public static void main(String[] args) {
String x = "abc";
String x1 = new String("abc");
String x2 = "abc";
System.out.println(x == x1);//false
System.out.println(x == x2);//true
}
}
输出结果为false和true。
字节码文件如下:
Constant pool:
#1 = Methodref #8.#21 // java/lang/Object."<init>":()V
#2 = String #22 // abc
#3 = Class #23 // java/lang/String
#4 = Methodref #3.#24 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #27.#28 // java/io/PrintStream.println:(Z)V
#7 = Class #29 // com/lht/demo/JvmDemo
#8 = Class #30 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 StackMapTable
#16 = Class #31 // "[Ljava/lang/String;"
#17 = Class #23 // java/lang/String
#18 = Class #32 // java/io/PrintStream
#19 = Utf8 SourceFile
#20 = Utf8 JvmDemo.java
#21 = NameAndType #9:#10 // "<init>":()V
#22 = Utf8 abc
#23 = Utf8 java/lang/String
#24 = NameAndType #9:#33 // "<init>":(Ljava/lang/String;)V
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Class #32 // java/io/PrintStream
#28 = NameAndType #37:#38 // println:(Z)V
#29 = Utf8 com/lht/demo/JvmDemo
#30 = Utf8 java/lang/Object
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 (Ljava/lang/String;)V
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 println
#38 = Utf8 (Z)V
{
public com.lht.demo.JvmDemo();
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 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #2 // String abc
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: ldc #2 // String abc
15: astore_3
16: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: aload_2
21: if_acmpne 28
24: iconst_1
25: goto 29
28: iconst_0
29: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
32: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
35: aload_1
36: aload_3
37: if_acmpne 44
40: iconst_1
41: goto 45
44: iconst_0
45: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
48: return
下面是我自己的一些理解,不一定对,仅供参考。
最上面的是常量池,里面存放的是各种class引用方法引用,还有一些字符串常量的引用。
main
方法上面那段,其实是调用的该类的无参构造方法,JAVA代码里面如果没有显示的制定构造方法会默认初始一个无参构造方法去调用object的init方法。
然后我们看下main方法:
- ldc指令是将目标对象从常量池压入操作数栈,这里压入的是上面常量池里对应标号为#2的"abc"
- 将其存在本地变量表的第一个槽位
- new一个string类型对象,复制其地址,压入栈中
- 将"abc"压入栈中
- 对栈顶前两个对象执行string(“abc”)的调用,将其结果存在本地变量表的第二个槽位
- 从常量池拿到"abc"存到第三个槽位
- 从常量池拿到System.out.PrintStream
- 加载槽位1,2的值并作比较
- 如果相等初始0,否则初始1,我理解这里的0对应 true,1是false
- 调用PrintStream.println方法输出结果
- 加载1,3槽位值比较
- 根据结果初始0或者1并输出
从上面流程可以看出,赋值的"abc"和new String(“abc”)产生的方式是完全不一样的,一个是直接从字符串常量池里面拿出来,一个是需要调用String的构造函数去新建一个对象,再将对象的地址指过来。
这里的字符串常量池我理解其实就是一个缓存池,为避免字符串频繁创建,对已经创建过得字符串放到池中,后续用到的话在从池里面拿。