JVM底层之类加载器

前言

这篇博客记录下JVM底层的类加载器,关于类加载器网络上有不少博客写到,我这边只是根据自己的理解记录下jvm类加载器的过程,可能不会太详细,只是用于自己平时偶尔翻翻回忆下自己的曾经所获所得。

JVM中的Klass模型

java中的每一个类在JVM中都是以Klass形式存在的,其中包含了元数据信息,包含属性信息、方法信息以及常量池。
Klass的模型结构如下:
在这里插入图片描述
最顶层的父类是MeaSpaceObj,然后元信息类Metadata是子类,而我们的Klass是是MetaspaceObj的子类,上图所描述的类继承关系指的都是C++对实现了JVM的类结构图,其中Klass有两个实现类InstanceKlass和ArrayKlass,
InstanceKlass有三个子类,分别是InstanceMirrorKlass、InstanceRefKlass、InstanceClassLoaderKlass

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

Java中的数组不是静态数据类型,是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:
1.TypeArrayKlass:用于表示基本类型的数组
2.ObjArrayKlass:用于表示引用类型的数组
比如我们在代码中这样写:

public static void main(String[] args) {
    
    
    int [] a = new int[2];
    Test [] tests = new Test[2];
    System.out.println(a);

    try {
    
    
        System.in.read();
    } catch (IOException e) {
    
    
        e.printStackTrace();
    }
}

其中a和tests都是一个数组,而a是基本类型数组,tests是引用类型数组
在java的字节码中a是newarray类型,tests是anewarray类型,我们看下字节码:

public static main([Ljava/lang/String;)V
    TRYCATCHBLOCK L0 L1 L2 java/io/IOException
   L3
    LINENUMBER 9 L3
    ICONST_2
    NEWARRAY T_INT
    ASTORE 1
   L4
    LINENUMBER 10 L4
    ICONST_2
    ANEWARRAY com/luban/Test
    ASTORE 2
   L5

以上的NEWARRAY T_INT就是a
NEWARRAY T_INT就是tests
我们再来看在jvm中以C++形式存在的类型
我们打开cmd执行java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
打开HSDB jvm调试工具:

在这里插入图片描述

我们启动我们刚刚写的代码找到进程号7172:
在这里插入图片描述

查看我们的main进程的内存堆栈信息:
在这里插入图片描述
最后我们找到PSYounGen[I后面的地址就是我们的int数组
我们拿到INT数组的内存地址*(后面的0x开头的),打开HSDB菜单的Tools下Inspector菜单
然后输入我们的int数组的内存地址:

在这里插入图片描述

可以看到我们的int数组加载到JVM过后,C++对它的处理是类型是TypeArrayKlass,而TypeArrayKlass是Klass的子类,我们从上图也可以看出他们的继承关系;所以TypeArrayKlass其实就是C++用来描述我们JAVA中的基本类型数组的。
同理,我们根据ObjArray找到我们的引用类型的数组:
在这里插入图片描述

可以看到ObjArrayKlass就是用来描述我们java类中引用类型数组
这个就是我们的引用数组tests是ObjArrayKlass
同样的ObjArrayKlass也是Klass的子类

所以我们的int数组在jvm中的存在类型是TypeArrayKlass
引用数组在我们的jvm中存在类型是ObjArrayKlass

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

何时加载:
主动使用时

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

预加载:包装类、String、Thread

故:
当我们定义了一个类,在本来中有静态 属性和静态方法,就算这个类有静态代码块,那么我们如果通过类.静态属性或者类.静态方法,如果这些静态的属性或者方法不在本来,是在父类,那么初始化的工作是不会进行的,看下面例子:

public class T0804 {
    
    

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

}


class T0804_01 extends T0804_p{
    
    


    static {
    
    
        str +="#456";
    }

}

class T0804_p{
    
    
    public static String  str = "123";
}

最后输出的是123而不是123#456
这个例子就是getstatic
但是如果我们在T0804_p有静态代码块,也会执行静态代码块,而且顺序是先执行,如:

class T0804_p{
    
    
    public static String  str = "123";
    static {
    
    
        System.out.println("T0804_p init");
    }
}

最后输出
T0804_p init
123
我们再修改下代码如下:

public class T0804 {
    
    

    public static void main(String[] args) {
    
    
        new T0804_01();
        System.out.println(T0804_01.str);
    }

}


class T0804_01 extends T0804_p{
    
    

    public T0804_01(){
    
    
        System.out.println("T0804_01 ..");
    }

    static {
    
    
        System.out.println("T0804_01 init...");
        str +="#456";
    }

}

class T0804_p{
    
    
    public static String  str = "123";
    static {
    
    
        System.out.println("T0804_p init");
    }
    public T0804_p(){
    
    
        System.out.println("T0804_p ...");
    }
}

最后输出:
T0804_p init
T0804_01 init…
T0804_p …
T0804_01 …
123#456
所以当通过new初始化一个类的时候
1.先初始化父类的静态代码块;
2.再初始化本类的静态代码块;
3.然后又初始化父类的构造;
4.最后初始化自己的构造;
而调用static的时候就要判断你调用的这个类中的某个属性或者方法是否在本类,如果在父类,那么只会初始化父类的静态代码块,不会初始化本类的静态代码块,比如:

public class T0804 {
    
    

    public static void main(String[] args) {
    
    
        System.out.println(T0804_01.getstr());
    }

}


class T0804_01 extends T0804_p{
    
    

    public T0804_01(){
    
    
        System.out.println("T0804_01 ..");
    }

    static {
    
    
        System.out.println("T0804_01 init...");
        str +="#456";
    }

}

class T0804_p{
    
    
    public static String  str = "123";
    static {
    
    
        System.out.println("T0804_p init");
        str="1111";
    }

我们通过T0804_01.getstr()调用静态犯法getstr(),而getStr这个方法没有在T0804_01中,而是在它的父类T0804_p中,所以调用完成过后,不会执行T0804_01中的静态代码块,只会执行父类的静态代码块,最后输出如下:
T0804_p init
1111

验证

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

准备

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

实例变量是在创建对象的时候完成赋值的,没有赋初值一说
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步

初始化

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

静态字段、静态代码段,字节码层面会生成clinit方法
方法中语句的先后顺序与代码的编写顺序相关

何时解析

思路:
1、加载阶段解析常量池时
2、用的时候

还有一个比较经典的知识点就是我们我们要使用一个类的时候,什么情况下这个类加载了要执行静态代码块,那些情况不执行,我们都知道我们手动加载一个类的时候常用的有两种,
用Class.forName和ClassLoader中的loadClass

1. Class<?> clzz = T0804.class.getClassLoader().loadClass("com.luban.t0806.T0804_p");

   2.Class<?> clzz2 = Class.forName("com.luban.t0806.T0804_p");

最后只有2能初始化T0804_p中静态代码块

所以综上所述:通过Class.forName会加载class的时候是会执行我们的静态初始化操作;
而使用classloader的loadClass仅仅是给我们加载了我们所要求的class,不会执行任何初始化方法

下一篇我们来扯一下类加载系统和SPI机制(文采不行_

猜你喜欢

转载自blog.csdn.net/scjava/article/details/108249127
今日推荐