类加载运行过程

我这里有个Math类,代码如下:

package com.guaiyu.jvm;

public class Math {

    public static final int INIT_DATA = 666;
    public static User user = new User();
    public int compute(){
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        System.out.println(c);
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

该类运行main方法,到方法结束,jvm是底层是如下图所示执行的:
类运行过程
其中jvm加载类的过程会经历以下几个步骤:
类加载过程

  • 加载: 在硬盘上查找并通过IO读入字节码文件(所以编译后的文件是.class文件,写代码的文件是.java文件),使用到类的时候才会加载,例如调用main()方法,new对象等等,在加载阶段会在内存中生成一个代表类的java.long.Class对象,最为方法区这个累的各种数据访问入口
  • 验证::校验字节码**.class**文件的正确性,例如字节码文件是十六进制数组文件组成,所有字节码文件都是 cafe babe开头,字节码文件部分内容如下图所示:
    字节码文件内容
  • 准备: 给静态变量分配内存,并赋予默认值,因为静态变量 是随着类加载的时候就被加载的,所以要提前为期分配内存,并赋予默认值,eg:Math类中的静态变量是user,相应的要为iuser赋值默认值null
    此处解释一下:被static修饰的是静态变量,代码中int被static final修饰,为常量常量在准备阶段要赋予实际值,及代码中int赋值为666
  • 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
  • 初始化: 对类的静态变量初始化为指定的值,执行静态代码块

类被加载到方法区之后主要包含运行时常量池类型信息字段信息方法信息类加载的引用对应class实例的引用等信息。

  • 类加载器的引用: 这个类到类加载器实例的引用。
  • 对饮class实例的引用: 类加载器在加载类信息放到方法区后,会创建一个对应class类型的对象放到 堆(Heap) 中,作为开发人员访问方法区中类定义的入口和切入点。
    注意: 主类在运行过程中加载到其他类,才会逐步去加载这些类,也就是说jar包或war包不是一次性加载的,是使用到时才会加载,也就是懒加载
    以下代码是对类懒加载的代码演示:
package com.guaiyu.jvm;

public class LazyLoadTest {

    static {
        System.out.println("************ 加载主类静态代码块 ***************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("************* lazy load test *******************");
        B b = null;
    }

}


class A {

    static {
        System.out.println("*************** 加载class A的静态代码块 ***************");
    }

    public A() {
        System.out.println("*************** initial A ******************");
    }
}

class B {

    static {
        System.out.println("*************** 加载class B的静态代码块 ***************");
    }

    public B() {
        System.out.println("*************** initial B ******************");
    }
}

运行结果:
************ 加载主类静态代码块 ***************
*************** 加载class A的静态代码块 ***************
*************** initial A ******************
************* lazy load test *******************

上述代码中调用了A,但是没调用B,所以只会加载Class A 而不会加载Class B

package com.guaiyu.jvm;

public class LazyLoadTest {

    static {
        System.out.println("************ 加载主类静态代码块 ***************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("************* lazy load test *******************");
        B b = new B();
    }

}


class A {

    static {
        System.out.println("*************** 加载class A的静态代码块 ***************");
    }

    public A() {
        System.out.println("*************** initial A ******************");
    }
}

class B {

    static {
        System.out.println("*************** 加载class B的静态代码块 ***************");
    }

    public B() {
        System.out.println("*************** initial B ******************");
    }
}

运行结果:
************ 加载主类静态代码块 ***************
*************** 加载class A的静态代码块 ***************
*************** initial A ******************
************* lazy load test *******************
*************** 加载class B的静态代码块 ***************
*************** initial B ******************

上述代码中调用了A,调用了B,所以逐步加载Class AClass B

类加载器

上述类加载过程,主要是通过类加载器实现的,类加载器有以下几种类型:

  • 引导类加载器: 负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器: 负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序类加载器: 负责加载ClassPath路径下的类包,主要加载的就是自己写的类
  • 自定义加载器: 负责加载用户自定义路劲下的类包

以下代码是对加载器类包的佐证:

package com.guaiyu.jvm;

import sun.misc.Launcher;

import java.net.URL;
import java.util.Optional;

public class TestJDKClassLoader {

    public static void main(String[] args) {
        System.out.println("bootstrapClassLoader引导类加载加载的文件:");

        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urLs.length; i++) {
            System.out.println(urLs[i]);
        }

        System.out.println("extClassLoader扩展类加载器加载文件:");
        String extPaths = System.getProperty("java.ext.dirs");
        String[] extUrls = Optional.ofNullable(extPaths)
                .map(S -> extPaths.split(";"))
                .orElse(new String[0]);
        for (int i = 0; i < extUrls.length; i++) {
            System.out.println(extUrls[i]);
        }


        System.out.println("appClassLoader引用程序类加载器加载文件:");
        String appPaths = System.getProperty("java.class.path");
        String[] appUrls = Optional.ofNullable(appPaths)
                .map(s -> appPaths.split(";"))
                .orElse(new String[0]);
        for (int i = 0; i < appUrls.length; i++) {
            System.out.println(appUrls[i]);
        }
    }
}

运行结果
bootstrapClassLoader引导类加载加载的文件:
file:/D:/env/jdk8/jre/lib/resources.jar
file:/D:/env/jdk8/jre/lib/rt.jar
file:/D:/env/jdk8/jre/lib/sunrsasign.jar
file:/D:/env/jdk8/jre/lib/jsse.jar
file:/D:/env/jdk8/jre/lib/jce.jar
file:/D:/env/jdk8/jre/lib/charsets.jar
file:/D:/env/jdk8/jre/lib/jfr.jar
file:/D:/env/jdk8/jre/classes
extClassLoader扩展类加载器加载文件:
D:\env\jdk8\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
appClassLoader引用程序类加载器加载文件:
D:\env\jdk8\jre\lib\charsets.jar
D:\env\jdk8\jre\lib\deploy.jar
D:\env\jdk8\jre\lib\ext\access-bridge-64.jar
D:\env\jdk8\jre\lib\ext\cldrdata.jar
D:\env\jdk8\jre\lib\ext\dnsns.jar
D:\env\jdk8\jre\lib\ext\jaccess.jar
D:\env\jdk8\jre\lib\ext\jfxrt.jar
D:\env\jdk8\jre\lib\ext\localedata.jar
D:\env\jdk8\jre\lib\ext\nashorn.jar
D:\env\jdk8\jre\lib\ext\sunec.jar
D:\env\jdk8\jre\lib\ext\sunjce_provider.jar
D:\env\jdk8\jre\lib\ext\sunmscapi.jar
D:\env\jdk8\jre\lib\ext\sunpkcs11.jar
D:\env\jdk8\jre\lib\ext\zipfs.jar
D:\env\jdk8\jre\lib\javaws.jar
D:\env\jdk8\jre\lib\jce.jar
D:\env\jdk8\jre\lib\jfr.jar
D:\env\jdk8\jre\lib\jfxswt.jar
D:\env\jdk8\jre\lib\jsse.jar
D:\env\jdk8\jre\lib\management-agent.jar
D:\env\jdk8\jre\lib\plugin.jar
D:\env\jdk8\jre\lib\resources.jar
D:\env\jdk8\jre\lib\rt.jar
D:\ideaSpacep\study\sourceCode\jvm\target
D:\env\maven\mvn_repo\org\projectlombok\lombok\1.18.36\lombok-1.18.36.jar
D:\env\IntelliJ IDEA 2023.2\lib\idea_rt.jar

双亲委派

上个章节说应用程序类加载器加载的是自己写的代码,但是为什么会加载jre/lib目录下的jar包和jre/lib/ext下的jar包呢,其实这就是我们接下来要说的内容了,是双亲委派机制造成的,废话不多说,先上图,再解释。
双亲委派
这里类加载其实就有双亲委派机制,加载某个类的时候会先看是否已经加载,如果已经加载过,则直接调用,如果未加载,则向上委托,如果委托到引导类加载器也寻找不到目标类,则在自己的类加载路劲中寻找并载入目标类。
比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
简单点说,双亲委派机制就是现在父亲加载,不行再有儿子自己加载
有个问题,为什么不直接从引导类加载器加载,而要从最子类加载器加载,绕一圈呢?

  1. 个人理解,没有求证,我们的程序90%以上都是自己写的,我们程序第一次加载由于懒加载机制可能需要绕一圈才能加载到,但是后续在使用该类时,已经加载过直接调用就好,无需再次引导类加载器加载,提高性能
  2. 沙箱安全机制,加入我们自己写一个java.long.String类,由于一开始没有加载过,会向上委托,找到了该类,这样我们自己写的String类就不会被加载,可以防止核心类库被篡改
    代码演示:
package java.lang;

public class String {

    public static void main(String[] args) {
        System.out.println("********* My String Class ************");
    }
}

运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
  1. 避免类的重复加载:当父加载器加载了该类,子加载器就没必要在加载,保证被加载类的唯一性

全盘负责委托机制

全盘负责是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

类加载过程源码解读

类加载
左边是有c++实现,主要看右边的加载过程

一 laucnher类实例化

c++创建jvm启动器后,实例化launcher类,launcher.getLauncher()初始化其他类加载器,源码如下:

// launcher在加载时创建了launcher对象
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
   	return launcher;	
}

// launcher的构造方法
// 这段代码我们可以知道,
//1. 扩展类加载器的父加载器就是引导类加载器,此处为null是因为引导类加载器是c++创建的对象,在java中其实看不到,所以为null,
//2. 应用程序类加载器父加载器就是扩展类加载器
public Launcher() {
    ExtClassLoader var1;
    try {
    	// 在构造器中直接创建了扩展类加载器,并将他的父加载器设置为null
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 在构造器中直接创建应用程序类加载器,并将他的服务器设置为var1,var1为扩展类加载器,这个代码和extClassLoader实例化代码相似,可以自己翻一下
        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");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
            } catch (InstantiationException var6) {
            } catch (ClassNotFoundException var7) {
            } catch (ClassCastException var8) {
            }
        } else {
            var3 = new SecurityManager();
        }

        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }

