JVM源码深入分析第一讲JVM字节码
首先给出一个程序:
Public class Demo{
Public static void main(String[] args){
System.out.println(“hello world”);
}
}
看到这个程序是不是很惊讶,对的任何一个语言的最简单的入门级程序,输出一个字符串“hello world”,那么我们是怎么运行的呢?相信大家都知道步骤吧:
- javac Demo.java
- Java Demo
那么第一步很明显是将java的源文件也就是上面的代码编译成.class字节码文件,第二步则是运行这个字节码,然后命令行界面就输出了hello world,相信大家说这个很简单啊,有啥好讲的,那么这一节我就针对这个简单的程序给大家展示一下这个字节码内的内容,然后再下一节我们在看看java这个命令是怎么启动一个JVM并且运行我们生成的这个字节码并输出了hello world。
Java提供了一个命令叫做javap,也就是用来将.class字节码文件反编译为我们人能理解的字节码形式,那么我们在上面编译好的.class文件路径输入javap -verbose Demo来看看结果:
上图就是字节码的反编译形式,是不是有些懵,且听我解释下,在这里我一行一行的进行解释[//也就是助记符,你可以把它看做是注释]:
Classfile /C:/Users/white/Desktop/java/Demo.class //这里表明了来自Demo.class这个类反编译过来的
Last modified 2018-10-22; size 413 bytes//这个就是磁盘上.class的文件属性:修改时间和大小
MD5 checksum 895a18d39bd79b9f02c70a747802a2c4//这个大家都知道了- -,MD5的信息摘要
Compiled from "Demo.java"//表明这个.class从哪个类编译过来的
public class Demo
minor version: 0 //这里就表明了JVM的版本信息,最小版本是0
major version: 52//最大版本是52查阅oracle的文档可以知道这里是JDK8,就类似于android开发中的sdk版本号一样
flags: ACC_PUBLIC, ACC_SUPER //这里就表明了这个Demo.class的类的标记:公有的、超类而非继承类
Constant pool://这里就是大家常说的常量池了
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V 方法表存储方法信息,前面的()V是JVM用来标识方法的形式,()标识入参,后面的V标识返回值,由于init方法没有返回值所以就是()V,至于这里的init我以后再给大家说,其实就是实例化最开始执行的初始化方法,注意这里不是构造器
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; 属性表存储属性的信息,可以看到这里包括方法里的属性,即main方法里面的属性
#3 = String #18 // hello world 字符串常量池
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V 同上,(Ljava/lang/String;)V表示入参是个字符串数组,在java中用L表示数组,返回值为Void
#5 = Class #21 // Demo 类常量池表明了这个字节码中用到的类,这里的Demo表示了Demo类本身,下面的Object代表了超类Object,为什么这里会出现Object类呢?大家都知道吧,在Java中所有的类均继承自Object类~
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init> //utf8表就保存了这个字节码内用到的所有utf8的字面量,包括方法描述符()V、类信息、字符串等,都是保存在这里,每个池子前面的#num表示了这个常量池的序号,而常量池名称后面的 #num表示了这里面的引用向哪个常量池,比如#6 = Class #22 表示了里面的引用指向22号常量池,即utf8表中的utf8符号java/lang/Object
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Demo.java
#15 = NameAndType #7:#8 // "<init>":()V 这里的NameAndType完整的表明了一个方法的描述,即方法名和方法的入参和返回值~,以下的类似
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Demo
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{//ok常量池过后,就是我们的每个方法的字节码描述
public Demo();//首先是构造器,还记得在java中你要是不写构造器,编译器会默认给你加上一个无参构造对吧,这里就是证明
descriptor: ()V//同样方法描述符表示无参数、无返回值
flags: ACC_PUBLIC//方法访问标记表明是公有的
Code://以下就是无参数构造中的供JVM运行的字节码
stack=1, locals=1, args_size=1 //这里是JVM栈帧中的操作数栈的深度和局部变量表的参数
0: aload_0 //这里就是字节码了 表示加载局部变量表中的 0号位的变量放到操作数栈顶,在实例方法中0号位就是存放this的地方
1: invokespecial #1 // Method java/lang/Object."<init>":()V 而这里就是调用了父类的无参构造,基础不好的同学建议先看基础,所以就不难看出了上面为何会包含Object类的信息
4: return // 最后返回
LineNumberTable: //这个是给debug调试源码时的对应符号位
line 1: 0//表明源码第一行代表了源码第一行关联到0号这个字节码处
public static void main(java.lang.String[]);//上面是对于构造器的表示,接下来是对于main方法的描述
descriptor: ([Ljava/lang/String;)V//同样描述符,表明入参是一个字符数组返回值为void
flags: ACC_PUBLIC, ACC_STATIC//增加了一个访问符号为ACC_STATIC表明这是一个静态方法
Code:
stack=2, locals=1, args_size=1//栈深度为2,本地变量表为1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; //getstatic指令表示获取一个静态对象的引用压入操作数栈的栈顶,这里压入的是System的out静态对象
3: ldc #3 // String hello world 将常量池中的字符串压入栈顶
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V //最后通过invokevirtual指令调用println方法输出刚才压入栈中的字符串字面量
8: return //最后返回
LineNumberTable://同样包括debug的信息看下图:
line 3: 0
line 4: 8
看着这张图想必大家都明了了吧
}
SourceFile: "Demo.java"
以上就是一个输出hello world的字节码解释,表面上写起来看起来简简单单,其实内藏乾坤,而这些就是我们要晋级必须要了解的,相信看完大家会有很多的疑问比如:方法描述符是什么?getStatic等等这些又是什么?栈帧是什么,局部变量表又是什么?不打紧,在之后我会通过源码来给大家展示这些到底是什么?毕竟学习源码最好的方法就是带着问题去看寻找问题的答案。有了这些常量池信息,那么我们下一节就详细看看JVM到底是如何启动的并且加载我们的主类并运行输出hello world的过程。有了这些认知想必大家对JVM会有更深入的认识,而不是去一昧的背别人给的结论导致面试时没有逻辑和深度~