JVM 类加载机制

JVM 类加载机制,第1张

JVM 类加载机制

由上一篇JVM工作的整体逻辑,这一篇讲一下其中的类加载机制

回顾一些工作模型:  一个我们自己编写的.java文件,编译生成.class文件,将.class文件加载到JVM中,JVM根据.class二进制文件的不同数据信息分配到运行时数据区的不同位置,根据栈的信息一步一步的交给java执行器执行代码,在执行的过程中动态的再生成信息等放入到运行时数据区

由类加载系统,运行时数据区,java执行器三部分协作执行

这一篇主要写一下类加载系统自己的一些理解 1.整体流程

1. 编译文件
2. 运行文件
3. 启动java虚拟机
4. 通过C++代码创建Bootstrap类加载器,这个加载器是C++实现
5. 通过Bootstrap加载Launcher这个类
6. 通过Laucher创建java的Extension和App加载器  appClassLoader和ExtClassLoader为Laucher的内部类
7. 通过加载器双亲委派机制进行尝试加载,解析等一系列
8. 加载完成,通过main方法调用
2. loadclass的过程

在源码中loadclass是一个整体流程,包含准备工作使用三个类加载器(通过双亲委培机制判断这个.class能不能加载),还有就是.class的二进制文件通过字节流读取到内存分配到运行时数据区的过程.

1. 将二进制文件加载到内存,放入到运行时数据区进行以下的步骤
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证:校验字节码文件的正确性

准备:给类的静态变量分配内存,并赋予默认值

解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下节课会讲到动态链接

初始化:对类的静态变量初始化为指定的值,执行静态代码块

主要是由URLClassLoader类中的defineClass完成,我们的加载解析,验证的一系列过程在这里完成,同时在这个过程还会创建这个类唯一的Class对象
2.加载前的准备类加载器和双亲委派机制

这里主要进行的是准备工作,在loadclass之前,先通过准备工作判断这个类能否被加载,准备工作涉及到三个类加载器,那么这三个类加载器是如何完成准备工作的?  这里也就是双亲委培机制

//ClassLoader的loadClass方法,里面实现了双亲委派机制
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();
                    //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                    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;
        }
 
双亲委派机制:
从下到上依次先去已经加载过的类中比对当前要加载的类是否加载过
从上到下依次尝试查看当前要加载的类是否为自己要加载的路径下的文件,如果能加载就加载,加载不了交给子类

为什么要双亲委派?

沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
​
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

全盘负责委托机制

“全盘负责”是指当一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入。
​
public class Math {
    public static final int initData = 666;
    public static User user = new User();
}
如果Math由appclassLoader进行加载,那么这个类中的属性等  User类也由这个加载器进行加载

4.自定义类加载器

public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
    private String classPath;
​
    public MyClassLoader(String classPath) {
    this.classPath = classPath;
    }
​
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
        + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
            fis.read(data);
        fis.close();
        return data;
    }
​
    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法

我们自定义的类加载器也是要严格遵守双亲委派机制,向上委派是在ClassLoader的loadClass完成的,并不需要我们实现,jvm给我们完成了将自定义加载器的父加载器设置为appClassLoader,自然可以完成向上委派,我们完成的是向下加载,在上层类加载器都没有加载,轮到自定义加载器加载时候的过程

什么时候appClassLoader不会加载,才能轮到自定义类加载器加载呢?

1. 明确一个问题    
Booststrap   Extension App这三个都有自己的加载路径
BootStrap:   jdk安装目录下/jre/lib/的一些jar
Extension:   jdk安装目录下/jre/lib/ext    还可以由 -Djava.ext.dirs指定
App:         加载环境变量中classpath中的路径的文件和执行文件时通过参数  java -cp XXX 传递进去的路径文件
2. 由上面可以看出,App加载器也有固定的路径 ,在程序启动以后就不会变化了,那么我们自定义的加载器只要不和上层加载器的路径重叠,在向下加载的过程中,上层加载器没有加载到,自然就轮到了我们自定义的加载器进行加载
    MyClassLoader classLoader = new MyClassLoader("D:/test");
    //我们自定义的加载器在创建对象时可以通过这种方式传递能够加载的路径,也可以在构造方法中写死
