Java-第十四部分-JVM-类生命周期和类加载器

443 阅读10分钟

JVM全文

类的生命周期

概述

  • 引用数据类型需要进行类的加载
  • 基本数据类型虚拟机预先定义好的,不需要加载
  • 生命周期 image.png
  • 使用过程 image.png

加载

  • 将java类的字节码文件加载到机器内存中,在内存中构建java类的原型类模板对象

类模板,类在内存中的快照,将字节码解析出的常量池/类字段/类方法存储到类模板,通过反射进行获取

  • 查找并加载类的二进制数据,生成class实例
  1. 通过类的全名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区数据接口
  3. 创建java.lang.Class类的实例(堆中),作为方法区这个类的各种数据的访问入口

获取二进制流

  • 通过文件系统,读入class后缀的文件
  • 读入jar/zip等归档数据包,提取类文件
  • 事先存放在数据库的类的二进制数据
  • 类似http协议进行网络加载
  • 运行时生成的一段class二进制信息
  • 如果输入的数据不是classFile结构,抛出ClassFormatError

类模板和class实例

  • 类模板存储在方法区
  • class文件完全加载进元空间后,在堆中创建Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每一个类都有对应的Class类型的对象
  1. Class类用来表示类的类,记录类的成员、方法等信息,类的实际表征,指向方法区的类模板
  2. instanceKlass -> mirror: Class实例 image.png

数组类的加载

  • 数组类本身并不是由类加载器负责创建,而是JVM在运行时根据需要直接创建,但是数组中的元素仍然需要类加载器去创建
  1. 元素类型如果是引用类型,遵循定义的加载过程递归加载和创建数据
  2. JVM使用指定的元素类型和数组维度来创建新的数组
  • 数组类型是引用类型,数组的访问权限由元素类型的访问权限决定

链接

验证

  • 保证加载的字节码是否符合规范 image.png
  • 格式验证CAFEBABE会和加载阶段一起执行,验证通过后,才会将类的二进制数据加载进方法区
  • 格式验证之外的验证操作,都将在方法区进行
  • 语义检查,是否存在不兼容的方法
  • 字节码验证,函数传参是否正确,变量的赋值是否正确
  1. 栈映射帧,StackMapTable,用于检测在特定的字节码处,其局部变量表和操作数栈是否有正确的数据类型,仅用于有跳转的字节指令中
  2. 跳转语句有块的概念。块的起点是跳转字节指令的目的地址,每个基本块是以栈映射帧的形式存在的
  3. 字节码偏移量,和栈映射帧的偏移量不是一样的,字节码偏移量+1=帧偏移量 image.png image.png
  • 符号引用在解析的时候才会执行

准备

  • 非final修饰的基本数据类型,为类的静态变量分配内存,初始化为默认值 image.png
  • 这里不包括基本数据类型的字段用static final修饰的情况,final在编译的时候就会分配,在准备阶段显式赋值

如果使用static final修饰String,字面量的形式赋值,也是在准备环节直接显式赋值

  • 在准备环节不会像初始化阶段,会有初始化或者代码被执行

解析

  • 将类、接口、字段、方法的符号引用转换为直接引用(实际的地址),实际使用中需要知道这个方法在方法表中的偏移量,通过解析可以转变为目标方法在类中方法表的位置,从而被成功调用
  • 通常在初始化之后进行
  • 字符串常量池部分在常量池中的体现,带有utf8的为字符串常量池中的字符串,在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例
  • jvm内部运行的常量池,维护一张字符串拘留表(intern),保存出现过的字符串常量,没有重复项 image.png

初始化

  • 类装载的最后一个阶段,执行<clinit>()方法,由类静态成员变量的赋值语句和static语句块组成
  • 为类的静态变量赋于正确的初始值
  • 这个阶段,真正执行类中定义的java程序代码
  • 由父及子,静态先行,先加载父类,再加载子类
  • 不生成<clinit>()
  1. 只有实例变量,只有非静态字段
  2. 静态变量没有被显式赋值
  3. final static修饰的基本数据类型(普通赋值情况,直接赋常量)和String(字面量形式赋值),在准备环节被赋值
  • 字面量赋值形式的String外的引用类型,或者通过方法(new)的形式为数据进行赋值,在初始化阶段赋值 image.png
  • 使用final static修饰,且显式赋值中不涉及方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行的

