JVM 底层之类加载

本贴最后更新于 1599 天前,其中的信息可能已经斗转星移

JVM 底层之类加载

klass 模型

Java 的每个类,在 JVM 中,都有一个对应的 Klass 类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息……

看下 klass 模型类的继承结构

image.png

从继承关系上也能看出来,类的元信息是存储在原空间的

类加载器将.class 文件加载进系统

将.class 文件解析,生成的就是 InstanceKlass

MetaspaceObj

	JDK8以后类的元信息都是存储在类的元空间里的就是**MetaspaceObj** 是所有类的顶层父类。

InstanceKlass

	InstanceKlass就是我们写的Java类(非数组),InstanceKlass就是类加载器把Java文件存储到内存中经过解析后生成的。  

	InstanceKlass包含的一些属性:注解 _annotations、__method..

普通的 Java 类在 JVM 中对应的是 instanceKlass 类的实例,再来说下它的三个子类

  1. InstanceMirrorKlass:用于表示 java.lang.Class,class 对象(就是我们所说的堆区就是存储在这里)Java 代码中获取到的 Class 对象,实际上就是这个 C++ 类的实例,存储在堆区,学名镜像类
  2. InstanceRefKlass:用于表示 java/lang/ref/Reference 类的子类,(引用就是存放在这里)
  3. InstanceClassLoaderKlass:用于遍历某个加载器加载的类

总结: 类加载器将.class 文件加载进系统,将.class 文件解析,生成的类的元信息以 InstanceKlass 存储在 JVM 中

ArrayKlass

ArrayKlass 就是用来存储数组的元信息。

Java 的数组

静态数据类型	JVM中内置的 八种数据类型

动态数组类型	运行时动态生成

证明: 为什么 Java 中的数组是动态生成的?

public class Test_1 {
public static void main(String[] args) {
//        System.out.printf(Test_1_B.str);
int[] arr = new int[1];
while (true);
}
}

运行结果:

image.png

结果生成一个 newarray 顾名思义就是生成一个数组嘛。

我们查看字节码手册也是可以看的到的如下信息:

指令码 助记符 说明
0xbc newarray 创建一个指定原始类型(如 int, float, char…)的数组,并将其引用值压入栈顶

我们再测试一个引用类型的数组:

public class Test_1 {
public static void main(String[] args) {
//        System.out.printf(Test_1_B.str);
int[] arr = new int[1];

Test_1[] arr2 =  new Test_1[1];
while (true);
}
}

输出:

image.png

可以看出输出的为 anewarray ,字节码文档解释如下:

指令码 助记符 说明
0xbd anewarray 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶

以上得知什么?简单来说就是,

newarray 也就是基本数据类型,在 JVM 中的存在形式是以 TypeArrayKlass 存在的。

anewarray 也就是引用类型的数组,在 JVM 中的存在形式是以 ObjArrayKlass 存在的。

类加载的过程

类加载由 7 个步骤完成,看图

image.png

加载

  1. 通过类的全限定名获取存储该类的 class 文件(没有指明必须从哪获取)
  2. 解析成运行时数据,即 instanceKlass 实例,存放在方法区
  3. 在堆区生成该类的 Class 对象,即 instanceMirrorKlass 实例

全限定名: 包名 + 类名

程序随便你怎么写,随便你用什么语言,只要能达到这个效果即可

就是说你可以改写 openjdk 源码,你写的程序能达到这三个效果即可

何时加载

主动使用时

  1. new、getstatic、putstatic、invokestatic
  2. 反射
  3. 初始化一个类的子类会去加载其父类
  4. 启动类(main 函数所在类)
  5. 当使用 jdk1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

预加载:包装类、String、Thread

因为没有指明必须从哪获取 class 文件,脑洞大开的工程师们开发了这些

  1. 从压缩包中读取,如 jar、war
  2. 从网络中获取,如 Web Applet
  3. 动态生成,如动态代理、CGLIB
  4. 由其他文件生成,如 JSP
  5. 从数据库读取
  6. 从加密文件中读取

验证

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备

为静态变量分配内存、赋初值

实例变量是在创建对象的时候完成赋值的,没有赋初值一说(final 修饰)

image.png

如果被 final 修饰,在编译的时候会给属性添加 ConstantValue 属性,准备阶段直接完成赋值,即没有赋初值这一步

验证:

public static final int a = 10;
public static int b = 10;

输出:

image.png

解析

将常量池中的符号引用转为直接引用

直接引用就是指向内存地址

间接引用指向运行时常量池

每个类都有一个常量池

class 常量池(静态的) HSDB 常量池(动态的)