        System.setSecurityManager(var3);
    }
	
}

// extClassLoader部分代码
static class ExtClassLoader extends URLClassLoader {
        private static volatile ExtClassLoader instance;

        public static ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = ExtClassLoader.class;
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                    	// 此处创建了extClassLoader
                        instance = createExtClassLoader();
                    }
                }
            }
            return instance;
        }
        // 创建extClassLoader
		private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
                    public ExtClassLoader run() throws IOException {
                        File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                        int var2 = var1.length;

                        for(int var3 = 0; var3 < var2; ++var3) {
                            MetaIndex.registerDirectory(var1[var3]);
                        }
						// 实例化extClassLoader
                        return new ExtClassLoader(var1);
                    }
                });
            } catch (PrivilegedActionException var1) {
                throw (IOException)var1.getException();
            }
        }
        // 并且指定父extClassLoader的附加载器为空
        public ExtClassLoader(File[] var1) throws IOException {
        	//这一行代码调用父类指定父加载器
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
        // 这段代码是supper点进去的,此时的parent为null
        public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }
	// 点到最后有一个为parent赋值的操作,parent父加载器为空
	private final ClassLoader parent;
	private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

二 加载器调用Math.class

此处就是调用了launcher.loadclass()和classLoader.loadclass(“com.guaiyu.jvm.Math”)加载类到内存的过程,也是双亲委派机制的源码实现,这一块的源码如下:
在Launcher类中,ctrl+alt+u,可以看到类结构如下
launcher类结构

