导读
- Class.forName究竟是怎么获取Class对象的,Class对象又是什么?
- Class文件是如何被加载到JVM里面的?
- 类变量是存在堆中还是存在方法区中?
以下是类的生命周期:
其中,如果是动态绑定或者晚期绑定,解析阶段不会再准备阶段后立刻执行。接下来我们就来看看是如何按照这个流程加载一个Class文件的。
思考:
1.有如下代码:
public class TestLoadSubClass { public static void main(String[] args) { System.out.println(B.value); } } class A { static { System.out.println("init A ..."); } static int value = 100; static final String DESC = "test"; } class B extends A { static { System.out.println("init B ..."); } }
猜猜会不会输出 init B
2.猜猜以下语句会不会输出 init A
A[] arrays = new A[10];
3.猜猜以下代码会不会输出 init A
System.out.println(A.DESC);
JVM规范并没有规定java.lang.Class类的实例要放到Java堆中,对于HotSpot虚拟机,是放到方法区里面的。这个class对象作为程序访问方法区中的这些类型数据的外部接口。
如上图,加载阶段主要做以下事情:
如上图,当以下任何一种情况发生的时候,会触发加载Class文件:
java.lang.reflect
包的方法对类进行反射的时候,如果类还没有初始化;这个时候通过类的全限定名称获取类的二进制字节流。
此时这个字节流为静态存储结构,需要转换为方法区的运行时数据结构。结构如上图方法区中所示。每个类生成一个对应的结构,结构里面的信息详细介绍参考此文:The Java Virtual Machine
其中:
ClassLoader的引用
指的是加载这个Class文件的ClassLoader实例的引用;
Class实例引用
指的是类加载器在加载类信息并放到方法区之后,然后创建对应的Class类型的实例,并把该实例的引用保存到Class实例引用中。
如上图描述的,JVM规范5.3. Creation and Loading并没有指定class文件二进制流需要从哪里以什么方式获取,目前主要有以下几种获取方式:
如上图所示,在加载阶段就已经开始做部分验证工作了,但是验证还是属于连接阶段的动作,下面介绍验证阶段。
如上图:连接阶段包括:验证,准备,解析
为了解释这一步的作用,我们先来做一个实验。
有如下一个类:
package com.itzhai.jvm.loadclass;
/**
* Created by arthinking on 4/1/2020.
*/
public class TestVerify {
public static void main(String[] args) {
System.out.println("Hello world !!!");
}
}
我们把Java文件编译为class文件,并执行之:
java com.itzhai.jvm.loadclass.TestVerify
可以发现输出:
Hello world !!!
现在我们使用前面Class文件16进制背后的秘密介绍的十六进制编辑方法,对class文件进行随意编辑,这里我们可以把常量池计数器故意调小一点,保存之后再次执行class文件:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid constant pool index 33 in class file com/itzhai/jvm/loadclass/TestVerify
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
可以发现抛出了异常:非法的常量池索引33,这正是验证阶段干的事情。
验证阶段干什么事情
我们知道,class文件是可以被认为篡改的,虚拟机如果直接拿来执行,可能会把系统给搞崩溃了,所以一定要先对Class文件做严格的验证。验证阶段主要完成以下检测动作:
主要按照Class文件16进制背后的秘密文章中的阐述的格式,严格的进行校验。
主要是语义校验,保证不存在不符合Java语言规范的元数据信息,如:没有父类,继承了final类,接口的非抽象类实现没有完整实现方法等。
主要对数据流和控制流进行分析,确定成行语义是否合法,符合逻辑。不合法的例子:
解析阶段发生的验证,当把符号引用转化为直接引用的时候进行验证。这主要是对类自身以外的信息进行匹配性校验。主要包括:
这个阶段还并没有开始执行类的构造方法,而只是为类变量分配内存并设置类变量初始值(零值)。这些变量所使用的内存都将在方法区中分配。
基本数据类型的零值:2.3. Primitive Types and Values
这里只分配static变量,不包括实例变量。
注意:static final类型的常量value会在准备阶段被初始化为常量指定的值。
静态变量存储在内存的PremGen(方法区域)空间中,其值存储在Heap中
解析阶段主要将常量池内的符号引用替换为直接引用。
**符号引用:**字面量,引用目标不一定已经加载到内存中;
**直接引用:**直接指向目标的指针,或者相对偏移量,或是一个能简介定位到目标的句柄。直接引用和虚拟机实现的内存布局相关。
**关于动态语言的支持:**通过invokedynamic指令支持动态语言。该指令会对符号引用进行解析,但是不会缓存解析的结果,每次执行指令都需要重新解析。
解析主要针对以下七类符号引用进行:
常量池中的14种常量结构
符号引用解析的过程或校验的过程中,可能又会触发另一个类的加载。
这阶段开始执行Java程序代码,这一步主要是执行类构造器
方法对类变量进行初始化的过程,注意,这个方法不是构造方法。
下面就来介绍一下这个方法:
方法此方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的方法,主要是给类变量做初始化工作的方法。
方法的实例有如下代码:
public class TestInit {
static {
DESC = "hello world!!!";
}
private static String DESC;
public void test() {
DESC = "a";
}
public static void main(String[] args) {
System.out.println(DESC);
}
}
这个类中有一个静态变量DESC,并且在静态代码块中进行了赋值操作,我们看看其生成的汇编代码:
Constant pool:
#1 = Methodref #8.#26 // java/lang/Object."":()V
#2 = String #27 // a
#3 = Fieldref #7.#28 // com/itzhai/classes/TestInit.DESC:Ljava/lang/String;
...
#7 = Class #34 // com/itzhai/classes/TestInit
...
#9 = Utf8 DESC
#10 = Utf8 Ljava/lang/String;
...
#23 = Utf8 <clinit>
#28 = NameAndType #9:#10 // DESC:Ljava/lang/String;
...
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #6 // String hello world!!!
2: putstatic #3 // Field DESC:Ljava/lang/String;
5: return
LineNumberTable:
line 9: 0
line 10: 5
可以发现,生成了这样的一个方法。此方法既是生成的
方法。这里指令比较简单,主要是:拿到”hello world!!!“字符串的引用,把他设置到DESC类变量中。
方法的注意事项
方法执行前,父类的
方法已经执行完毕;
方法:**虽然接口不能有静态语句块,但是可以给静态变量初始化值,所以也可以生成
方法;
方法不需要先执行父接口的
方法;
方法只有一个线程执行,其他线程会阻塞,所以要确保静态代码块中不要写可能回到成进程阻塞的代码。JVM Internals
Where are static methods and static variables stored in Java?
运行时常量池 和 字符串常量池相关:The String Constant Pool
关于方法区中的Class文件信息说明:Chapter 5 of Inside the Java Virtual Machine
《深入理解Java虚拟机-JVM高级特性与最佳实践》
Chapter 5. Loading, Linking, and Initializing
本文作者: arthinking
本文链接: https://www.itzhai.com/jvm/how-java-runtime-data-area-works.html
一篇图文彻底弄懂Class文件是如何被加载进JVM的 | 类加载器,加载,连接,初始化
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!