• Java-类加载机制


      类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。【感觉和单例模式中的懒汉单例有异曲同工之妙】

    1,类的生命周期【7 个阶段=5+2】

    2,类加载过程【初始化之前的步骤】

      包含了加载、验证、准备、解析和初始化这 5 个阶段

    2.1 加载【读取二进制字节流】

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

    加载过程完成以下三件事:

      ①通过类的完全限定名称获取定义该类的二进制字节流

      ②将该字节流表示的静态存储结构转换为方法区的运行时存储结构

      ③在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口

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

      ①从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。

      ②从网络中获取,最典型的应用是 Applet。

      ③运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。

      ④由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

    2.2 验证【验证是否适用】

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

    2.3 准备【静态变量分配内存,static和static final】

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

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

      普通static变量和static final变量:

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

    public static int value = 123;

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

    public static final int value = 123;

    4. 解析【有些疑问?符号引用】

      解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
      符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
      直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

      虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

      解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

      将常量池的符号引用替换为直接引用的过程。【常量池为JVM方法栈中的内存空间,栈中还有局部变量表、操作数栈】

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

       

    参考链接:https://www.zhihu.com/question/30300585?sort=created

    5. 初始化

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

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

    package com.cnblogs.mufasa;
    
    public class demo2_5 {
        static class Parent {
            public static int A = 1;
            static {
                A = 2;
            }
        }
    
        static class Sub extends Parent {
            public static int B = A;
        }
    
        static {
            int i=0;
    //        i=0;//非法向前引用 Illegal forward reference
            System.out.println(i);
        }
    
        public static void main(String[] args) {
            System.out.println(Sub.B);  // 2
            System.out.println(Sub.B);  // 2
        }
    }
    View Code
    0
    2
    2

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

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

    3,类初始化时机

    3.1 主动引用

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

    • 遇到 newgetstaticputstaticinvokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:①使用 new 关键字实例化对象的时候;②③读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;④以及调用一个类的静态方法的时候。

    • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化【反射实例化】

    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化【先后问题】

    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类【Java程序的开端】

    • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

    package com.cnblogs.mufasa.demo3_1;
    
    
    import java.lang.invoke.MethodHandles;
    import java.lang.invoke.MethodType;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import  java.lang.invoke.MethodHandle;
    import java.util.List;
    
    class test_reflect{
        static {
            System.out.println("类加载初始化");
        }
        public static String  printOut(String str){
            System.out.println("静态方法调用"+str);
            return str+"123";
        }
    }
    
    class child extends test_reflect{
        static {
            System.out.println("子类静态代码块");
        }
        public static void printOut(){
            System.out.println("子类静态方法调用");
        }
    }
    
    //class test_MethodHandles{
    //
    //}
    
    
    public class Client2 {
        //4,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
    //    static {
    //        System.out.println("虚拟机初始化主类");
    //    }
    
        public static void main(String[] args) throws Throwable {
            //2,使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
    //        Class clazz=test_reflect.class;
    //        Method[] methods=clazz.getDeclaredMethods();
    //        for(Method method:methods){
    //            method.invoke(clazz);
    //        }
            /**
             * 输出:
             * 类加载初始化
             * 静态方法调用
             */
    
            //3,当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
    //        Class clazz=child.class;
    //        Method[] methods=clazz.getDeclaredMethods();
    //        for(Method method:methods){
    //            method.invoke(clazz);
    //        }
    
            //5,当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle
            // 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,
            // 并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;【MethodHandle需要继续完成!】
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MethodType mt = MethodType.methodType(String.class,char.class,char.class);
            try {
                MethodHandle mh = lookup.findVirtual(String.class,"replace", mt);
                String handled_str = (String) mh.invoke("abc",'a','c');
                System.out.print(handled_str);
            } catch (NoSuchMethodException | IllegalAccessException e) {
                e.printStackTrace();
            }
    
        }
    }

    3.2 被动引用

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

    • 通过子类引用父类的静态字段,不会导致子类初始化。验证成功【调用父类静态变量,子类不初始化;调用子类静态变量,父类初始化,并且是先父类后子类初始化】
    System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
    • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
    SuperClass[] sca = new SuperClass[10];
    • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
    System.out.println(ConstClass.HELLOWORLD);
    package com.cnblogs.mufasa.demo3_2;
    
    class SuperClass{
        static int value=10;
        static final int NUM=20;
        static {
            System.out.println("父类加载初始化-完成");
        }
    }
    
    class SubClass extends SuperClass{
    //    static int value=11;
    //    static final int NUM=22;
        static {
            System.out.println("子类加载初始化-完成");
        }
    
    }
    
    public class Client {
        public static void main(String[] args) {
            //1,通过子类引用父类的静态字段,不会导致子类初始化.【验证成功】-【static变量分配方法区中的静态池中,并且赋值】
    //        System.out.println(SubClass.value);
    
            //2,通过数组定义来引用类,不会触发此类的初始化。【验证成功】-【只是分配了这种类型的数组空间,并没有真正开始使用】
            // 该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法
    //        SuperClass[] sca = new SuperClass[10];
    
            //3,常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
            //【验证成功】-静态常量数据在类加载的准备阶段就直接在方法区中分配空间并初始化了,static的修饰遍历只是先分配空间
    //        System.out.println(SuperClass.NUM);
        }
    }

    4,类与类加载器

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

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

    5,类加载器分类

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

    • 启动类加载器Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分

    • 所有其它类的加载器,使用 Java 实现独立于虚拟机,继承自抽象类 java.lang.ClassLoader

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

    • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。

    • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

    • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    6,双亲委派模型

    应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。

    下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。

    6.1. 工作过程

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

    6.2. 好处

      使得 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。

    6.3. 实现

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

    7,自定义类加载器实现

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

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

    【本质:读取.class字节流--通过defineClass--类的实例】

    package com.cnblogs.mufasa.demo7;
    
    import java.io.*;
    
    class FileSystemClassLoader extends ClassLoader{
        private String rootDir;
        public FileSystemClassLoader(String rootDir){
            this.rootDir=rootDir;
        }
        protected Class<?> findClass(String name) throws ClassNotFoundException{
            byte[] classData= new byte[0];
            try {
                classData = getClassData(name);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if(classData==null){
                throw new ClassNotFoundException();
            }else {
                return defineClass(name,classData,0,classData.length);
            }
        }
    
        private byte[] getClassData(String className) throws IOException {
            String path=classNameToPath(className);
            System.out.println(path);
            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){
    //        System.out.println(rootDir+File.separatorChar+className.replace('.',File.separatorChar)+".class");
            return rootDir+File.separatorChar+className.replace('.',File.separatorChar)+".class";
        }
    }
    
    public class Client {
        public static void main(String[] args) throws ClassNotFoundException {
            FileSystemClassLoader fsc=new FileSystemClassLoader("E:\data\personal\博客园\2019.09.05Java类加载机制\src\com\cnblogs\mufasa\test");
            Class preClass=fsc.findClass("Test_classLoader");
            System.out.println(preClass.getClass().getName());
        }
    }
    View Code

    8,类的使用

      通过new或者反射获取新的实例化对象的过程【】,【注意:原型模式生成的实例化对象,是已有实例化对象的clone结果,并没有通过类的加载来实现】

    9,类的卸载

    9.1 所处空间地址

      方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

      和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

      对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

      HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

      方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

    9.2 

      由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。

      前面介绍过,Java虚拟机自带的类加载器包括启动类加载器、扩展类加载器和应用程序类加载器

      Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

      由用户自定义的类加载器加载的类是可以被卸载的。

      loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。

       如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

      当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)。

      (1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范);

      (2) 被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则);

      (3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)

    综合以上三点, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能。

    10,总结

    1. 类加载器和单例模式有点相似;
    2. 类加载器的流程:①加载;②验证;③准备;④解析;⑤初始化;⑥使用;⑦卸载;
    3. 主动加载【使用】的五种模式,被动加载【使用】的3中模式;
    4. 类加载的原理:.class文件【转换】二进制字节流【转换】类对象;
    5. 主要有三类类加载器:①启动类加载器<JRE_HOME>/lib;②扩展类加载器<JRE_HOME>/lib/ext;③应用程序类加载器classPath;
  • 相关阅读:
    oracle中获取当前整点和上一个小时整点,日期类型
    MYSQL中替换oracle中runum用法
    oracle 中备份表
    發生了不愉快的事情
    今年下雪了。。。
    VB.net下非常好用的实现FTP的类
    今年過節不回家了
    焕肤:不要暗沉
    不要打梦到的电话号码。。。
    關於IT職業的思考
  • 原文地址:https://www.cnblogs.com/Mufasa/p/11460257.html
Copyright © 2020-2023  润新知