从JDK源码级别彻底剖析 java类加载机制

首选上一张图,大致了解类加载的大体流程

从JDK源码级别彻底剖析 java类加载机制_第1张图片

流程说明:

上图 通过java命令运行一个Math类 ,首先java.exe 会调用底层的jvm.dll文件创建java虚拟机(c++实现),然后虚拟机会创建一个引导类加载器。。。。

类加载过程(loadClass)主要分为以下几步:

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

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

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

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

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

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

注:

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

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

3.主类在运行过程中如果使用到其它类,会逐步加载这些类,jar包或war包里的类不是一次性全部加载的,是使用到时(new对象 或 调用main函数)才加载。

类加载器双亲委派机制

1.类加载过程主要通过如下几种类加载器来完成:

1)引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等

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

3)应用程序类加载器:负责加载classpath路径下的类包,主要是加载自己写的那些类;

4)自定义加载器:负责加载用户自定义路径下的类包;

以下,来看一段加载器的示例代码:

package com.provider.ClassLoadMachine;

import com.sun.crypto.provider.DESKeyFactory;
import sun.misc.Launcher;

import java.net.URL;

public class TestClassLoad {
    public static void main(String[] args) {
        //引导类加载器不是JAVA对象 是C++生成的对象 所以再JVAV这边表现不出来
        System.out.println("核心类加载器:"+String.class.getClassLoader());
        //自己写的类由appClassLoader加载
        System.out.println("自己写的类使用的加载器:"+TestClassLoad.class.getClassLoader());
        //使用扩展类加载器
        System.out.println("扩展类包下的加载器:"+DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println();

        //AppClassloader 由Classloader里的静态方法由getSystemClassLoader进行获取
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        //appClassLoader的父类时ExtClassLoader
        ClassLoader extClassLoader = appClassLoader.getParent();
        //ExtClassLoader的父类是BootstrapClassLoader
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("appClassLoader:"+appClassLoader);
        System.out.println("extClassLoader:"+extClassLoader);
        System.out.println("bootstrapClassLoader:"+bootstrapClassLoader);
        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for(Object url : urls){
            System.out.println(url);
        }

        //extClassLoader加载以下文件:
        System.out.println();
        System.out.println("extClassLoader加载以下文件");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件");
        System.out.println("java.class.path");
    }
}

执行结果如下:

核心类加载器:null
自己写的类使用的加载器:sun.misc.Launcher$AppClassLoader@58644d46
扩展类包下的加载器:sun.misc.Launcher$ExtClassLoader

appClassLoader:sun.misc.Launcher$AppClassLoader@58644d46
extClassLoader:sun.misc.Launcher$ExtClassLoader@533ddba
bootstrapClassLoader:null

bootstrapLoader加载以下文件:
file:/D:/JDK/jdk1.8.0_20/jre/lib/resources.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/rt.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/sunrsasign.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jsse.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jce.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/charsets.jar
file:/D:/JDK/jdk1.8.0_20/jre/lib/jfr.jar
file:/D:/JDK/jdk1.8.0_20/jre/classes

extClassLoader加载以下文件
D:\JDK\jdk1.8.0_20\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件
java.class.path
 

注:以上所有涉及到的应到类加载器打印都是为空,原因是应到类加载器是C++代码生成的对象所以再java这边打印出来为空

类加载器的初始化过程:

JVM启动时会创建JVM启动器实例sun.misc.Launcher.sun.misc.Launcher初始化使用了单例模式设计保证了一个JVM虚拟机内只有一个sun.misc.Launcher实例,在Launcher类的构造方法内,其创建了两个类加载器,分别是ExtClassLoader和AppClassLoader,JVM 默认使用Launcher类的getClassLoader来获取AppClassLoader的实例加载我们的应用程序自己的类:

从JDK源码级别彻底剖析 java类加载机制_第2张图片