注意:   一定不能路径和上层重叠.

例: 在linux中,我们编译以后的.class文件是如何被解析的?

export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
我们在环境变量中配置了.    表示当前执行路径可以被App进行加载
​
[root@node01 test]# java /usr/local/tmp/demo
Error: Could not find or load main class .usr.local.tmp.demo
当我们不执行当前文件夹下的.class    就不会被App加载器加载,自然执行失败
​
export CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/usr/local/tmp
当环境变量这样配置时,除了上述文件夹的.class  其余的都无法被加载

自定义类加载器的parent是如何指定的

MyClassLoader classLoader = new MyClassLoader("D:/test");
//在这个自定义加载器初始化时,会先初始化ClassLoader
用super(parent)指定
自定义的classLoader没有构造方法,默认访问的父类的无参构造方法
在这里getSystemClassLoader指的就是Appclassloader,写死的
       protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
而Ext和App是的parent并不是由ClassLoader类的无参构造方法设置的
5.打破双亲委派机制
由自定义加载器可以看出来,严格遵守双亲委派机制,双亲委派机制是由loadClass中classloader来进行完成的
那我们的自定义类加载器已经继承了loadclass,只需要重写classloader,把双亲委派的判断代码去掉即可
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) {
            // 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.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
        }
}
​
public static void main(String args[]) throws Exception {
    MyClassLoader classLoader = new MyClassLoader("D:/test");
    //尝试用自己改写类加载机制去加载自己写的java.jvm.user类
    Class clazz = classLoader.loadClass("java.jvm.user");
    Object obj = clazz.newInstance();
    Method method= clazz.getDeclaredMethod("sout", null);
    method.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName());
    }
 }
//我们在App加载器的classpath下放一份,也在D盘的这个位置放一份,取消掉双亲委派的代码查看结果
结果:
java.io.FileNotFounddException: D:testjavalangObject.class   (系统找不到指定的路径.)
所有的类都是继承自Object类的,双亲委派机制被打破,我们现在只能从D:test文件夹下加载Object类
因此,我们想直接将Object类复制到这个文件夹下,让我们的自定义类加载器加载就可以了
结果:
java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
沙箱安全机制
字节码校验器(bytecode verifier),类裝载器(class loader)
完成沙箱安全机制,jdk核心的包不可以被自定义的类加载器加载,String   Object等一些类的核心包
那么如何打破
修改loadClass方法,让我们自定义的类破环双亲委派,核心包下的还是由双亲委派完成加载
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) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            //在这里进行判断
            //如果这个类不是我们要打破双亲委派的类,就通过ClassLoader的loadClass双亲委派执行
            //如果是我们自己的就打破    这样Object就可以被BootStrap加载器加载了
            if(!name.startsWirh("com.jvm")) {
                c = this.getParent().loadClass(name)
            }else{             
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
        }
}
6.Tomcat打破双亲委派
1.  一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。例如: 两个war包,一个是spring4,一个是spring5的,通过双亲委派可以看出,相同类名包名的类不能被重复加载
​
2.  部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
​
3.  web容器也有自己依赖的类库,不能与应用程序的类库混淆。
​
上述问题如何解决?
为每个war包的类独立创建一个类加载器,每个独立的类加载器只能加载它自己独立路径下的类

1. JVM沙箱安全的类由 JVM的App Ext Bootstrap加载,进行双亲委派
2. Tomcat内部,每个war包都需要用的关于Tomcat的类,由虚线框加载,到达虚线处不再向上双亲委派
3. 我们自己的war包,由每一个独立的WebappclassLoader加载,并且不进行向上双亲委派
tomcat的几个主要类加载器:
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的
WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5684254.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存