这篇文章本来只是想讲一下class文件格式,讲着讲着越讲越多。JVM这一块吧,知识比较散比较多,如果深研究下去如死扣《深入理解Java虚拟机》,这本书很深很细,全记住是不可能的,其实也没必要。趁这个机会直接把标题中的这些的主要知识点都总结一下,不会过深,也不会是太浮于表面的八股文,总结一下比较好记,也省的后面自己再忘了。
主要内容包括以下几部分:
class文件格式
JVM加载class文件流程
JVM运行时内存区域
对象分配内存流程
一,class文件格式
在idea中,一般我们直接点开target目录中的class文件,idea会帮助我们反编译后展示为java文件,那么如何观察到class文件的格式呢?
这里我们需要在idea中安装一个Jclasslib的插件,如下图所示:
鼠标光标停在类名上,点击view->show bytecode with jclasslib,即可查看到class文件。如下图所示:
class文件[注2]包含但不限于以下几个部分,
开头是著名的魔数0xCAFEBABE
(如果没有特殊说明,这里及下面的进制都是16进制),CAFEBABE这个魔数是用于标识和校验这个文件是否是一个class文件的。
此外,class文件中还包括如Minor Version
、Major Version
,用于标识Java版本号;
constant_pool(常量池)
,常量池用于存储字面量(Literal)
和符号引用(Symbolic References)
。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包含下面几类常量[1]:
被模块导出或者开放的包(Package)
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符()
方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
常量池中的这些常量和符号引用,在JVM运行时会存放到方法区中的常量池中[1]。而如Fields、Methods等元数据信息,存放在方法区中。
另外,什么是符号引用呢?这就涉及到了JVM的懒加载
机制[2],就是说在编译成字节码class文件后,JVM不会立即加载class文件,而是用到的时候再进行加载[注1]。这就导致了class文件在被编译出来后,无法立即被分配到内存,也就没有直接指向内存的指针。那么,我们的字节码指令,又是如何调用类、方法、成员变量的呢?
在字节码class文件中,方法、成员变量、类以全限定名(即字符串常量)的方式被引用。如下图所示,Fieldref_info,即存储指向成员变量的指针(在下图中举例的是指向字符串String s),可以看到,描述了其类的全限定名、成员变量的名字和其类型的全限定名,点击跳转到字段,会跳到字段中,即完成了通过字符串常量作为指针,跳转到字段/方法/类的功能,这就是所谓的字符引用,说白了就是把全限定名的字符串作为引用的标识。
这些符号引用一部分会在类加载阶段或者第一次使用的时候就要被转化为直接引用,这种转换成为静态解析
。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
[1]。
java虚拟机支持5种方法调用字节码指令:
invokestatic:调用静态方法
invokespecial:用于调用实例构造器()方法、私有方法和父类中的方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时再确定一个实现该接口的对象
invokedynamic:先在运行时动态解析出调用带你限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
其中只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法
”(Non-Virtual-Method),与之相反,其他方法就被称为“虚方法
”[1]。