1.java类加载机制图

上图是java类的加载过程以及涉及到类加载:

1.我们都知道.java文件最终要编译成.class文件才可以被执行,例如com.tynet.module.admin.controller.LoginController.java文件会被编译成com.tynet.module.admin.controller.LoginController.class这种class文件然后放入JVM才可以被执行。

2.既然说到了JVM肯定有创建JVM的地方,windows系统下java.exe调用底层的jvm.dll文件创建java虚拟机,然后通过C++的实现来创建引导类加载器,他的父类加载器是null,因为他自己就是最底层的类加载器器了。引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等

3.c++调用java代码创建JVM启动器,实例化sun.misc.Launcher这个对象,该类由引导类加载器负责加载和创建,然后在由引导类加载器分别创建扩展类加载器和应用类加载器,下面是核心的代码,分别在创建的时候设置父类加载器。

扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包

应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类 

// 在启动的时候调用静态方法创建了一个launcher对象
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
        return launcher;
    }
扩展类加载器:
引导类加载器创建扩展类加载器,并且将引导类加载器作为扩展类加载器父类
Launcher.ExtClassLoader.getExtClassLoader();
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
应用类加载器:
扩展类加载器创建应用类加载器,并且应用类加载器的父类是扩展类加器器
Launcher.ExtClassLoader var1;
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
super(var1, var2, Launcher.factory);
Launcher.getClassLoader方法调用类加载器,默认是应用类加载器
 public ClassLoader getClassLoader() {
        return this.loader;
    }
 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
针对不同的类,运行不同的classLoader,来加载不同类信息,比如String这个类是由引导类加载器加载,jre下面的ext包下的所有jar交给扩展类加载器加载,我们程序自己写的类Controller类等都是应用类加载器加载

4.ClassLoader.loadClass加载要运行的类LoginController,默认使用的是双亲委派的原则。

其中还要经过:加载, 验证,准备, 解析,初始化过程:

加载:java类加载机制是懒加载,使用类的时候才会加载,会在硬盘上查找并通过IO读入字节码文件到内存中,在内存中会生成一个代表这个类的CLASS对象,放在方法区中。

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

准备:给类的静态变量分配内存,并赋予默认值,比如有一个public static int i = 5,在这个阶段会给i开辟一个空间,并且将i的值设置为0

解析:将符号引用替换为直接引用,该阶段会把符号引用如static public main方法等符号引用替换为在内存中的真实地址,这就是静态链接,还有一种是在类运行的期间才可以知道调用那个类,例如接口的多态,只有在调用期间才可以知道调用那个类,这个属于动态链接。

初始化:对类的静态变量初始化为指定的值,上面的例子这个阶段会将i的值设置为5,初始化的时候执行静态代码块,且只执行一次。

5.执行完上面的步骤就会将该类加入到JVM中运行,当运行结束后,JVM会把该类销毁回收掉。

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

解释:双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改

举个例子:我们自己定义一个java.lang.String的一个类,在这个类中我们写一个main方法然后执行这个方法

package java.lang;
public class String1{ 
    public static void main(String[] args) {
        System.out.println("打印自己的string类");
    }
}


打印结果:错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application


结论:
我们自己定义一个String类,根据双亲委派的原则,由于在java的lib下面也有这个类,所以引导类加载器会加载到这个类,就不会加载自己写的,这样保证了java的源代码无法被篡改,防止黑客攻击置入后门程序。

2.避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。

7.如何自定义类加载器:

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

1.在maven项目中有User这个类,在要加载的路径下也有这个类。

public class MyClassLoaderTest extends ClassLoader{


    private String classPath;

    public MyClassLoaderTest(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();
        }
    }

    public static void main(String args[]) throws Exception {     
        MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/test");
        //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class clazz = classLoader.loadClass("JVM.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());    
    }
}


// 执行结果:
自定义类加载器的执行
sun.misc.Launcher$AppClassLoader

// 结论:
按照双亲委派的原则:在项目中自己写的类加载是使用应用类加载器加载,那么只要加载到了就会直接返回结果,就不会在自定义的类加载器中加载这个类,保证的加载类的唯一性。所以执行的结果看出使用的类加载器是应用类加载器。


2.在maven项目下没有User1这个类,在要加载的目录中有这个类

public class MyClassLoaderTest extends ClassLoader{


    private String classPath;