clinit的线程安全

  • 虚拟机会确保多线程环境下的安全,保证一个类的clinit在多线程环境中被正确地加锁、同步
  • 带的是隐式的锁,如果一个类的clinit耗时很长,有可能造成多线程阻塞,死锁,但是很难发现
  • 如果之前的线程成功加载类,在队列中等待的线程没有机会再次执行clinit,使用这个类时直接返回已经准备好的信息

主动使用和被动使用

  • 使用链接过程准备好的资源,不会进行初始化
  • 主动使用会执行初始化阶段
  1. 创建一个类的实例,new关键字、反射、克隆、反序列化
  2. 调用类的静态方法,使用了字节码invokestatic指令
  3. 使用类、接口的静态字段,使用getstatic/pustatic指令;final static修饰的基本数据类型或字面量赋值的String不会主动调用初始化
  4. 使用java.lang.reflect包中反射类的方法,Class.forName("全类名"),主动加载类
  5. 初始化子类,发现父类还没有被初始化,需要先初始化父类,-XX:+TraceClassLoading追踪类加载信息;初始化类的时候,并不会先初始化所实现的接口
  6. 一个接口定义了default方法,直接实现或间接实现该接口的类的初始化
  7. 虚拟机启动,用户需要指定一个要执行的主类,包括main方法的那个类
  8. 初次调用MethodHandle实例(反射中),初始化该MethodHandle指向的方法所在的类
  • 被动使用不会引起类的初始化
  1. 当访问一个静态字段时,静态字段属于谁,初始化谁
  2. 通过数组定义类引用,不会触发
  3. 引用常量不会触发,常量在链接阶段显式赋值
  4. ClassLoader类loadClass()方法加载一个类,并不是类的主动使用,不会导致类的初始化

使用

  • 在加载并初始化成功之后,可以访问和调用类中的静态类成员变量,使用new关键字为其创建对象实例

类的卸载

  • 类加载器对象加载类,并用一个集合存放所加载类的引用;Class对象总是指向加载该类的加载器,调用getClassLoader()获取加载器
  • 类的实例引用这个类的Class对象,getClass()方法,返回Class对象的引用 image.png
  • 它的Class对象生命周期决定类的生命周期

回收

  • 方法区主要回收的部分 常量池中废弃的常量/不再使用
  • 类型回收条件
  1. 该类的所有实例被回收,不存在任何派生子类的实例
  2. 加载该类的类的回收器被回收
  3. 对应的Class对象在任何地方没有被引用 image.png

类的加载器

概述

  • classLoader负责将class信息的二进制流数据读入jvm,转换为一个与目标对应的Class对象实例,然后交给jvm虚拟机进行链接、初始化 image.png
  • 显式加载,通过调用ClassLoader加载class对象
  • 隐式加载,调用对象时会加载class对象
  • 命名空间
  1. 类的唯一性,类的加载器(实例)和类本身确定类的唯一
  2. 命名空间,由该类的加载器以及所有的父加载器所加载的类组成;同一个命名空间下,不会出现类完整名称相同的两个类
  • 加载机制的特征
  1. 双亲委派模型
  2. 可见性,子加载器加载的类可以访问父加载器加载的类型
  3. 单一性,父加载器的类型对于子加载器是可见的,父加载器加载过的类型就不会在子加载器中重复加载,但是同级加载器,可以重复加载同一类型多次

类加载器的分类

  • 主要分为引导类加载器Bootstrap ClassLoader(C++实现)和自定义类加载器User-Defined ClassLoader(Java实现) image.png
  • 下层加载器包含上层加载器的引用
  1. 引导类加载器 BootStrap ClassLoader,C/C++实现,嵌套在jvm内部,用来加载java的核心库,包名为java/javax/sun...;并不继承自ClassLoader;加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  2. 自定义类加载器 User-Defined ClassLoader,所有派生于抽象类ClassLoader的类加载器,java实现
  3. 扩展类加载器,Extension Classloader,父类为启动类加载器,从java.ext.dirs系统属性所指定的目录加载类库,或从JDK安装目录的jre/lib/ext子目录下加载类库,用户创建的jar放在此目录下,也会由扩展类加载器加载
  4. 应用程序类加载器,,父类加载器为扩展类加载器,AppClassLoader,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;程序中默认的类加载器
  • 获取加载器