 双亲委派机制:

下图是双亲委派机制的简单流程图

从JDK源码级别彻底剖析 java类加载机制_第3张图片

概念:当一个JAVA类需要加载时 它首先会委托给应用程序加载器,AppClassLoader会向上委托给ExtClassLoader加载,加载成功直接返回,如果加载失败扩展类加载器又会委托给应用程序类加载器加载,BootstrapClassLoader首先再自己核心目录下找目标类,加载失败则退回给应用程序加载器自己调用findClass自己进行加载。。。

沙箱安全机制:自定义加载器加载JDk的核心类式会跑SecurityException 沙箱安全机制

从AppClassLoader源码分析双亲委派机制的简单实现;

 从JDK源码级别彻底剖析 java类加载机制_第4张图片

 自定义加载器示例:

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

以下是自定义加载器的代码实现:

package com.provider.ClassLoadMachine;

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

/**
 *@description:自定义类加载器
 *@author: WangJiKui
 *@date: 2020/8/4 17:52
 */
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader{
        private String classPath;

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

        /**
         * 将硬盘中的class文件转换为字节数组
         * @param name
         * @return
         * @throws Exception
         */
        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;
        }

        /**
         * 重写findClass方法
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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) {
            try {
                //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader()
                MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu");
                //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致
                //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader
                //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了(双亲委派)
                Class arthas = myClassLoader.loadClass("com.provider.dao.Arthas");
                Object object = arthas.newInstance();
                Method[] declaredMethods = arthas.getDeclaredMethods();
                for (int i=0;i 
   

//打印结果

public static void com.provider.dao.Arthas.main(java.lang.String[])
private static void com.provider.dao.Arthas.lambda$cpuHigh$1()
public static void com.provider.dao.Arthas.addHashSetThread()
private static void com.provider.dao.Arthas.deadThread()
public static void com.provider.dao.Arthas.cpuHigh()
private static void com.provider.dao.Arthas.lambda$deadThread$3(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$deadThread$2(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$addHashSetThread$0()

*******************************************以上是被加载类中的方法******************************************************
此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$
MyClassLoader

打破双亲委派机制:就是类加载时不委托给父类加载器加载,直接由自己的加载器加载;

话不多说,直接上代码:(思路:就是在以上自定义加载器的基础上,再重写ClassLoader中的loaderClass()方法,进行相应的逻辑修改)

package com.provider.ClassLoadMachine;

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

/**
 *@description:自定义类加载器
 *@author: WangJiKui
 *@date: 2020/8/4 17:52
 */
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader{
        private String classPath;

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

        /**
         * 将硬盘中的class文件转换为字节数组
         * @param name
         * @return
         * @throws Exception
         */
        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;
        }

