美文网首页
JVM(五)类加载

JVM(五)类加载

作者: NIIIICO | 来源:发表于2022-04-20 16:20 被阅读0次

    一、类文件

    1、结构:

    ClassFile {
        u4 magic;// 魔数,用于确定文件类型
        u2 minor_version;// 副版本号
        u2 major_version;// 主版本号,代表JDK版本,如:0034,十进制为52,表示JDK8
        u2 constant_pool_count;// 常量池数量
        cp_info constant_pool[ constant_pool_count - 1];// 常量池
        u2 access_flags;// 标记,识别类是class还是接口;是否是public;是否是abstract;是否是final等
        u2 this_class;// 当前类
        u2 super_class;// 父类
        u2 interfaces_count;// 接口数量
        u2 interfaces[ interfaces_count];// 接口
        u2 fields_count;// 字段数量
        field_info fields[ fields_count];// 字段
        u2 methods_count;// 方法数量
        method_info methods[ methods_count];// 方法
        u2 attributes_count;// 属性数量
        attribute_info attributes[ attributes_count];// 属性
    }
    

    文件通过二进制存储,以8个字节为一组,下图为16进制视图下类文件的内容:

    class文件内容

    具体内容及常量池结构等,可以参考Java Language and Virtual Machine Specifications

    2、生命周期

    类从被加载到虚拟机内存到从内存卸载,包含以下几个阶段:


    类的生命周期
    (1)加载
    • 通过全类名获取类的二进制数据流。
    • 将类的二进制数据解析为方法区内的数据结构Klass。
    • 堆区创建java.lang.Class类的实例。
    • 数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。如果数组元素是引用类型,则会先加载该引用类型,再创建数组。
    类信息在jvm中的存储方式
    (2)验证
    • 为确保class信息符合当前虚拟机的要求,做以下验证:格式检查、语义检查、字节码验证、符号引用验证
    验证
    (3)准备
    • 为类变量(static)分配内存,并初始化为默认值。
    • static final修饰的属性为常量,在常量池初始化的时候确认内存。
    (4)解析
    • 把类、接口、字段、方法的符号引用替换成直接引用。
    • class文件中不会保存各个方法和字段最终内存布局信息,这些符号引用不经过转换就无法直接被虚拟机使用。
    • 符号引用以一组符号来描述所引用的目标,只要使用时能无歧义地定位到目标即可。
    • 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
    (5)初始化
    • 为类变量赋予正确的初始化值
    • 初始化阶段会执行<clinit>(),该方法是javac编译器收集类变量的赋值动作和静态代码快中的语句合并而来的。
    (6)使用
    • 经历过上述阶段,类就可以使用了。
    • 可以在程序中访问和调用它的静态类成员信息,或者通过new创建对象实例。
    (7)卸载
    • 常量回收:常量没有被任何地方引用的时候,则被标记为废弃常量。
    • 类回收:Java堆中不存在该类的任何实例对象;加载该类的类加载器已经被回收;该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法;则类可以被回收。
    引用关系

    二、类加载器

    负责读取指定目录下的class字节码文件,并进行解析,然后转换成方法区的响应结构。

    1、分类

    类加载器分为两类:启动(引导)类加载器(Bootstrap ClassLoader)、自定义类加载器(User-Defined ClassLoader)。Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

    注意:父加载器和继承没有关系,是类加载器中的parent字段。

    (1)启动(引导)类加载器 BootstrapClassLoader
    • 嵌在JVM内核中,用C/C++语言编写;
    • 加载路径:JAVA_HOME/lib/;
    • 不继承自ClassLoader,没有父加载器。
    (2)自定义类加载器 User-DefinedClassLoader

    <2.1>扩展类加载器 ExtClassLoader

    • Java编写,负责加载Java的扩展类库;
    • 加载路径:JAVA_HOME/jre/lib/ext/。
    • 派生自ClassLoader,父加载器为BootstrapClassLoader;
    • 继承关系:ExtClassLoader->URLClassLoader->SecureClassLoader->ClassLoader

    <2.2>应用程序类加载器 AppClassLoader

    • 加载路径:CLASSPATH下我们自己写的类。
    • 派生自ClassLoader,父加载器为ExtClassLoader;
    • 继承关系:AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader

    <2.3>自定义类加载器

    • 用户自己定义的类加载器

    2、双亲委派

    双亲委派
    • 类加载器收到了类加载请求,并不会自己先去加载,而是把这个请求委托给父加载器去执行;
    • 如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器;
    • 如果父加载器可以完成类加载任务,就成功返回;若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。

    3、android的类加载器

    (1)BootClassLoader
    • ClassLoader的子类
    • 加载FrameWork层的代码
    • 没有父加载器
    (2)BaseDexClassLoader
    • ClassLoader的子类
    • 实现了ClassLoader的大部分功能
    (3)DexClassLoader
    • BaseDexClassLoader的子类
    • 加载/data/app目录下的dex文件
    (4)PathClassLoader
    • BaseDexClassLoader的子类
    • 加载指定目录的dex文件、包含dex的apk文件、jar文件等。
    (5)类加载流程

    由于DexClassLoader、PathClassLoader继承自BaseDexClassLoader,而BaseDexClassLoader继承自ClassLoader,当我们调用loadClass时,会调用到如下代码:

    public abstract class ClassLoader {
        static private class SystemClassLoader {
            public static ClassLoader loader = ClassLoader.createSystemClassLoader();
        }
    
        private static ClassLoader createSystemClassLoader() {
            String classPath = System.getProperty("java.class.path", ".");
            String librarySearchPath = System.getProperty("java.library.path", "");
            // 创建PathClassLoader
            return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
        }
    
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
                // First, check if the class has already been loaded
                // 首先,检查是否已经加载过
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        // 然后,如果有父加载器,调用父加载器的loadClass
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            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加载
                        c = findClass(name);
                    }
                }
                return c;
        }
    }
    
    • 先查询自己是否加载过
    • 如果有父加载器,通过父加载器加载
    • 如果父加载器没找到,通过findClass加载

    上述代码可以看到,创建PathClassLoader传入的parent是BootClassLoader 。

    class BootClassLoader extends ClassLoader {
        protected Class<?> loadClass(String className, boolean resolve)
               throws ClassNotFoundException {
            Class<?> clazz = findLoadedClass(className);
    
            if (clazz == null) {
                clazz = findClass(className);
            }
    
            return clazz;
        }
    
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            return Class.classForName(name, false, null);
        }
    }
    

    如果BootClassLoader没加载到class,则会回到PathClassLoader的代码中执行,执行到它的findClass方法。在findClass中,会通过DexPathList查找class。如果最终未找到,会报ClassNotFoundException异常。

    public class BaseDexClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            // 通过pathList寻找class
            Class c = pathList.findClass(name, suppressedExceptions);
            if (c == null) {
                ClassNotFoundException cnfe = new ClassNotFoundException(
                        "Didn't find class \"" + name + "\" on path: " + pathList);
                for (Throwable t : suppressedExceptions) {
                    cnfe.addSuppressed(t);
                }
                throw cnfe;
            }
            return c;
        }
    }
    
    
    final class DexPathList {
        private Element[] dexElements;
    
        public Class<?> findClass(String name, List<Throwable> suppressed) {
            // 遍历element,寻找class
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    }
    

    4、热修复原理

    上文已经知道Android的类加载流程,可以看到会通过BaseDexClassLoader.pathList来寻找class,而DexPathList又是通过字段dexElements进行查找,热修复就是通过反射拿到DexPathList,生成新的Element数据,将他的dexElements替换掉从而达成修复的目的。

    相关文章

      网友评论

          本文标题:JVM(五)类加载

          本文链接:https://www.haomeiwen.com/subject/vqmkjrtx.html