一篇图文彻底弄懂Class文件是如何被加载进JVM的原创
导读
1. Class.forName究竟是怎么获取Class对象的,Class对象又是什么?
2. Class文件是如何被加载到JVM里面的?
3. 类变量是存在堆中还是存在方法区中?
1、加载一个Class文件
思考: 1.有如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public 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
1
2A[] arrays = new A[10];
3.猜猜以下代码会不会输出 init A
1
2System.out.println(A.DESC);
1.1、加载阶段
JVM规范并没有规定java.lang.Class类的实例要放到Java堆中,对于HotSpot虚拟机,是放到方法区里面的。 这个class对象作为程序访问方法区中的这些类型数据的外部接口。
-
通过类全限定名获取定义此类的二进制字节流;
-
将字节流代表的静态存储结构转换为方法区的运行时数据结构;
-
在内存中生成此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
1.1.1、如何触发加载Class文件
-
遇到new、getstatic、putstatic或者invokestatic字节码指令的时候,如果类还没有初始化。对应场景为:
-
new一个对象;
-
读取或者设置一个类的静态字段;
-
调用类的静态方法的时候;
-
使用
java.lang.reflect
包的方法对类进行反射的时候,如果类还没有初始化; -
初始化类的时候,如果父类还没有初始化,则触发父类初始化;
-
虚拟机器启动时,main方法所在的类会首先进行初始化;
-
JDK1.7中使用动态语言支持的时候,如果一个java.lang.invoke.MethodHandler实例最后解析为:REF_getStatic,REF_putStatic,REF_invokeStatic方法句柄的时候,并且句柄所对应的类没有进行过初始化。
https://www.artima.com/insidejvm/ed2/jvm5.html
ClassLoader的引用
指的是加载这个Class文件的ClassLoader实例的引用;Class实例引用
指的是类加载器在加载类信息并放到方法区之后,然后创建对应的Class类型的实例,并把该实例的引用保存到Class实例引用中。1.1.2、获取二进制流的方式
( https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.3 ) 并没有指定class文件二进制流需要从哪里以什么方式获取,目前主要有以下几种获取方式:
-
zip包,延伸为JAR、EAR、WAR包;
-
网络,如Applet;
-
动态代理;
-
JSP生成;
-
数据库获取;
1.1.3、验证二进制字节流
1.2、连接阶段
1.2.1、验证阶段
验证阶段做什么事情
2 3 4 5 6 7 8 9 10 11 |
/** * Created by arthinking on 4/1/2020. */ public class TestVerify { public static void main(String[] args) { System.out.println("Hello world !!!"); } } |
java com.itzhai.jvm.loadclass.TestVerify
|
|
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) |
1.2.1.1、文件格式验证
1.2.1.2、元数据验证
1.2.1.3、字节码验证
-
操作数栈放置了int类型数据,却当成long类型使用;
-
把父类对象赋值给了子类数据类型;
-
…
1.2.1.4、符号引用验证
-
全限定名是否可以找到对应的类;
-
指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
-
校验类,字段和方法的可见性;
1.2.2、准备阶段
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.3
注意: static final类型的常量value会在准备阶段被初始化为常量指定的值。 静态变量存储在内存的PremGen(方法区域)空间中,其值存储在Heap中
1.2.3、解析阶段
关于动态语言的支持:通过invokedynamic指令支持动态语言。 该指令会对符号引用进行解析,但是不会缓存解析的结果,每次执行指令都需要重新解析。
-
类或接口 CONSTANT_Class_info
-
字段 CONSTANT_Fieldref_info
-
类方法 CONSTANT_Methodref_info
-
接口方法 CONSTANT_InterfaceMethodref_info
-
方法类型 CONSTANT_MethodType_info
-
方法句柄 CONSTANT_MethodHandle_info
-
调用限定符 CONSTANT_InvokeDynamic_info
符号引用解析的过程或校验的过程中,可能又会触发另一个类的加载。
1.3、初始化阶段
<clinit>
方法对类变量进行初始化的过程,注意,这个方法不是构造方法。1.3.1、<clinit>
方法
生成<clinit>
方法的实例
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static { DESC = "hello world!!!"; } private static String DESC; public void test() { DESC = "a"; } public static void main(String[] args) { System.out.println(DESC); } } |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#1 = Methodref #8.#26 // java/lang/Object."<init>":()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 |
<clinit>
方法。 这里指令比较简单,主要是: 拿到”hello world!!!“字符串的引用,把他设置到DESC类变量中。关于<clinit>
方法的注意事项
-
顺序问题:静态语句块后面的静态变量,静态语句块中可以赋值,但不可以访问;
-
继承执行顺序:无需显示调用,虚拟机会保证子类的
<clinit>
方法执行前,父类的<clinit>
方法已经执行完毕; -
接口的
<clinit>
方法:虽然接口不能有静态语句块,但是可以给静态变量初始化值,所以也可以生成<clinit>
方法; -
接口继承:除非使用到父接口的变量,否则执行子接口的
<clinit>
方法不需要先执行父接口的<clinit>
方法; -
在并发场景,虚拟机会保证一个类的
<clinit>
方法只有一个线程执行,其他线程会阻塞,所以要确保静态代码块中不要写可能回到成进程阻塞的代码。
References
http://blog.jamesdbloom.com/JVMInternals.html#constant_pool
https://stackoverflow.com/questions/8387989/where-are-static-methods-and-static-variables-stored-in-java
http://www.thejavageek.com/2013/06/19/the-string-constant-pool/
https://www.artima.com/insidejvm/ed2/jvm5.html
欢迎关注微信公众号《Java架构杂谈》。