//引导类 null
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
//当前线程获取上下文,系统类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
  • 自定义加载器,实现类库动态加载
  • 数据类型的加载,与元素类型有关,与元素类型的类加载器一致;基本数据类型不需要加载,虚拟机已经预先定义好
String[] arrStr = new String[10];
Person[] persons = new Person[10];
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(persons.getClass().getClassLoader());
//null
System.out.println(arrStr.getClass().getClassLoader());
  • 继承关系 image.png

源码

ClassLoader

  • 引导类加载器为空

Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.

  • 基本数据类型的加载器为空

If this object represents a primitive type or void, null is returned.

  • 数组

The class loader for an array class, as returned by {@link Class#getClassLoader()} is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

  • 双亲委派模型

When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself.

Launcher

  • 扩展类的父类加载器(上层类加载器)为null image.png image.png image.png
  • 创建系统类加载器,传入扩展类 image.png image.png
  • 设置当前线程上下文加载器 image.png

ClassLoader内部方法

  • 获取父类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader.getParent());
  • loadClass,内部实现逻辑为双亲委派原则
// resolve 如果为true,加载class的同时,进行解析操作
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) { //同步操纵只能加载一次
        // First, check if the class has already been loaded
        // 首先在缓存中判断是否加载同名的类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) { //当前类加载器的父类加载器
                    c = parent.loadClass(name, false); //先让父类尝试加载
                } else { //parent==null,则委托引导类加载器进行加载
                    c = findBootstrapClassOrNull(name); 
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) { //当前类的父类加载器未加载此类
                // If still not found, then invoke findClass in order
                // to find the class.
                //调用当前类的加载器的findClass方法加载类
                long t1 = System.nanoTime();
                c = findClass(name);
            
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) { //解析类
            resolveClass(c);
        }
        return c;
    }
}
  • findClass,在URLClassLoader中进行了重写,负责加载类成Class实例
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    //类的路径
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            //根据给的数据,返回对应类的Class实例,真正加载二进制流数据
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

SecureClassLoader与URLClassLoader

  • SecureClassLoader,对代码源的位置及其证书验证,权限定义类验证
  • URLClassLoader,真正加载class文件,将二进制流封装成Class实例

Class.forName()与ClassLoader.loadClass()

  • Class.forName(),静态方法,根据传入的类的全限定名返回Class对象,将Class加载到内存后,进行类的初始化
  • ClassLoader.loadClass(),实例方法,需要一个对象调用该方法,将class文件加载进内存时,不会执行类的初始化,直到这个类第一次被使用

双亲委派模型

  • jdk1.2开始
  • 一个类加载器接到加载器的请求时,首先不会尝试加载这个类,而是将这个类的加载任务委托给父类,当父类加载器无法完成加载任务时,自己去尝试加载

引导类先加载 -> 扩展类 -> 系统类 -> 自定义类 IMG_18ED51017539-1.jpeg

  • 优势
  1. 避免类的重复加载,确保类的全局唯一性,优先级的层次关系
  2. 保护程序安全,防止核心API被随意篡改
  • 代码体现在loadClass()
  1. 现在当前加载器的缓存中查找是否有此类,如果有,直接返回
  2. 判断当前加载器的父类加载器是否为空,不为空,调用父类的loadClass()进行加载;为空,调用findBootstrapClassOrNull(),用引导类加载器进行加载
  3. 如果都不行,则用当前类加载器进行加载,最终调用defineClass加载目标类
  • 核心类库保护机制,类加载最终都会调用defineClass(),进一步执行preDefineClass(),对核心类库进行了保护
  • 弊端
  1. 委托过程是单向的,顶层的ClassLoader无法访问底层的ClassLoader所加载的类
  2. 情景,系统类接口绑定一个工厂方法,用于创建该接口的实例,但是该接口的实现由应用类加载器进行加载,而接口和工厂方法都由启动类加载器加载,那么该工厂方法无法创建由应用类加载器加载的接口实例
  • JVM没有明确要求类的加载机制一定使用双亲委派机制

