类加载机制

1. 类加载的时机

类从被虚拟机加载到内存中,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段。其中连接=验证+准备+解析。

1.1 何时开始加载?

Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机得到具体实现来自由把握。但是对于初始化阶段,虚拟机规范则严格规定了有且只有5种情况必须立即对类进行“初始化“。

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

接口和类的初始化不同:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化的时候,并不要求其父接口全部都完成了初始化,只有在真正使用到了父接口的时候(如引用接口中的常量)才会初始化。

2. 类加载

2.1 加载

加载是类加载过程的一个阶段,在这个阶段,虚拟机需要完成三件事:

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

加载阶段的加载既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存区域实例化一个java.lang.Class类的对象(并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区中),这个对象将作为程序访问方法区中的这些类型的外部接口。

2.2 验证

验证是连接的第一步,验证主要的目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证包含四个检验动作:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些对象所使用的内存都将在方法区中进行分配。注意此时进行内存分配的对象仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中。这里的初始值一般是0值。赋值动作是发生在准备阶段之后,在初始化阶段完成。

2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是他们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中了。

解析也有四种类型的解析:

  1. 类或接口解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

2.5 初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程,除了加载阶段用户程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java代码(或者说是字节码)。

3. 类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

3.1 类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,前提是这两个类是由同一个类加载器加载的前提下才有意义。否则即使这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。

3.2 双亲委派模型

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。

绝大部分Java程序都会使用到以下3种系统提供的类加载器:

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

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要还可以加入自己定义的类加载器。

类加载器的双亲委派模型并不是一个请执行的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到对应的类)时,子加载器才会尝试自己去加载。

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法中。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    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.
                    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;
        }
    }

3.3 破坏双亲委派模型

在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3次较大规模的“被破坏”的情况。

双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前jdk1.2之前。而类加载器和抽象类java.lang.ClassLoader则在jdk1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,jdk1.2以后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前用户去继承ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在类加载的时候会调用加载器的私有方法loadClassInternal,而这个方法的唯一逻辑就是去调用自己的loadClass().

双亲委派模型的第二次“被破坏”是由于这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础之所以称之为基础,是因为他们总是被作为被用户代码调用的API。(如果基础类又要调回用户的代码,此时可以使用JNDI,这个是java的命名和目录服务,它的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商 实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface))的代码。但启动类加载器无法识别这些代码。于是:

Java设计团队引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的, 动态性是指的:代码热替换、模块热部署(Hot Deployment)。(OSGI实现,Open Service Gateway Initiative)开发服务网关协议。

/**
 * @author caoduanxi
 * @Date 2020/1/16 15:29
 * 这里的ClassLoader重写了loadClass方法,破坏了双亲委派机制
 * 因为在双亲委派机制中,是需要父类执行loadClass方法来加载类的,即使父类没有能力加载
 * 会继续寻找启动类加载器继续执行加载,最后实在找不到就调用findClass方法
 * <p>
 * 而直接重写loadClass的话就破坏了,它没有机会向父类去提交加载的请求。所以破坏了双亲委派机制
 * 为什么不同类加载器加载出来的类不同,因为不同类加载器之后有相应的隔离机制
 */
public class ClassLoaderTest {
    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) {
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Class<?> aClass = myLoader.loadClass("com.duanxi.jvm.classloading.ClassLoaderTest");
        Object obj = aClass.newInstance();
        System.out.println(obj.getClass());
        System.out.println(aClass.getClassLoader());
        // 虽然获取的都是ClassLoaderTest这个类,但是由于使用的类加载器不同,所以instanceof判断会false
        System.out.println(obj instanceof com.duanxi.jvm.classloading.ClassLoaderTest);

    }
}
/**
 * @author caoduanxi
 * @Date 2020/1/16 15:59
 * 重写的是findClass()方法
 * 因为双亲委派机制的存在,一般一个类被加载进来之后调用loadClass()方法
 * loadClass()中需要执行判断当前类是否被加载,被加载的话直接返回
 * 如果没有被加载就向上找寻自己的父类加载器(注意父类加载器也会向上寻找父类加载器)
 * 如果还是加载失败的话,就直接找启动类加载器Bootstrap ClassLoader,如果还是不能加载的话
 * 此时只能调用自身的findClass()看看是否能够加载。
 * 重写findClass()方法的好处在于可以保证新写出来的类加载器是符合双亲委派机制的。
 */
public class ClassLoaderTestTwo {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.findClass(name);
                    }
                    byte[] b = new byte[is.available()];
//                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Class<?> aClass = myLoader.loadClass("com.duanxi.jvm.classloading.ClassLoaderTestTwo");
        Object obj = aClass.newInstance();
        System.out.println(obj.getClass());
        System.out.println(aClass.getClassLoader());
        // 虽然获取的都是ClassLoaderTest这个类,但是由于使用的类加载器不同,所以instanceof判断会false
        System.out.println(obj instanceof com.duanxi.jvm.classloading.ClassLoaderTestTwo);
    }
}

4. 小结

主要是关于类加载的五个阶段,加载,验证,准备,解析,初始化的阶段虚拟机在干些啥,首先加载利用类的全限定名将类加载成二进制文件流进入,将字节流中的静态存储结构转化为运行时的数据结构,在内存中生成一个Class对象,作为方法区的各种数据的反问入口。然后就是验证,验证Class文件流中的数据是否对虚拟机运行安全造成威胁。准备阶段就是为类变量分配内存并设置类变量的初始值(在方法区中进行分配),解析阶段就是将常量池内(常量池在Class对象中)的符号引用替换为直接引用。最后就是初始化,就是正式执行程序员写的相关指令,执行真正的java代码。

然后就是关于类加载器的知识点,主要就是类加载器是如何实现的,如何自定义类加载器,类加载器是怎么运行的,引出双亲委派机制,通过双亲委派模型被破坏引出JNDI以及OSGI两者的对应的类加载机制。

发布了72 篇原创文章 · 获赞 12 · 访问量 7592
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览