    public MyClassLoaderTest(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();
        }
    }

    public static void main(String args[]) throws Exception {        
        MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/test");        
        Class clazz = classLoader.loadClass("JVM.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}


执行结果:
自定义类加载器的执行User1
JVM.MyClassLoaderTest


最终结论:
按照双亲委派的原则:先由父类加载,如果父类无法加载该类,在交给子类加载,现在我们自己写的类,应用类加载器没有加载到,那就交给我们自定义的加载器加载,加载到了直接返回结果,否则抛出异常。从执行结果看我们使用的是自定义的类加载器,打印出要执行的结果。

8.如何打破双亲委派机制?

解释:打破双亲委派机制:就是要加载一个类,交给父类加载器加载,没有找到然后在由子类加载,打破的话就是说:不交给父类加载,我们自己定义加载的方式和findClass的方法,然后实现加载类。

package JVM.ClassLoad;

import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * @author: yangwenbin
 * @date: 2021/7/12 11:44
 * @since: JDK 1.8
 * @description: 自定义类加载器 打破双亲委派机制
 */
public class MyClassLoaderTestPo extends ClassLoader{


    private String classPath;

    public MyClassLoaderTestPo(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();
        }
    }


    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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);               
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    /**
     * @author: yangwenbin
     * @date: 2021/7/12 13:51
     * @since: JDK 1.8
     * @description: 打破双亲委派机制的话  user1类的父类是OBJECT类  他也会加载object类 现在打破了双亲委派机制 找不到类了  Prohibited package name: java.lang
     * @param: [args]
     * @return: void
     */
    public static void main(String args[]) throws Exception {
        MyClassLoaderTestPo classLoader = new MyClassLoaderTestPo("D:/test");
        //尝试用自己改写类加载机制去加载自己写的java.lang.String.class
        Class clazz = classLoader.loadClass("JVM.User1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());

        // 自定义类加载器的执行User1
        //JVM.MyClassLoaderTestPo
    }
}

用上面的方式会产生的结果是:这样会产生一个问题就是:我们都知道一个类的顶层的类是Object类,这样的话我们打破了双亲委派机制,这样父类加载器引导类加载器就无法加载Object类,当我们在运行的时候就会提示找不到Object类,java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)

解决办法:既然java出于安全的考虑,防止Object类被篡改,这样像String,Object这样的类只能由引导类加载器加载,那么我们就定义那个路径下的是由我们的自己定义的类加载器加载,其他的像object还是交给引导类加载器加载,这样就可以了。

修改一下loadClass方法:(自己写的使用自己的类加载器加载,Object的那种类还是使用双亲委派方式)

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(name.startsWith("JVM")){
                    c = findClass(name);
                }else{
                    // 其他的有可能就是像OBJECT就需要引导类加载器加载 还是要使用双亲委派机制
                    c = this.getParent().loadClass(name);
                }     
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

执行结果:
自定义类加载器的执行User1
JVM.MyClassLoaderTestPo

 这样就实现了打破双亲委派机制。

9.tomcat如何打破双亲委派模式?

 上图就是tomcat类加载的基本模型:

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。

很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

10.模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离?

package JVM.ClassLoad;

import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * @author: yangwenbin
 * @date: 2021/7/12 16:34
 * @since: JDK 1.8
 * @description: tomcat打破双亲委派机制
 */
public class MyClassLoaderTomcat extends ClassLoader{
    private String classPath;

    public MyClassLoaderTomcat(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);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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();

                //非自定义的类还是走双亲委派加载
                if (!name.startsWith("JVM")){
                    c = this.getParent().loadClass(name);
                }else{
                    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 {
        MyClassLoaderTomcat classLoader = new MyClassLoaderTomcat("D:/test");
        Class clazz = classLoader.loadClass("JVM.User1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();
        MyClassLoaderTomcat classLoader1 = new MyClassLoaderTomcat("D:/test1");
        Class clazz1 = classLoader1.loadClass("JVM.User1");
        Object obj1 = clazz1.newInstance();
        Method method1= clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());

    }
}

 // 执行结果:自己使用自己类路径下的类名 然后使用自己定义的类加载器加载,也打破了双亲委派机制,不用父类加载
//        自定义类加载器的执行User1
//        JVM.MyClassLoaderTomcat@6ce253f1
//
//        自定义类加载器的执行User1
//        JVM.MyClassLoaderTomcat@65ab7765

谢谢观看,欢迎指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值