破坏双亲委派机制

  • 1.2之前没有双亲委派机制,1.2之后需要兼容已有的代码,引导用户编写的类加载逻辑时,重写findClass()方法
  • 模型的缺陷,基础类无法调用用户的代码,由应有类加载器进行加载
  1. SPI,Service Provider Interface,JNDI服务提供者接口,可由应用层自行实现的基础类中的接口,
  2. 当顶层加载器需要加载应用层的类,委托给线程上下文加载器去调用
  • 用户对于程序动态性的追求,每一个程序的模块都有自己的类加载器,当需要更换一个部分时,就直接换掉类加载器以及相关模块内容,以实现代码热替换,类的加载机制则为更复杂的网状结构
  1. 代码热替换、模块热部署,可以不用重启服务,就能进行代码修改
  2. 用两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为是两个不同的类 IMG_A15CD2F651DE-1.jpeg

沙箱安全机制

  • sandbox,限制程序运行的环境
  • 保证程序安全,保护java原生的jdk代码
  • 限定java代码在jvm特定的运行范围中,并且严格限制代码对本地系统资源的访问,CPU、内存、文件系统、网络
  1. 1.0 本地代码无条件授权;远程代码有限制
  2. 1.1 对受信任的远程代码,允许用户指定代码对本地资源的访问权限
  3. 1.2 代码签名,增加权限组,按照用户设定的权限进行访问
  4. 1.6 会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,应用域通过系统域的部分代理来对各种资源进行访问

自定义类加载器

  • 隔离加载类,实现不同中间件类的隔离

类仲裁机制,如果全限定类名一样,会导致类冲突,但是同一个框架中,不同的服务可能依赖同一个类的不同版本

  • 修改类的加载方式,在某个时间点按需加载
  • 扩展加载源,数据库、网络、电视机顶盒
  • 防止源码泄漏,可以在自定类加载器中进行编译加密
  • 不同类加载器加载同一个类,其实是两个命名空间下的,被认为是两个类

实现自定义类加载器

  • 重写findClass,保存loadClass中的双亲委派模型
  • 父类加载器为系统类加载器
public class MyClassLoader extends ClassLoader {
    //路径
    private String byteCodePath;
    public MyClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }
    public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.byteCodePath = byteCodePath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String filename = byteCodePath + name + ".class";
        //获取对应class文件的二进制数据流
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //获取一个输入流
            bis = new BufferedInputStream(new FileInputStream(filename));
            //获取一个输出流,输出内存中的byteArr的数组中
            baos = new ByteArrayOutputStream();
            //写数据
            int len;
            byte[] buff = new byte[1024];
            while ((len = bis.read(buff)) != -1) {
                baos.write(buff, 0,len);
            }
            //获取完整的字节数组数据
            byte[] bytes = baos.toByteArray();
            //将字节数组的数据转换为Class的实例
            Class<?> aClass = defineClass(null, bytes, 0, bytes.length);
            return aClass;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null) {
                    baos.close();
                }
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return null;
    }
}
  • 调用
public static void main(String[] args) {
    MyClassLoader loader = new MyClassLoader("/Users/apple/Desktop/java/");
    try {
        Class<?> aClass = loader.loadClass("ADDTest");
        //com.java.test.MyClassLoader
        System.out.println(aClass.getClassLoader().getClass().getName());
        //sun.misc.Launcher$AppClassLoader
        System.out.println(aClass.getClassLoader().getParent().getClass().getName());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

JDK9新特性

  • 模块化构建
  • 没有改变三层类加载器架构和双亲委派模型
//jdk.internal.loader.ClassLoaders$AppClassLoader@7ad041f3
System.out.println(ClassLoaderTest.class.getClassLoader());
//jdk.internal.loader.ClassLoaders$PlatformClassLoader@5acf9800
System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
//null
System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());
  • 扩展机制被移除,向后兼容性被保留,但是被重命名为平台类加载器
ClassLoader platformClassLoader = ClassLoader.getPlatformClassLoader();
System.out.println(platformClassLoader);
  • 平台类加载器和应用类加载器不在继承于URLClassLoader image.png
  • 启动类加载器,在jvm内部与java类库共同协作实现的类加载器,具体获取依然是null值
  • 获取加载器的名称
String name = platformClassLoader.getName();
System.out.println(name);
  • 双亲委派模型的变动,先判断该类是否能够归属到某一个系统模块中,如果可以,优先委派给负责那个模块的加载器完成加载 IMG_22D187925F5E-1.jpeg