JVM-1.从JDK源码级别剖析JVM类加载机制

学习内容重点:

1、Java类加载运行全过程

2、从JDK源码级别剖析JVM核心类加载器

3、从JDK源码级别剖析类加载双亲委派机制

4、手写自定义类加载器打破双亲委派机制

5、Tomcat类加载机制深度剖析

6、手写Tomcat类加载器实现多版本代码共存隔离

1、类加载运行全过程

用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

package com.tl.jvm;
public class Math {

  public static final int initData = 888;
  public static User user = new User();
  public int compute() {  
    int a = 1;
    int b = 2;
    int c = (a + b) * 10;
    return c;
  }

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

执行javap命令可以看到class文件里的信息

javap -v Math.class

JVM-1.从JDK源码级别剖析JVM类加载机制_第1张图片

1.Java命令执行代码的大体流程

JVM-1.从JDK源码级别剖析JVM类加载机制_第2张图片

2.其中loadClass的类加载过程有如下几步

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

JVM-1.从JDK源码级别剖析JVM类加载机制_第3张图片

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

类加载器的引用:这个类到类加载器实例的引用

对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。

jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

package com.tl.jvm;
public class TestDynamicLoad {

  static {
    System.out.println("*************load TestDynamicLoad************");
  }

  public static void main(String[] args) {
    new A();
    System.out.println("*************load test************");
    B b = null;  //B不会加载,除非这里执行 new B()
    //B b = new B();  
  }
}

class A {
  static {
    System.out.println("*************load A************");
  }

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

class B {
  static {
    System.out.println("*************load B************");
  }

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

执行结果:

JVM-1.从JDK源码级别剖析JVM类加载机制_第4张图片

3.双亲委派机制

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

JVM-1.从JDK源码级别剖析JVM类加载机制_第5张图片

看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        //( 1.检查当前类加载器是否已经加载了该类如果加载过了,就不需要再加载,直接返回。)
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
//2.如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
                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();
                //如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的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;
    }
}

备注:一些关键的方法debug到最后都是执行的底层方法,带有native关键字,如果想深入了解,需要获取Hotspot源码看

自定义类加载器示例:

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

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

    }

    public static void main(String args[]) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //D盘创建 test/com/tl/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class clazz = classLoader.loadClass("com.tl.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("test1", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

在对应的电脑盘中建好相应的目录:

JVM-1.从JDK源码级别剖析JVM类加载机制_第6张图片

JVM-1.从JDK源码级别剖析JVM类加载机制_第7张图片

JVM-1.从JDK源码级别剖析JVM类加载机制_第8张图片

4.打破双亲委派机制

再来一个沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的类或者改写String类

 在上面的代码中新增一个方法:

  /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载,实际就是去掉中间的判断部分
         * @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.tl.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;
            }
        }

JVM-1.从JDK源码级别剖析JVM类加载机制_第9张图片

package java.lang;

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

5.Tomcat打破双亲委派机制

tomcat 为了实现隔离性,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

6.Tomcat自定义加载器详解

JVM-1.从JDK源码级别剖析JVM类加载机制_第10张图片

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

//在上面代码的基础上修改main方法

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.tl.jvm.User1");
        Object obj = clazz.newInstance();
        Method method= clazz.getDeclaredMethod("test1", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());
        
        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/test2");
        Class clazz1 = classLoader1.loadClass("com.tl.jvm.User1");
        Object obj1 = clazz1.newInstance();
        Method method1= clazz1.getDeclaredMethod("test1", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }

 

运行结果:

JVM-1.从JDK源码级别剖析JVM类加载机制_第11张图片

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

模拟实现Tomcat的JasperLoader热加载

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

#未完待续

你可能感兴趣的:(Java架构学习之旅,jvm,java,开发语言)