// launcher.loadclass方法,最终调用到父类的loadclass
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
			// 安全校验代码,前面的可以不看
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

            if (this.ucp.knownToNotExist(var1)) {
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }

                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
            	// 调用父类的loadclass方法,var1表示加载的类名:com.guaiyu.jvm.Math
                return super.loadClass(var1, var2);
            }
        }
		// 父类(ClassLoader类)的loadclass源码
		protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 此处表示先去是否已经加载了该类,该方法是本地方法调用了,是C++的代码了,我们不需要管,native代表本地方法的意思,下面注释的代码是Class<?> c = findLoadedClass(name);点进去的代码
            /**
            protected final Class<?> findLoadedClass(String name) {
		        if (!checkName(name))
		            return null;
		        return findLoadedClass0(name);
		    }
		
		    private native final Class<?> findLoadedClass0(String name);
            */
            Class<?> c = findLoadedClass(name);
            // 如果加载了,直接返回该类,如果没加载执行下面的方法
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	// 此时是appClassLoader加载器,所以父加载器extClassLoader加载器
                    if (parent != null) {
                    	// 调用extClassLoader.loadClass()方法,其实最终调用到父类的该方法,这里可以理解成一个递归调用,首先也会调用:
                    	// Class<?> c = findLoadedClass(name); 由于c未null,又会判断parent是否为空,
                        c = parent.loadClass(name, false);
                    } else {
                    	// 因为extClassLoader的parent为空,所以调用引导类加载器去调用,此处也印证了扩展类加载器的父加载器是引导类加载
                    	// 此处其实也是调用到本地方法到C++了,感兴趣的可以点击查看,这块也找不到com.guaiyu.jvm.Math
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				// 代码执行到此处,c为空
                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;
        }
    }
	// URLClassLoader.findClass("com.guaiyu.jvm.Math")寻找类的方法
	protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                    	// 此处相当于吧com.guaiyu.jvm.Math转换成com/guaiyu/jvm/Math.class到磁盘中target目录下寻找该十六进制文件
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	// 此处就是类加载的代码了(加载->验证->准备->解析->初始化的过程了),这里就不接着看了
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

