欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM类结构及加载过程

程序员文章站 2022-03-19 16:03:29
Java的每个类,在JVM中,都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息。jvm中类结构klass InstanceKlass java类(非数组)普通的Java类在JVM中对应的是instanceKlass类的实例 InstanceMirrorKlass 用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类......

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

jvm中类结构

klass

      InstanceKlass java类(非数组)普通的Java类在JVM中对应的是instanceKlass类的实例

              InstanceMirrorKlass

              用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类

              InstanceRefKlass

              用于表示java/lang/ref/Reference类的子类

             InstanceClassLoaderKlass

             用于遍历某个加载器加载的类

     ArrayKlass 动态数据类型,即是运行期生成的, Java数组的元信息用ArrayKlass的子类来表示

             TypeArrayKlass

             用于表示基本数据类型的数组 jvm中基本类型数组表现形式

             ObjArrayKlass

            用于表示引用类型的数组 jvm中引用类型数组表现形式

 

问题:类加载器将.class文件加载进系统,将.class文件解析,生成的是什么,类的元信息如何存储的?

答案:就是 InstanceKlass,元信息存储在InstanceKlass里

jvm中类结构验证

接下来我们需要用到HSDB。

如何使用HSDB?

进入到jdk的lib文件夹下 ,打开cmd,执行命令 java -cp sa-jdi.jar sun.jvm.hotspot.HSDB,会打开一个HSDB的图形化界面

跑一个不会停的项目,使用jps命令找出端口号,1272就是我现在跑的项目

JVM类结构及加载过程

打开HSDB ,file-》attach ,输入1272端口号确认。

报错:Exception in thread "Thread-1" java.lang.UnsatisfiedLinkError: Can't load library: C:\Program Files\Java\jre1.8.0_20\bin\sawindbg.dll

sawindbg.dll这个文件出错,去网上下载一个对应的版本,64位找64位的,32位找32位的。替换这个文件,重新运行HSDB,attach成功。

我这里保存了一份资源上传到csdn了,win10 64位:https://download.csdn.net/download/qq_39404258/13980337。

attach该进程端口号,然后tools-》class brower ,找到运行的类的内存地址

JVM类结构及加载过程

找到该地址后,然后tools-》inspector ,将内存地址放上去回车,就可以看到该类在jvm中就是一个instanceKlass。

而下面的 java_mirror就是java对象。

JVM类结构及加载过程

class和klass区别

class是java中的 (java代码)

klass是jvm中的 (c++代码)

接下里验证数组

比如我的demo是这样的:

public static void main(String[] args) {
   int[] arr1 = new int[1];
   Test1[] arr2 = new Test1[1];
}

使用HSDB查看main方法里面的堆栈

JVM类结构及加载过程

根据操作查看main的堆栈情况

JVM类结构及加载过程

我们看一下后两个内存地址是什么对象

第一条为TypeArrayKlass,即基本数据类型的数组

JVM类结构及加载过程

第二条为ObjArrayKlass,即引用类型数组

JVM类结构及加载过程

这也验证了jvm中数组的存在形式。

在idea中下载插件,jclasslib,可以看到字节码文件内容。

点击view-》show Bytecode with jclasslib查看字节码文件内容。

JVM类结构及加载过程

如我刚才的demo对应的字节码内容为:

JVM类结构及加载过程

通过查看jvm字节码手册可知,以上数组部分字节码的含义

JVM类结构及加载过程

而后面的数字10代表基本数据的类型

JVM类结构及加载过程

类加载过程

类加载过程分为这几部:

加载-》(验证-》准备-》解析)链接-》初始化-》使用-》卸载

加载阶段

加载:

1、通过类的全限定名获取存储该类的class文件

2、解析成运行时数据,即instanceKlass实例,存放在方法区

3、在堆区生成该类的Class对象,即instanceMirrorKlass实例

(通常来讲如果不使用,这个class只会在那,并没有被加载。而如果使用了肯定创建了实例)

从哪里获取

1、从压缩包中读取,如jar、war

2、从网络中获取,如Web Applet

3、动态生成,如动态代理、CGLIB

4、由其他文件生成,如JSP

5、从数据库读取

6、从加密文件中读取

何时加载

1、new、getstatic、putstatic、invokestatic

2、反射

3、初始化一个类的子类会去加载其父类

4、启动类(main函数所在类)

5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

当然也有一些例外:包装类、String、Thread属于预加载。

验证阶段

1、文件格式验证

2、元数据验证

3、字节码验证

4、符号引用验证

准备阶段

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

实例变量是在创建对象的时候完成赋值的,没有赋初值这一过程。

JVM类结构及加载过程

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

如何查看常量池

进入到classes目录下执行命令

javap -verbose 全限定名

如: javap -verbose com.chuan.Test1

可以看到constant pool 也是常说的class文件常量池,可以理解为class文件的资源仓库。常量池(类常量池)就是类在编译后的class文件中的一部分

JVM类结构及加载过程

从这里可以看到,final修饰的已经有值了,而其他的都是在之后才赋的值。

常量池主要存放两大类常量:字面量(文本字符串、声明为final的常量值等)和符号引用(有三类:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)

解析阶段

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

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

解析前常量池和解析后的常量池,一个是引用了一个字符串,一个是指向了内存地址

JVM类结构及加载过程

解析什么内容:

1、类或接口的解析

2、字段解析

3、方法解析

4、接口方法解析

何时解析:

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

2.在执行特定字节码之前进行解析。

初始化阶段

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

静态字段、静态代码段,字节码层面会生成clinit方法

方法中语句的先后顺序与代码的编写顺序相关

之前在常量池中可以看到,a最终是a:I保存了下来,只赋了初值,接下来在clinit方法里生成了字节码文件,进行初始化操作。

JVM类结构及加载过程

后面使用和销毁就不作分析了。

来几个栗子

demo1:

public class Test_1 {
    public static void main(String[] args) {
        System.out.printf(Test_1_B.str);
    }
}
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");
    }
}

结果:

A Static Block

A str

分析:

因为调用Test_1_B.str,其实还是用到了A的属性,和B没关系,所以不加载B,就不会执行B的静态代码块。

 

demo2:

public class Test_2 {
    public static void main(String[] args) {
        System.out.printf(Test_2_B.str);
    }
}
class Test_2_A {
    static {
        System.out.println("A Static Block");
    }
}
class Test_2_B extends Test_2_A {
    public static String str = "B str";
    static {
        System.out.println("B Static Block");
    }
}

结果:

A Static Block

B Static Block

B str

分析:

因为调用的是B里面的属性,但子类加载父类一定加载,所以A的静态代码块也会执行。

 

demo3:

public class Test_3 {

    public static void main(String[] args) {
        System.out.printf(Test_3_B.str);
    }
}

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

class Test_3_B extends Test_3_A {
    public static String str = "B str";
    static {
        System.out.println("B Static Block");
    }
}

结果:

A Static Block

B Static Block

B str

分析:子类有这个方法就调用子类自己的属性。

 

demo4:

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");
    }
}

结果:

A Str

分析:str属性被final修饰,str的值存在了常量池,所以无需这个对象,即不会执行静态代码块。

 

demo5:

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");
    }
}

结果:

Test_7_A Static Block

89ee5c04-8146-4be0-b51f-3901391a2019

分析:因为uuid这个值是动态的,不能存在常量池中,所以需要加载A

 

本文地址:https://blog.csdn.net/qq_39404258/article/details/111976908

相关标签: jvm 类加载过程