解析后的信息存储在 ConstantPoolCache 类实例中

  1. 类或接口的解析
  2. 字段解析
  3. 方法解析
  4. 接口方法解析

何时解析

思路:

  1. 加载阶段解析常量池时
  2. 用的时候

openjdk 是第二种思路,在执行特定的字节码指令之前进行解析:

anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield

初始化

执行静态代码块,完成静态变量的赋值

  1. 定义一个 static 静态代码块,JVM 底层会生成一个 clinit,生成 clinit 方法
  2. 代码顺序和定义顺序保持一致的。

image.png

来一个小问题代码如下:

public class Test_21 {
public static void main(String[] args) {
Test_21_A obj = Test_21_A.getInstance();
System.out.println(Test_21_A.val1);
System.out.println(Test_21_A.val2);
}
}
class Test_21_A {
public static int val1;
public static int val2 = 1;
public static Test_21_A instance = new Test_21_A();
Test_21_A() {
val1++;
val2++;
}
public static Test_21_A getInstance() {
return instance;
}
}

输出:

image.png

why?

很明显:

val1 = 0 val2 = 1

执行代码块之后各自 +1,

所以输出结果为 1 2

再看一个题:

public class Test_22 {
public static void main(String[] args) {
Test_22_A obj = Test_22_A.getInstance();
System.out.println(Test_22_A.val1);
System.out.println(Test_22_A.val2);
}
}
class Test_22_A {
public static int val1;
public static Test_22_A instance = new Test_22_A();
Test_22_A() {
val1++;
val2++;
}
public static int val2 = 1;
public static Test_22_A getInstance() {
return instance;
}
}

输出:

image.png

分析:

结合上一题这个代码会出来一个覆盖的情况,所以输出为 1 1

类加载细节

JVM 加载类是懒加载模式

public class Test_1 {
public static void main(String[] args) {
System.out.printf(Test_1_B.str);
while (true);
}
}
class Test_1_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
static {
System.out.println("B Static Block");
}
}

输出:

证明:

类只有在使用的时候才会加载

public class Test_1 {
public static void main(String[] args) {
System.out.printf(new Test_1_B().str);
while (true);
}
}
class Test_1_A {
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
public String str = "A str";

static {
    System.out.println("B Static Block");
}
  public String str = "A str";

static {
    System.out.println("B Static Block");
}
}

输出:

image.png

证明:

主动使用子类,父类也会加载。

public class Test_4 {
public static void main(String[] args) {
Test_4 arrs[] = new Test_4[1];
}
}
class Test_4_A {
static {
System.out.println("Test_4_A Static Block");
}
}

其实没有输出,以上代码只是定义了一个数据类型。

public class Test_6 {
public static void main(String[] args) {
System.out.println(Test_6_A.str);
}
}
class Test_6_A {
public static final String str = "A Str";
static {
System.out.println("Test_6_A Static Block");
}
}

输出:

image.png

因为我们使用 final 修饰它定义的是常量,JVM 将常量 str 写入了 Test_6 的常量池中

public class Test_7 {
public static void main(String[] args) {
System.out.println(Test_7_A.uuid);
}
}
class Test_7_A {
public static final String uuid = UUID.randomUUID().toString();
static {
System.out.println("Test_7_A Static Block");
}
}

输出:

image.png

uuid 是动态生成的所以 JVM 没办法把 UUID 放入到 Test_7 的常量池中,所以会加载,它生成的是动态代码段

public class Test_8 {
static {
System.out.println("Test_8 Static Block");
}
public static void main(String[] args) throws ClassNotFoundException {
Class

输出:

image.png

这就是一个反射,读取静态属性时,反射也会读取

读取静态字段的实现原理

public class Test_1 {
public static void main(String[] args) {
System.out.printf(new Test_1_B().str);
while (true);
}
}
class Test_1_A {
public String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
static {
System.out.println("B Static Block");
}
}

思路:

1. 先去Test_1_B的镜像类中去取,如果有直接返回,如果没有,会沿着继承链将请求往上抛。这种算法的性能随着继承链的death而上升,算法复杂度为0(0);

2. 借助另外的数据结构实现,使用K-V的格式存储,查询性能为O(1)

Hotspot 就是使用的第二种方式,借助另外的数据结构 ConstantPoolCache,常量池类 ConstantPool 中有个属性_cache 指向了这个结构。每一条数据对应一个类 ConstantPoolCacheEntry。

ConstantPoolCache 主要用于存储某些字节码指令所需的解析(resolve)好的常量项,例如给[get|put]static、[get|put]field、invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。

!!!!撒花!!!!

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...