自定义类加载器

上述我们看到了有自定义类加载器,自定义类加载器其实就是继承java.lang.ClassLoader类,通过上述源码我们可以知道其实主要重写loadclass(String,Boolean)(实现双亲委派机制)和findclass()方法,loadclass()直接调用父类就好,默认实现空方法,我们只需要重写findclass确保能找到我们的目标类就好

**前置条件:**我模拟类加载器,将我target中的User.class文件拷贝到D:/test/com/guaiyu/jvm/User.class,然后为了区别效果,我在User中写了一个打印方法,打印一句话:应用类加载器/自定义类加载调用方法,看输出内容
自定义类加载器

// 我们项目中的user.java源码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Integer id;

    private String nickName;

    public void print(){
        System.out.println("***********应用程序类加载器加载类调用方法*************");
    }
}

// D:/test/com/guaiyu/jvm/User.class的源码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Integer id;

    private String nickName;

    public void print(){
        System.out.println("***********自定义类加载器加载类调用方法*************");
    }
}

// 自定义类加载器实现
public class MyLoadClassTest {

    // 继承classloader,重写findClass方法
    static class MyClassLoader extends ClassLoader{

        private String classPath;

        public MyClassLoader(String classPath){
            this.classPath = classPath;
        }

        protected Class<?> findClass(final String name)
                throws ClassNotFoundException
        {
            try {
                // findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
                byte[] data = loadByte(name);
                return defineClass(name,data,0,data.length);
            } catch (Exception e) {
                throw new ClassNotFoundException();
            }
        }

