JVM(一)——浅析JVM类加载机制及双亲委派机制

类加载过程

将java文件打成jar包或者war包之后,由某个主类的main函数进行启动,需要通过类加载器把主类加载到JVM,jar包里的类不是一次性加载,是在使用到的时候才会加载。

类加载步骤

  • 加载:在硬盘上查找并通过IO读取字节码文件
  • 验证:验证字节码文件正确性
  • 准备:给类的静态变量分配内存,并赋默认值。例如:int=0、boolean=false、对象=null
  • 解析:将静态方法符号引用替换为直接引用——静态链接;程序运行期间完成符号引用替换为直接引用——动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

类加载器

引导类加载器(启动类加载器)——BootstrapClassLoader

由C++实现,嵌入在JVM内部,负责加载JRE的lib目录下的核心类库,比如rt,jar、charsets.jar等

扩展类加载器——ExtClassLoader

由Java代码实现,在JVM启动器实例(sun.misc.Lanucher)初始化时创建,是负责加载JRE的lib目录下ext的jar包

应用程序类加载器——AppClassLoader

由Java代码实现,在JVM启动器实例(sun.misc.Lanucher)初始化时创建,负责加载ClassPath路径下的类包,主要就是加载自己写的类

自定义加载器

负责加载自定义路径下的类包

类加载器初始化过程

底层C++代码调用Java代码创建JVM启动器实例sum.misc.Launcher,Launcher初始化使用了单例模式,保证JVM虚拟机只有一个sum.misc.Launcher实例。
在Launcher构造方法内部创建了两个类加载器,一个是扩展类加载器,一个是应用类加载器。JVM默认使用getAppClassLoader()方法返回的类加载器来加载我们的应用程序。
示例代码如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //构造扩展类加载器ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }
 
    try {
        //构造应用类加载器AppClassLoader,并设置父类加载器为ExtClassLoader,设置Launcher的loader属性为AppClassLoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
 
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
 
}

双亲委派机制

image.png
下面我们来看下应用类加载器AppClassLoader加载类的双亲委派机制源码,源码如下:

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 {
                        //若父类加载器为空,则由引导类加载器BootstrapClassLoader加载()
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // 如果仍未找到,则调用当前类加载器的findClass方法继续查找
                    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;
        }
    }

结合源码双亲委派大致步骤如下:

  1. 首先先判断类是否被加载过,如果被加载过直接返回。
  2. 如果没有被加载过,如果有父类加载器则有父类加载,如果没有父类加载器则有JVM内置的引导类加载器进行加载。
  3. 如果父类加载器及引导类加载器(BootstrapClassLoader)都未查找到指定类,则由调用当前的类加载器的findClass方法自行加载。

为什么要设计双亲委派机制

  • 沙箱安全机制:防止核心类库被篡改
  • 避免类的重复加载:父类已经加载了该类,没必要子类再加载一次,保证被加载类的唯一性

自定义类加载器

自定义加载器只需要集成ClassLoader类,这个类有两个核心方法,一个是loadClass方法实现了双亲委派机制、一个是findClass方法默认,自定义类加载器主要就是重写findClass方法。
示例代码如下:

public class MyClassLoaderTest  {
 
    @AllArgsConstructor
    @Data
    static class MyClassLoader extends ClassLoader{
        private String classPath;
 
        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                throw new ClassNotFoundException();
            }
        }
 
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
            int length = inputStream.available();
            byte[] data = new byte[length];
            inputStream.read(data);
            inputStream.close();
            return data;
        }
    }
 
    public static void main(String[] args) throws Exception {
 
        //自定义类加载器加载路径
        MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
        //类加载器加载指定类,将指定加载类的class文件放在指定的加载路径中
        Class clazz = myClassLoader.loadClass("com.demo.zsydemo.jvm.Test");
        Object object = clazz.newInstance();
        Method method = object.getClass().getMethod("sout");
        method.invoke(object,null);
        //输出当前类加载器是否是自定义的类加载器
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
 
}
public class Test {
    public static void sout(){
        System.out.println("这是自定义类加载器加载的");
    }
}

运行结果:

这是自定义类加载器加载的
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader

如何打破双亲委派

类加载器中实现双亲委派的方法是loadClass方法,我们只需在掐面写的自定义类加载器中重写一下loadClass方法,干掉原生loadClass方法中的向上委托的地方即可打破双亲委派机制。下面是尝试打破双亲委派机制,用自定义类加载器加载自己实现的java.lang.String.class的示例代码如下:

public class MyClassLoaderTest {
 
    @AllArgsConstructor
    @Data
    static class MyClassLoader extends ClassLoader {
        private String classPath;
 
        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;
            }
        }
 
        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
 
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
            int length = inputStream.available();
            byte[] data = new byte[length];
            inputStream.read(data);
            inputStream.close();
            return data;
        }
    }
 
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
        Class clazz = myClassLoader.loadClass("java.lang.String");
        Object object = clazz.newInstance();
        Method method = object.getClass().getMethod("replace");
        method.invoke(object, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
 
}

