JVM学习笔记——类加载机制

1,072 阅读6分钟

简述

类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。

加载

①.通过一个类的全限定名来获取定义此类的二进制字节流
②.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
虚拟机设计团队将第一步操作放置Java虚拟机外部,以便让应用程序自己决定去获取所需要的类,实现这操作的代码模块称为类加载器(可以通过继承ClassLoader重写loadClass()方法),JVM提供了3中类加载器:

  • 启动类加载器(Bootstrap ClassLoader)
  • 主要加载核心类库,负责存放在JAVA_HOME\lib目录中的,或者-Xbootclasspath参数指定路径中的,并且被虚拟机识别(仅按照文件名识别,如rt.jar)的类库加载到虚拟机内存中。控制台输出下面代码,可以看到具体哪些类库。
    
    System.getProperty("sun.boot.class.path")
    

  • 扩展类加载器(Extension ClassLoader)
  • 负责加载JAVA_HOME\lib\ext目录中,或者被java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader)
  • 负责加载用户类路径(ClassPath)上所指定的类库

    应用程序都是由这3种类加载器互相配合进行加载,我们自己也可以定义类加载器。JVM使用双亲委派模型来组织类加载器之间关系(组合关系)

    双亲委派模型工作过程:如果一个类加载器收到类加载请求,它首先判断这个class是不是已经加载成功,如果没有委派父类加载器处理,逐层相同操作,最终加载请求都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载请求时,子加载器才会尝试自己去加载。

    双亲委派模型好处:因为不同类加载器加载同一个Class文件会是两个独立的类,所以双亲委派模型让Java类随着它的类加载器一起具备一种优先级的层次关系,例如类java.lang.Object,它存放在rt.jar包中,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类

    注:
    有关ClassLoader源码层次解析其加载

    验证

    为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,主要完成下面4个阶段:

  • 格式验证
  • 验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理,该验证阶段的主要目的是保证输入的字节流能正确地解析并存储到方法区中。只有通过此验证,字节流才会进入内存的方法区进行存储,后面3个阶段都基于方法区的存储结构进行,不直接操作字节流。

  • 元数据验证
  • 对元数据信息中的数据类型校验,以保证其描述的信息符合Java语言规范的要求。e.g.是否继承了final修饰的类等

  • 字节码验证
  • 对类的方法体进行校验,保证类的方法运行时不会做出危害虚拟机安全的事件。

  • 符号引用验证
  • 对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,为了确保解析动作能正常执行。e.g.通过字符串描述的全限定名是否能找到对应的类、是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

    准备

    正式为类变量分配内存并在方法区设置类变量初始值

    
        public static int value = 123;
    

    通常情况下变量value在准备阶段过后var初始值为0而不是123,初始值为数据类型的默认值。到了初始化阶段,value复制123会在类构造器clinit()方法执行。

    
        public static  final int value = 123;
    

    特殊情况在编译阶段会为value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将value赋值为100 (如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者java.lang.String,就会生成ConstantValue属性)。

    解析

    此阶段虚拟机将常量池内的符号引用替换为直接引用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。

    符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
    直接引用:直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄

    初始化

    到了初始化阶段,类中的Java程序代码才开始执行,是执行类构造器clinit方法的过程,clinit方法由类变量赋值动作静态语句块(static{})组成,顺序由语句在源文件出现的顺序决定。

    注:
    ①.clinit方法与init方法不同,它不需要显式地调用父类clinit方法,虚拟机会保证父类的clinit优先执行
    ②.clinit方法对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器可以不为这个类生成clinit方法
    ③.行接口的clinit方法不需要先执行父接口的clinit方法,只有使用了父接口中定义的变量时,父接口才会初始化。接口的实现类在初始化时也一样不会执行接口的clinit方法
    ④.如果多个线程同时去初始化一个类,只会有一个线程去执行这个类的clinit方法,其余线程阻塞等待,当执行clinit方法的那条线程退出clinit方法后,其余线程唤醒后也不会再进入clinit方法。同一个类加载器下,一个类型只会初始化一次

    不同类加载器初始化:

    
    public class ClassLoaderTest {
    
        static {
            System.out.println("clinit");
        }
    
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            ClassLoader myLoader = new ClassLoader() {
                @Override
                public Class loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if(is == null){
                            return super.loadClass(name);
                        }
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException();
                    }
                }
            };
            myLoader.loadClass("load.ClassLoaderTest").newInstance();
            //        Object obj =  Class.forName("load.ClassLoaderTest").newInstance();
        }
    
    }
    

    输出:
    clinit
    clinit

    感谢

    《深入理解Java虚拟机》