
InstanceKlass,保存了类的信息的C++对象java.lang.Class对象与InstanceKlass相关联,在Java代码中获取类的信息以及存储静态字段的数据。Note
- InstanceKlass在方法区,通常Java代码无法直接访问,从而控制开发者访问数据的范围,可以使用HSDB查看。它向Class类暴露了方法,可以使用Class的反射访问到InstanceKlass的部分数据。 - HSDB:JDK8使用java -cp sa-jdi.jar sun.jvm.hotspot.HSDB ; JDK17使用JAVA_HOME\bin目录下的jhsdb - 静态字段:JDK8之前静态字段全部储存在方法区,8之后部分存储在堆区。
验证(Verification):确保被加载的类的正确性。
准备(Preparation):为类的静态变量分配内存并设置初始值。
0,引用为null等)解析(Resolution):将符号引用(cp info \#xx)转换为指向内存的直接引用(形如@0x000000007e0000c0)。
连接(Linking):连接包含验证、准备、初始化三个阶段。连接阶段不会执行程序员写得代码。
<clinit>()方法,真正初始化类变量和其他资源。
<cinit>()执行静态变量赋值语句;执行静态代码块(即Java中的static{})<clinit>()先于子类执行Note
- <cinit>()先执行赋值语句还是先执行代码块和java代码中编写的顺序一致,所以为保证先赋值再执行代码块,需要将所有的 static Object varX = new Object 放到类声明紧接的下一行。
- 调用 Class.forName(String) 、访问类的静态变量、new一个对象或者执行Main方法的类四种情况会触发初始化阶段。注意变量被final修饰且等号右边是常量,不会触发初始化,因为在3阶段变量已经被赋值。
类加载器是Java虚拟机提供给程序去实现获取类和接口字节码是数据的技术,这也意味着类加载器不止可以使用Java编写。类加载器只参与了第一个阶段的第一个步骤,获取字节码文件。
类加载器的应用场景:SPI机制、类的热部署、Tomcat类的隔离、Arthas。
类加载器分为虚拟机底层用C++实现的启动类加载器和JDK默认提供的Java编写的扩展类加载器和应用程序类加载器。
Warning
本篇所描述的类加载器环境是JDK8及之前版本,JDK8之后的版本不适用。
由Hotspot虚拟机提供的、使用C++编写的类加载器。默认加载 JAVA_HOME/jre/lib下的类文件,比如rt.jar、tools.jar、resources.jar。
使用getClassLoader()方法获取启动类加载器返回null,说明了启动类加载器不会被程序员直接使用。如果想使用启动类加载器加载用户jar方法有两个:
jre/lib目录中-Xbootclasspath/a:jar包目录/jar包名 虚拟机参数(推荐)扩展类加载器由JDK提供使用Java编写的类加载器,默认加载JAVA_HOME/jre/lib/ext目录下的类文件。扩展类加载器加载用户jar的方法:
jre/lib/ext目录-Djava.ext.dirs=jar包目录 虚拟机参数(推荐),这种方式会覆盖原始扩展目录,多个目录使用;(或者:)隔开加载classpath下的类文件,包括项目文件和项目使用的第三方依赖文件,即运行的程序通常由应用程序类加载器加载。
图中展示了扩展类加载器和应用程序类加载器的继承关系。

现有如下代码:
package xyz.sl;
public class Main {
public static void main(String[] args) throws IOException {
// new B02(); // new一个B02对象
System.out.println(B02.a);
System.in.read();
}
}
class A02 {
static int a = 0;
static {
a = 1;
}
}
class B02 extends A02{
static {
a = 2;
}
}
子类B02的初始化需要先初始化父类A02,因此A02.a的值被赋值为1。因为main方法没有触发B02的初始化,所以B02.a不会被赋值为2,所以最终程序输出结果为1。但若取消代码中的注释,new B02()触发B02的初始化,a被赋值为2,最终会输出2。
首先来看没有取消注释的情况。打开Jclasslib加载编译生成的Main.class文件,查看main方法的指令。第二行可以看到是 getstatic \#13 <xyz/sl/B02.a : I>。
接下来运行main方法,然后再cmd中使用jps命令查看刚刚运行的Main程序的Pid(jps命令和java命令在同一个文件夹下面)。然后打开HSDB窗口,点击File->Attach to HotSpot->输入刚才得到的Pid->点击确定。在HSDB中呈现给我们的就是Main进程在JVM内存中的字节码。Attach成功之后,点击Tools->Class Browser->输入包名并点击回车。
点击Main所在的class引用,再点击main()所在的方法引用,现在呈现的就是java程序实际执行的保存在JVM内存的代码。在第二行能明显看到该处与字节码文件中不一致,也就意味着实际执行的指令是 getstatic #13 \[Field int a\] of class xyz.sl.A02 @0x000001229d000a00 。也就是说虽然代码中通过B02.a访问,实际加载到内存的引用是A02.a。

当取消掉注释,new B02()触发了新对象的创建。导致B02中的静态代码块被执行,最终输出2。