        private byte[] loadByte(String name) throws UnsupportedEncodingException {
            // 转化类路径
            name = name.replaceAll("\\.", "/");
            // 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
           FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User");
        Object obj = clazz.newInstance();
        // 利用发射机制获取类方法
        Method method = clazz.getDeclaredMethod("print", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}
运行结果:
***********应用程序类加载器加载类调用方法*************
sun.misc.Launcher$AppClassLoader

我们可以看到,自定义类加载器并没有被执行,这个进一步验证了我们双亲委派机制,因为我们在应用类加载器里面有个同样的com.guaiyu.jvm.User.class,直接加载应用程序类里的类了,也证明了自定义类加载器的父加载器是应用程序类加载器
那我们现在要想加载自定义加载器的类,我们把自定义类加载器的类名称改成User1,将编译文件放入D:test/com/guaiyu/jvm/User1.class
自定义类
重新修改main方法,代码如下:

// 修改类名User -> User1
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User1 {

    private Integer id;

    private String nickName;

    public void print(){
        System.out.println("***********自定义类加载器加载类调用方法*************");
    }
}

// 修改main()
public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
        Object obj = clazz.newInstance();
        // 利用发射机制获取类方法
        Method method = clazz.getDeclaredMethod("print", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
}
运行结果:
***********自定义类加载器加载类调用方法*************
com.guaiyu.jvm.MyLoadClassTest$MyClassLoader

自定义类加载器验证完毕了!!!!

打破双亲委派机制

现在我们尝试打破双亲委派机制,这个时候我们在加载类的时候不交给父加载器,直接调用自己写的findClass方法,所以在上述代码中我们还需要重写loadClass()

public class MyLoadClassTest {

    // 继承classloader,重写findClass方法
    static class MyClassLoader extends ClassLoader{

        private String classPath;

        public MyClassLoader(String classPath){
            this.classPath = classPath;
        }

        protected Class<?> findClass(final String name)
                throws ClassNotFoundException
        {
            try {
                // findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
                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("\\.", "/");
            // 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
		// 重写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();
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        // 不委托父加载器去加载,而是直接调用重写的findClass去找目标列
                        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 static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
        Object obj = clazz.newInstance();
        // 利用发射机制获取类方法
        Method method = clazz.getDeclaredMethod("print", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
    运行结果:
    Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.findClass(MyLoadClassTest.java:25)
	at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.loadClass(MyLoadClassTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at com.guaiyu.jvm.MyLoadClassTest.main(MyLoadClassTest.java:73)
Caused by: java.lang.ClassNotFoundException
	at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.findClass(MyLoadClassTest.java:27)
	at com.guaiyu.jvm.MyLoadClassTest$MyClassLoader.loadClass(MyLoadClassTest.java:55)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 7 more

可以看到在调用的时候会依赖到好多类,我们不可能一个一个去复制class文件到我们自己的类加载器,所以我们需要动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托,而且有些是核心类库,也不允许我们修改
所以我们要修改loadClass方法,修改代码如下:

public class MyLoadClassTest {

    // 继承classloader,重写findClass方法
    static class MyClassLoader extends ClassLoader{

        private String classPath;

        public MyClassLoader(String classPath){
            this.classPath = classPath;
        }

        protected Class<?> findClass(final String name)
                throws ClassNotFoundException
        {
            try {
                // findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
                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("\\.", "/");
            // 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
            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<?> 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();
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        // 动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托
                        if (name.equals("com.guaiyu.jvm.User1")){
                            c = this.findClass(name);
                        }else {
                            c = super.loadClass(name,false);
                        }
                        // 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 static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class<?> clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
        Object obj = clazz.newInstance();
        // 利用发射机制获取类方法
        Method method = clazz.getDeclaredMethod("print", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}
运行结果:
***********自定义类加载器加载类调用方法*************
com.guaiyu.jvm.MyLoadClassTest$MyClassLoader

假设我们现在修改核心类库,java.lang.String,看是否可以打破双亲委派机制
我们把上面写的java.long.String类复制到我们D:/test/java/long/String.class,代码如下:

package com.guaiyu.jvm;

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

public class MyLoadClassTest {

    // 继承classloader,重写findClass方法
    static class MyClassLoader extends ClassLoader{

        private String classPath;

        public MyClassLoader(String classPath){
            this.classPath = classPath;
        }

        protected Class<?> findClass(final String name)
                throws ClassNotFoundException
        {
            try {
                // findClass核心代码转化为类路劲转化,然后执行defineClass方法加载类
                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("\\.", "/");
            // 找到类的全路径 D:/test/com/guaiyu/jvm/User.class
            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<?> 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();
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        // 动态判断类路径,是自己写的类不向上上委托,如果是其他类则向上委托
                        if (name.equals("java.long.String")){
                            c = this.findClass(name);
                        }else {
                            c = super.loadClass(name,false);
                        }
                        // 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 static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class<?> clazz = classLoader.loadClass("java.long.String");
        Object obj = clazz.newInstance();
        // 利用发射机制获取类方法
        Method method = clazz.getDeclaredMethod("print", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}
运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:758)

所以对于核心类库,是不允许打破双亲委派机制的!!!

tomcat打破双亲委派机制

以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
Tomcat自定义加载器详解
在这里插入图片描述
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版本;

从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

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

模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离
依照上面自定义加载器,我们在创建一个D:/test1/com/guaiyu/jvm/User1.class

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);
                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("com.guaiyu.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 {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.guaiyu.jvm.User1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());
        
        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/test1");
        Class clazz1 = classLoader1.loadClass("com.guaiyu.jvm.User1");
        Object obj1 = clazz1.newInstance();
        Method method1= clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }
}

运行结果:
=======自己的加载器加载类调用方法=======
com.guaiyu.jvm.MyClassLoaderTest$MyClassLoader@266474c2

=======另外一个User1版本:自己的加载器加载类调用方法=======
com.guaiyu.jvm.MyClassLoaderTest$MyClassLoader@66d3c617

注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

模拟实现Tomcat的JasperLoader热加载
原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