运行结果:

java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
    at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:66)
    at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:48)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    at com.demo.zsydemo.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:86)
Exception in thread "main" java.lang.ClassNotFoundException
    at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.findClass(MyClassLoaderTest.java:69)
    at com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader.loadClass(MyClassLoaderTest.java:48)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    at com.demo.zsydemo.jvm.MyClassLoaderTest.main(MyClassLoaderTest.java:86)

这个异常是返回的安全异常,禁止加载java.lang包,这个就是前面讲的沙箱安全机制,这个机制可以防止核心类库被篡改。

打破双亲委派的场景

Tomcat容器如何打破双亲委派机制

tomcat是一个web容器,它需要解决什么问题:

  1. 一个web容器可能需要部署多个应用,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个类库都是独立的,保证相互隔离
  2. 部署在同一个web容器中,相同的类库的相同版本可以共享
  3. web容器也有自己依赖的类库,不能需应用程序的类库混淆,需要将容器类库与应用程序类库保持隔离
  4. web容器需要支持jsp的修改,需要支持修改jsp后不重启

基于上面四个问题,我们可以看下tomcat为什么要打破双亲委派
第一个问题,如果使用默认的类加载机制,是不支持加载相同类库不同版本的,默认类加载器需要确保被加载类的唯一性
第二个问题,默认类加载器可以实现,默认类加载器的职责就是唯一的
第三个问题和第一个问题一样
第四个问题,如果要实现jsp文件的热加载,jsp文件其实也就是class文件,如果文件修改,类名称不变,默认类加载器还是会去直接取方法区中已经存在的,修改后的jsp文件是不会被重新加载的,每个jsp文件都会对应一个jsp类加载器,如果jsp被修改了,就会直接卸载这个jsp类加载器,重新创建类加载器,重新加载jsp文件
image.png

Tomcat主要类加载器:

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径的class对web容器和各个webapp都可见。
  • CatalinaClassLoader:web容器私有的类加载器,加载路径的class只对web容器可见。
  • SharedClassLoader:各个webapp共享的类加载器,加载路径的class对所有webapp可见,对web容器不可见。
  • WebappClassLoader:各个webapp的私有类加载器,加载路径的class只对当前webapp可见。

通过上图的委派关系中可以看出:

  • CommonClassLoader加载的类也都能被CatalinaClassLoader和SharedClassLoader使用,实现了公有库公用的问题。
  • CatalinaClassLoader和SharedClassLoader自己能加载的类对对方相互隔离。
  • WebappClassLoader可以使用SharedClassLoader加载的类,每个war包都有自己对应的WebappClassLoader,各个实例之间可以实现相互隔离。
  • JaspLoader加载范围仅仅是这个jsp所编译出来的那一个class文件,出现的目的就是为了被丢弃,每当jsp文件被修改,会替换到当前JaspLoader的实例,并通过在新建一个新的jsp类加载器实现jsp的热加载功能。

tomcat这种为了实现隔离性,WebappClassLoader加载自己目录下的class文件,不会传递给父类加载器加载,打破了双亲委派机制。
下面是模拟tomcat实现WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离,也是基于上面的打破双亲委派的自定义类加载器实现的。
代码示例如下:

public class MyClassLoaderTest {
 
    @AllArgsConstructor
    @Data
    static class MyClassLoader extends ClassLoader {
        private String classPath;
 
        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();
                        if (!name.startsWith("com.demo.zsydemo.jvm")) {
                            c = this.getParent().loadClass(name);
                        } else {
                            c = findClass(name);
                        }
 
 
                        // this is the defining class loader; record the stats
                        PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
 
        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
 
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
            int length = inputStream.available();
            byte[] data = new byte[length];
            inputStream.read(data);
            inputStream.close();
            return data;
        }
    }
 
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/yyyz/env/jvmtest");
        Class clazz = myClassLoader.loadClass("com.demo.zsydemo.jvm.TomcatTest");
        Object object = clazz.newInstance();
        Method method = object.getClass().getMethod("sout");
        method.invoke(object, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
 
        System.out.println("--------------分割线--------------");
 
        MyClassLoader myClassLoader1= new MyClassLoader("/Users/yyyz/env/jvmtest01");
        Class clazz1 = myClassLoader1.loadClass("com.demo.zsydemo.jvm.TomcatTest");
        Object object1 = clazz1.newInstance();
        Method method1 = object1.getClass().getMethod("sout");
        method1.invoke(object1, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
 

运行结果:

模拟tomcat测试类——TomcatTest
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader
--------------分割线--------------
另外一个模拟tomcat测试类——TomcatTest
com.demo.zsydemo.jvm.MyClassLoaderTest$MyClassLoader

同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可能是不一样,所以看两个对象是否是同一个,除了看类名和包名是否是同一个,还要看类加载器是否是同一个。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值