原理收藏-技术篇编程语言-Java系列

JVM——类加载器深度剖析

2020-05-05  本文已影响0人  小波同学

类加载

在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
提供了更大的灵活性,增加了更多的可能性

类加载器深入剖析

Java虚拟机与程序的生命周期在如下几种情况下,Java虚拟机将结束生命周期。

类的加载、连接与初始化

Java程序对类的使用方式可分为两种

所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。

主动使用(七种)

被动使用

除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化(可能会加载和连接但不会初始化)

类的加载

案例1:

/**
 * @Description:
 * 对于静态字段来说,只有直接定义了该字段的类才会被初始化
 * 当一个类在初始化时,要求其父类全部都已经初始化完毕了
 * -XX:+TraceClassLoading 用于追踪类的加载信息并打印出来
 *
 * JVM参数
 * 三种配置参数情况:
 *  -XX:+<option> :开启option选项
 *  -XX:-<option> :关闭option选项
 *  -XX:<option>=<value> :将option选项的值设置为value
 */
public class MyTest1 {

    public static void main(String[] args) {
        //子类类名.父类静态变量
//        System.out.println(MyChild1.str);

        //子类类名.子类静态变量
        System.out.println(MyChild1.str2);
    }
}

class MyParent1{
    public static String str = "hello world";

    static{
        System.out.println("MyParent1 static block");
    }
}

class MyChild1 extends MyParent1{

    public static String str2 = "welcome";

    static{
        System.out.println("MyChild1 static block");
    }
}

案例2:

/**
 * @Description:
 * 常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
 * 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量类的初始化
 * 注意:这里指的是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了
 * 甚至,我们可以将MyParent2的class文件删除
 *
 * 助记符:
 * ldc:表示将int、float和String类型的常量值从常量池中推送至栈顶
 * bipush:表示将单字节(-128 - 127)的常量值推送至栈顶
 * sipush:表示将一个短整型常量值(-32768 - 32767)的常量值推送至栈顶
 * iconst_1:表示将int类型的1推送至栈顶 (iconst_m1 - iconst_5)
 */
public class MyTest2 {

    public static void main(String[] args) {
        System.out.println(MyParent2.str);
        System.out.println(MyParent2.sh);
        System.out.println(MyParent2.i);
        System.out.println(MyParent2.in);
    }
}

class MyParent2{

    public static final String str = "hello world";

    public static final short sh = 7;

    public static final int i = 1;

    public static final int in = 128;

    static{
        System.out.println("MyParent2 static block");
    }
}

通过反编译可以看到更直观的信息


案例3:

/**
 * @Description:
 * 当一个常量的值在编译期不能确定时,那么其值就不会被放到调用类的常量池中
 * 这时在程序运行,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
 */
public class MyTest3 {

    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}

class MyParent3{

    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("MyParent3 static code");
    }
}

案例4:

/**
 * @Description:
 * 对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为[Lcom.yibo.jvm.classloader.MyParent4这种形式
 * 动态生成的类型,其父类型就是Object
 * 对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型
 *
 * 助记符:
 * anewarray:表示创建一个引用类似的(类、接口、数组)数组,并将其引用值压入栈顶
 *  newarray:表示创建一个指定的基本类型(int、float、char等)的数组,并将其引用值压入栈顶
 *
 */
public class MyTest4 {

    public static void main(String[] args) {
//        MyParent4 myParent4 = new MyParent4();

        MyParent4[] myParent4s = new MyParent4[1];
        System.out.println(myParent4s.getClass());
        System.out.println(myParent4s.getClass().getSuperclass());

        MyParent4[][] myParent4s1 = new MyParent4[1][1];
        System.out.println(myParent4s1.getClass());
        System.out.println(myParent4s1.getClass().getSuperclass());

        int[] arr = new int[1];
        System.out.println(arr.getClass());
        System.out.println(arr.getClass().getSuperclass());
    }
}

class MyParent4{
    static {
        System.out.println("MyParent4 static block");
    }
}

完整流程:

有两种类型的类加载器:

Java虚拟机自带的加载器

用户自定义的类加载器

类的验证

类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
类的验证的内容
* 类文件的结构检查:确保类文件遵从Java类文件的固定格式。
* 语义检查:确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖
* 字节码验证:确保字节码流可以被Java虚拟机安全地执行。字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
* 二进制兼容性的验证

类的准备

类的解析

类的初始化

类的初始化的步骤

