zoukankan      html  css  js  c++  java
  • 剖析 JVM 相关知识点(下)——类的加载过程

    类加载的过程

    类加载过程:加载->连接->初始化。连接过程又可分为:验证->准备->解析

    1. 加载

    加载是类加载的一个阶段,注意不要混淆!

    加载主要完成了以下 3 件事情:

    • 通过一个类名的全限定名来获取此类的二进制字节流;
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

    注:全限定名是:Java 类包的定名,从最原始最上层的地方援引到具体的对象,这就是全限定名了。

    其中二进制字节流可以从以下方式中获取:

    • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
    • 从网络中获取,最典型的应用是 Applet。
    • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
    • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

    加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

    2. 验证

    确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    3. 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将放在方法区中进行分配。

    类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存

    实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

    初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

    public static int value = 123;
    

    如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

    public static final int value = 123;
    

    4. 解析

    将常量池的符号引用替换为直接引用的过程。

    • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关。Class 文件中的常量池中包括字面量符号引用(字段的名称和描述符、方法的名称和描述符、类和接口的全限定名),在 Class 文件中不会保存各个方法字段的最终布局信息,因此这些符号引用不经过转化是无法得到真正的内存入口地址的
    • 直接引用(Direct References):符号引用与虚拟机实现的内存布局有关。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
      * 补充:在对象访问定位中有两种主流的访问方式:①句柄;②直接指针。(P48)
      * 句柄:使用句柄访问的话,Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
      * 直接指针:直接指针的话,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。

    其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

    5. 初始化

    初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

    <clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

    public class Test {
        static {
            i = 0;                // 给变量赋值可以正常编译通过
            System.out.print(i);  // 这句编译器会提示“非法向前引用”
        }
        static int i = 1;
    }
    

    由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }
    
    static class Sub extends Parent {
        public static int B = A;
    }
    
    public static void main(String[] args) {
         System.out.println(Sub.B);  // 2
    }
    

    接口中不可以使用静态语句块,但仍然可以有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。

    虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

    类初始化时机

    1. 主动引用

    虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

    • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
    • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
    • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    2. 被动引用

    以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

    • 通过子类引用父类的静态字段,不会导致子类初始化。
    System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
    
    • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
    SuperClass[] sca = new SuperClass[10];
    
    • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
    System.out.println(ConstClass.HELLOWORLD);
    

    类与类加载器

    两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

    这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

    类加载器分类

    从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

    • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
    • 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。

    从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

    • 启动类加载器 (Bootstrap ClassLoader):此类加载器负责将存放在 <JRE_HOME>lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
    • 扩展类加载器(Extension ClassLoader):这个类加载器是由 sun.misc.Launcher$ExtClassLoader实现的。它负责将 <JAVA_HOME>/lib/ext 或者被java.ext.dir系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
    • 应用程序类加载器(Application ClassLoader):这个类加载器是由 sun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    双亲委派模型

    每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

    1. 工作过程

    一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

    通过下面的程序来验证:

    public class ClassLoaderDemo {
        public static void main(String[] args) {
            System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
            System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
            System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
        }
    }
    

    输出结果:

    ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
    The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
    The GrandParent of ClassLodarDemo's ClassLoader is null
    

    AppClassLoader的父类加载器为ExtClassLoader
    ExtClassLoader的父类加载器为null,**null并不代表ExtClassLoader没有父类加载器,而是 Bootstrap ClassLoader **。

    2. 好处

    1) 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

    例如java.lang.Object存放在 rt.jar 中,如果编写另外一个java.lang.Object并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

    2) 双亲委派模型可以保证 Java 程序运行的稳定性,但是实现简单

    3. 实现

    以下是抽象类java.lang.ClassLoader的代码片段,其中的loadClass()方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException,此时尝试自己去加载。

    public abstract class ClassLoader {
        // The parent class loader for delegation
        private final ClassLoader parent;
    
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // 首先,检查请求的类是否已经被加载过
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                            c = parent.loadClass(name, false);
                        } else { //父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        //抛出异常说明父类加载器无法完成加载请求
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        c = findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
        
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
        }
    }
    

    3. 自定义实现类加载器

    以下代码中的FileSystemClassLoader是自定义类加载器,继承自java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。

    java.lang.ClassLoaderloadClass()实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。

    public class FileSystemClassLoader extends ClassLoader {
    
        private String rootDir;
    
        public FileSystemClassLoader(String rootDir) {
            this.rootDir = rootDir;
        }
    
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classData = getClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, classData, 0, classData.length);
            }
        }
    
        private byte[] getClassData(String className) {
            String path = classNameToPath(className);
            try {
                InputStream ins = new FileInputStream(path);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int bufferSize = 4096;
                byte[] buffer = new byte[bufferSize];
                int bytesNumRead;
                while ((bytesNumRead = ins.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesNumRead);
                }
                return baos.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private String classNameToPath(String className) {
            return rootDir + File.separatorChar
                    + className.replace('.', File.separatorChar) + ".class";
        }
    }
    
    作者:意无尽 公众号:意无尽 关于作者:本人目前传统专业,现自学 Java,后续会有向大数据方向转型。希望自己能一步一个脚印的走下去,以此博客来见证我技术的成长轨迹!
  • 相关阅读:
    java ssh整合报错:java.lang.NoSuchMethodError: antlr.collections.AST.getLine()I
    Ubuntu16.04安装搜狗输入法后有黑边问题的解决方法
    socket
    vim编辑器的使用
    linux用户和群组
    bash shell
    [LightOJ 1128]Greatest Parent
    [Luogu P4180][BJWC 2010]严格次小生成树
    函数、方法区别
    有关_meta内容(持续更新)
  • 原文地址:https://www.cnblogs.com/reformdai/p/11199743.html
Copyright © 2011-2022 走看看