zoukankan      html  css  js  c++  java
  • JVM(九)类加载

    问题

    • 类加载机制,一个类加载到虚拟机中一共有几个步骤,这些步骤的顺序哪些是固定的,哪些是不固定的,为什么不固定

    答 : 1.加载 2.校验 3.准备 4.静态解析(不固定) 5.初始化 6.使用 7.动态解析(不固定) 8.卸载 参考连接 : https://www.jianshu.com/p/2a3cdc027c2c

    概述

    • java 的类加载主要给三个步骤
    1. 加载
    2. 链接(还分为验证,准备,解析三个小步骤)
    3. 初始化
    • 加载中有一个重要的原则 : 双亲委派模型 。

    加载

    类加载过程的第一步,主要完成下面3件事情:

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

    java 类标识

    在运行时期,一个类的标识不仅由它的名字决定,而且由它的二进制名字和定义它的loader,每一个类或是接口属于一个单独的 run-time package 。 而这个 run-time package 内的类和接口的定义由包名和定义它的加载器决定。

    我们知道java语言类型有两类: 引用类型(reference types)和基本类型(primitive types) ,基本类型是由java 虚拟机预先定义好的,引用类型,java将其分为四种: 类,接口,数组类和泛型参数。由于泛型参数会在编译过程中被擦除,因此java虚拟机实际上只有三种 : 接口,类,数组。

    接口和类由类加载器进行加载,而数组类型并不由类加载器进行加载,是由jvm进行加载。

    加载过程概述

    下面描述来自 : https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html

    在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

    双亲委托加载

    1297993-20200416102038427-116171272.jpg

    需要注意的是以上的各个类加载器并不是继承的(extend)关系而是组合持有的关系,当一个子类在加载一个类的时候它首先会先交给父类加载,假如父类加载过了,那么父类返回加载过的类,假如父类无法加载那么再由子类加载器进行加载。

    public abstract class ClassLoader {
    
        private static native void registerNatives();
        static {
            registerNatives();
        }
    
        // The parent class loader for delegation
        // Note: VM hardcoded the offset of this field, thus all new fields
        // must be added *after* it.
        // 持有的父类类加载器
        private final ClassLoader parent;
    
    
        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 {
                            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;
            }
        }
    
    }
    
    
    

    loadClass 方法的注解。

    Loads the class with the specified binary name. The default implementation of this method searches for classes in the following order:
    1. Invoke findLoadedClass(String) to check if the class has already been loaded.
    2. Invoke the loadClass method on the parent class loader. If the parent is null the class loader built-in to the virtual machine is used, instead.
    3. Invoke the findClass(String) method to find the class.
    
    If the class was found using the above steps, and the resolve flag is true, this method will then invoke the resolveClass(Class) method on the resulting Class object.
    
    Subclasses of ClassLoader are encouraged to override findClass(String), rather than this method.
    Unless overridden, this method synchronizes on the result of getClassLoadingLock method during the entire class loading process.
    

    其中

    • Bootstrap ClassLoader : 用来加载java核心库。主要是 jre/lib目录(来源于环境变量sun.boot.class.path)。其由c++编写,本身属于虚拟机的一部分,无法在java代码中获取它的引用。
    • Extensions ClassLoader : 用来加载java的扩展库。
    • Application ClassLoader : 用来加载运行的java应用中的库类,可以通过 ClassLoader.getSystemClassLoader()来获取它。例如 pom.xml 文件中的库类。

    上述代码的加载过程就是代理模式

    代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

    下面是双亲委托加载的示意图,图片来自参考资料 :

    1297993-20200416135145267-264158634.jpg

    为什么要使用双亲委托模式呢?

    有一点是为了安全,假如作为基础类 java.lang.Object 在父类中加载过了,某个开发又从其他地方加载同样的 Object 那么就会失败,因为父类已经加载过了,这也保证了java中核心类加载的安全性。

    破坏双亲委托模式

    首先我们要知道什么才算破坏,之前双亲委托模式情况加载前都要交给 parent 进行加载先,破坏的意思是:子类加载器不再先从父类加载器进行加载,而是自己加载。

    破坏双亲委托的例子

    例子来源 : https://juejin.im/post/5a59f2296fb9a01ca871eb8c

    如果基础类调用会用户的代码怎么办呢? 一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。怎么办呢?

    java团队引入了线程上下文类加载器 。

    线程上下文类加载器

    线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器(Application ClassLoader)。在线程中运行的代码可以通过此类加载器来加载类和资源。

    当前线程上下文加载器的获取

    Thread.currentThread().getContextClassLoader();
    ClassLoader.getSystemClassLoader();
    
    

    有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则

    使用自定义的类加载器

    在某个路径下创建java程序如下 :

    public class Test{
        private int age; 
        private String name;
        public Test(){
            this.age = 15;
            this.name ="Aaaa";
        }
    }
    

    然后 javac Test.java 生成class文件后。

    public class MyClassLoader extends ClassLoader {
    
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            File file = getClassFile(name);
            try {
                byte[] bytes = getClassBytes(file);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return super.findClass(name);
        }
    
        private File getClassFile(String name) {
    
            return new File("C:\Users\Administrator\Desktop\Test.class");
        }
    
        private static byte[] getClassBytes(File file) throws Exception {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream aos = new ByteArrayOutputStream(fis.available());
            byte[] bytes = new byte[fis.available()];    //使用fis.avaliable()方法确保整个字节数组没有多余数据
            fis.read(bytes);
            aos.write(bytes);
            fis.close();
            return aos.toByteArray();
        }
    
        public static void main(String[] args) throws Exception {
            MyClassLoader ct = new MyClassLoader();
            Class c = Class.forName("Test", true, ct);
            System.out.println(c.getClassLoader());
        }
    }
    
    

    运行看到结果。

    补充

    • tomcat 中也是破坏了双亲委托模式
    • 热部署相关 建议还是看《深入理解java虚拟机》,网上博客写得乱七八糟。

    参考资料

    • https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3.2 (官方文档)
    • https://www.cnblogs.com/aspirant/p/8991830.html (看)
    • https://www.cnblogs.com/huxuhong/p/11856786.html (看)
    • https://www.jianshu.com/p/9df9d318e838
  • 相关阅读:
    硬件的效率与一致性
    深入理解SPI机制-服务发现机制
    spring 之7种重要设计模式
    list里放map list 放list
    jvm 三种编译
    几种不同格式的json解析
    Java知识点梳理——集合
    判断2个list中是否有相同的数据(相交)Collections.disjoint
    键相同,比较两个map中的值是否相同
    Map类型数据导出Excel--poi
  • 原文地址:https://www.cnblogs.com/Benjious/p/12712816.html
Copyright © 2011-2022 走看看