类的初始化时机

类加载器

类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。

类加载器ClassLoader,它是一个抽象类,ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的负责整个类装载流程中的“加载”阶段。

有两种类型的类加载器:

Java虚拟机自带的加载器

用户自定义的类加载器

类加载的双亲委派机制

双亲委派模式工作原理

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

类加载器关系

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

ClassLoader的重要方法:

载入并返回一个类。

public Class<?> loadClass(String name) throws ClassNotFoundException

定义一个类,该方法不公开被调用。

protected final Class<?> defineClass(byte[] b, int off, int len)

查找类,loadClass的回调方法

protected Class<?> findClass(String name) throws ClassNotFoundException

查找已经加载的类。

protected final Class<?> findLoadedClass(String name)

每个ClassLoader都有另外一个ClassLoader作为父ClassLoader,BootStrap Classloader除外,它没有父Classloader。
ClassLoader加载机制如下:



自下向上检查类是否被加载,一般情况下,首先从App ClassLoader中调用findLoadedClass方法查看是否已经加载,如果没有加载,则会交给父类,Extension ClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 Bootstrap ClassLoader到 App ClassLoader依次尝试加载。

值得注意的是即使两个类来源于相同的class文件,如果使用不同的类加载器加载,加载后的对象是完全不同的,这个不同反应在对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。

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方法,直到BootstrapClassLoader(没有父类),我们把这个过程叫做双亲委派,或父类委托。

创建用户自定义的类加载器

public class MyClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private final String fileExtension = ".class";

    public MyClassLoader(String classLoaderName){
        super();//将系统类加载器作为该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader parent,String classLoaderName){
        super(parent);//显示指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "MyTest15{" +
                "classLoaderName='" + classLoaderName + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked:"+className);
        System.out.println("class loader name:"+this.classLoaderName);
        byte[] data = this.loadClassData(className);
        return this.defineClass(className,data,0,data.length);
    }

    private byte[] loadClassData(String className){
        InputStream inputStream = null;
        byte[] data = null;
        ByteArrayOutputStream bos = null;

        try {
            this.path = path.replace(".","/");
            this.classLoaderName = classLoaderName.replace(".","/");
            inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
            bos = new ByteArrayOutputStream();
            int ch;
            while((ch = inputStream.read()) != -1){
                bos.write(ch);
            }
            data = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }


    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
        myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz1:" + clazz1);
        Object object1 = clazz1.newInstance();
        System.out.println(object1);
        System.out.println(myClassLoader1.getClass().getClassLoader());

        System.out.println("------------------");

        MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
        myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz2:" + clazz2);
        Object object2 = clazz2.newInstance();
        System.out.println(object2);

        System.out.println("------------------");

        MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
        myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz3:" + clazz3);
        Object object3 = clazz3.newInstance();
        System.out.println(object3);
    }
}

类加载器的命名空间

运行时包

由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一运行时包,不仅要看他们的包名是否相同,还要看定义类加载器是都相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的可见成员。假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。

不同类加载器的命名空间关系

类的卸载

双亲模式的问题

顶层ClassLoader,无法加载底层ClassLoader的类

Java框架(rt.jar)如何加载应用的类?
比如:javax.xml.parsers包中定义了xml解析的类接口
Service Provider Interface SPI 位于rt.jar
即接口在启动ClassLoader中。
而SPI的实现类,在AppLoader。
这样就无法用BootstrapClassLoader去加载SPI的实现类。

解决

JDK中提供了一个方法:

获得ClassLoader的途径

Jar Hell 问题以及解决方案

ClassLoader cl = Thread.currentThread().getContextClassLoader();
String resouceName = "java/lang/String.class";
Enumeration<URL> resources;
try {
    resources = cl.getResources(resouceName);
    while (resources.hasMoreElements()) {
        URL nextElement = resources.nextElement();
        System.out.println(nextElement);

    }
} catch (IOException e) {

}

双亲模式的破坏

双亲模式是默认的模式,但不是必须这么做;
Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent;
OSGi的ClassLoader形成网状结构,根据需要自由加载Class。

参考:
https://blog.csdn.net/weixin_43907332/article/details/86625277

https://www.cnblogs.com/luckgood/p/8981508.html

https://www.cnblogs.com/mybatis/p/9396135.html

https://www.cnblogs.com/zhihaospace/p/12290386.html

上一篇 下一篇

猜你喜欢

热点阅读