        /**
         * 重写findClass方法
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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();
            }
        }

        /**
         * 重写LoaderClass方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                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.provider.dao")){
                        //自己的类才打破双亲委派
                        c = findClass(name);
                    }else{
                        //否则依然走双亲委派逻辑(因为自定义加载器无法加载JDK的核心类--沙箱安全机制)
                        c = this.getParent().loadClass(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) {
            try {
                //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader()
                MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu");
                //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致
                //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader
                //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了
                Class arthas = myClassLoader.loadClass("com.provider.dao.Arthas");
                Object object = arthas.newInstance();
                Method[] declaredMethods = arthas.getDeclaredMethods();
                for (int i=0;i 
   

//打印结果

public static void com.provider.dao.Arthas.main(java.lang.String[])
private static void com.provider.dao.Arthas.deadThread()
private static void com.provider.dao.Arthas.lambda$cpuHigh$1()
public static void com.provider.dao.Arthas.cpuHigh()
public static void com.provider.dao.Arthas.addHashSetThread()
private static void com.provider.dao.Arthas.lambda$deadThread$2(java.lang.Object,java.lang.Object)
private static void com.provider.dao.Arthas.lambda$addHashSetThread$0()
private static void com.provider.dao.Arthas.lambda$deadThread$3(java.lang.Object,java.lang.Object)

----------------------此刻即使父类已经加载过了Arthas这个类,也不会使用,只由MyClassLoader自己去加载----------------------
此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader

Tomcat如何打破双亲委派机制:

从JDK源码级别彻底剖析 java类加载机制_第5张图片

Tomcat几个主要的类加载器:

commonClassLoader:Tomcat最基本的类加载器,加载路径中的class,可以被Tomcat容器以及各个webapp访问;

catalinaClassLoader:Tomcat容器私有类加载器,加载路径中的class对于webapp不可见

shareClassLoader:各个webapp共享的类加载器,加载路径中的class对于所有的Webapp可见,但对于Tomcat容器不可见

webAppClassLoader:各个webApp私有的类加载器,加载路径中的class只对当前的webapp可见,比如加载war包里相关类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同的war包应用引入不同的Spring版本,这样实现就能加载各自的spring版本

从上述流程图分析委派关系:

commonClassLoader能加载的类都可以被catalinaClassLoadershareClassLoader使用从而实现了共有类库的公用,而catalinaClassLoaderSharedClassLoader自己能加载的类则与对方隔离;

WebappClassLoader可以使用ShareClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件:当web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的jsp类加载器来实现JSP文件的热加载功能;

Tomcat这种类加载机制有没有违背双亲委派机制呢?

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

模拟Tomcat的webClassLoader加载自己war包应用内不同版本类实现共存与隔离:(其实就是,自己应用下的类通过自身的WebAppClassLoader去加载,核心类继续使用双亲委派机制(简单实现))

package com.provider.ClassLoadMachine;

import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.reflect.Method;
import java.util.logging.Logger;

/**
 *@description:自定义类加载器
 *@author: WangJiKui
 *@date: 2020/8/4 17:52
 */
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader{
        private String classPath;

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

        /**
         * 将硬盘中的class文件转换为字节数组
         * @param name
         * @return
         * @throws Exception
         */
        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;
        }

        /**
         * 重写findClass方法
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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();
            }
        }

        /**
         * 重写LoaderClass方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                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.provider.dao")){
                        //自己的类才打破双亲委派
                        c = findClass(name);
                    }else{
                        //否则依然走双亲委派逻辑(因为自定义加载器无法加载JDK的核心类--沙箱安全机制)
                        c = this.getParent().loadClass(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) {
            try {
                //初始化父类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器getSystemClassLoader()
                //模拟WebappClassLoader01
                MyClassLoader myClassLoader = new MyClassLoader("D:/testCpu");
                //这里的ClassName 是class文件中包加类的全路径名,单独建目录的时候,目录名也必须与class文件中的全包名一致
                //场景:此类是我从项目中copy到D盘新建的一个文件,当项目中的此文件为删除时,此时打印出的加载器依然是AppClassLoader
                //因为MyClassLoader会先委派父类去加载,恰好此类之前由AppClassLoader已经加载过了,所以直接返回了
                Class arthas = myClassLoader.loadClass("com.provider.dao.Arthas");
             /*   Object object = arthas.newInstance();
                Method[] declaredMethods = arthas.getDeclaredMethods();
                for (int i=0;i arthas01 = myClassLoader01.loadClass("com.provider.dao.Arthas");
                System.out.println("testCpu01使用的类加载器为:"+arthas01.getClassLoader());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

//打印结果l(两个类的全包名以及类名虽然完全相同,但类加载器不同,就可以区别为不同的类)

此时使用的类加载器是:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader@685f4c2e

testCpu01使用的类加载器为:com.provider.ClassLoadMachine.MyClassLoaderTest$MyClassLoader@2e5d6d97
l

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

。。。。。。。。文章或尚存在些许不足,欢迎留言指正。。。。。。。谢谢!!!

你可能感兴趣的:(JVM类加载机制,jvm)