【JVM】类加载器及其命名空间详解(源码分析+多问题解答)

1.类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象( 规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构

2.类加载器的种类

启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
应用(系统)类加载器,Application ClassLoader,加载ClassPath中的类库
自定义类加载器,通过继承ClassLoader实现,由用户自定义实现

3.双亲委派模型

在这里插入图片描述
双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的根加载器中),如果父类加载器无法完成这个加载(对应路径无法找到该文件),子类尝试自己加载。
要注意的是,这里说的父子关系并不是继承关系而是子类中保存有父类的对象,这更像包含关系,所以可以每次去向上抛。
部分源码示例:

 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) {//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;
        }
    }

而且除了根加载器外,每个CLassLoader都有一个parent的成员:

 // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;//保存该加载器的父

值得一提的是findClass(String name)这个方法,源码文档中给出描述;

<p> The network class loader subclass must define the methods {@link
 * #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
 * from the network.  Once it has downloaded the bytes that make up the class,
 * it should use the method {@link #defineClass <tt>defineClass</tt>} to
 * create a class instance.  A sample implementation is:
 *
 * <blockquote><pre>
 *     class NetworkClassLoader extends ClassLoader {
 *         String host;
 *         int port;
 *
 *         public Class findClass(String name) {
 *             byte[] b = loadClassData(name);
 *             return defineClass(name, b, 0, b.length);
 *         }
 *
 *         private byte[] loadClassData(String name) {
 *             // load the class data from the connection
 *             &nbsp;.&nbsp;.&nbsp;.
 *         }
 *     }

我们知道所加载的内容不一定是通过编译得到的二进制信息,当通过网络来获取二进制信息时就得从写这个findclass方法,通过调用自定义loadClassData(String name)方法,将数据转化成字节数组,在通过调用ClassLoader自带的defineClass方法得到最终的Class对象。在编写用户自定义加载器时也要去覆盖这个findClass方法

Class对象:

通过上面部分源码可以看出,类加载的最终产物是位于内存中的Class对象,
Class对象封装了类在方法区的数据结构,并提供了访问方法区内数据结构的接口。

定义类加载器与初始类加载器

若有一个类加载器能够成功加载指定的类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。
例如:通过自定义加载器去在加载一个文件位于classpath路径下的类,由于双亲委派机制的影响,该类其实是被Appclassloader加载的,所以Appclassloader就被称为定义类加载器,而用户自定义加载器和Appclassloader加载器都可以被称为初始类加载器;(用户自定义加载器可以返回Class对象的引用是因为其保存着父类的对象,故可以访问到父的命名空间得到该引用)

4.类加载器的命名空间

这是类加载器所有知识中最重要的知识之一,一个类只会被加载一次这是真的吗?
这个问题很容易被测试出结果,大家可以自定义两个类加载器,然后用这两个自定义的加载器去加载同一个类(这个类一定不能位于classpath下,不然由于双亲委派机制的影响,将都会由Appclassloder加载),分别输出两个加载后的Class对象,你会发现这两个Class对象是不同的,接下来再分别去用这两个Class对象通过newInstance产生两个实例对象,这两个实例对象是无法相互调用的。无法调用这一点最好的测试方法就是给被加载的类中添加一个参数为本类类型的方法,然后将其中一个实例对象作为调用者另一个实例对象作为参数,接下来会出现明明是同一个类的对象,但是却无法转换的异常。
然后尝试在用同一个加载器去加载两次这个类,发现得到的class对象是同一个,甚至你可以通过覆盖的findclass方法做标记,会发现这个标记只会被输出一次,第二次就不会输出,这一点通过阅读上面的load方法源码很容易得出。
这就初步证明了,在同一个类加载器中一个类会被加载一次,但这个类可被不同加载器加载多次。由此就引出了命名空间的概念:
每个类加载器都有自己的命名空间,
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

通过一个例子来加深对命名空间的理解:

public class Dog {
	
	public Dog() {
		System.out.println("DogClass : " + this.getClass().getClassLoader());
		new Cat();
		System.out.println("getCat : " + Cat.class);
	}
}

public class Cat {

	public Cat() {
		System.out.println("CatClass : " + this.getClass().getClassLoader());
		System.out.println("getDog : " + Dog.class);
	}
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
		//启动1个自定义加载器
		Classload loader1 = new Classload("loader1");
		Class<?> clazz = loader1.load("com.mec.classloader.Dog");
		Object object1 = clazz.newInstance();//实例化对象,就是主动使用,会导致该类初始化
	
		
	}
	}

首先要说的是,当一个类被类加载器a加载时,他的所有依附类都会去尝试用加载器a去加载。
不同条件下的运行结果:

情况1

Dog类Class文件和Cat类Class文件在Classpath中,
按照代码输出的结果为:
DogClass :AppClassloader
CatClass : AppClassloader
getDog : class com.mec.classloader.Dog
getCat : class com.mec.classloader.Cat
这个很容易理解,因为双亲委派机制的影响,最终都是由其父系统类加载器来加载。

情况2

Dog和Cat文件都从ClassPath中移动到桌面;
DogClass :com.mec.classloader.ClassLoader
CatClass :com.mec.classloader.ClassLoader
getDog : class com.mec.classloader.Dog
getCat : class com.mec.classloader.Cat
两个输出结果都是用户自定义加载器,因为从上到下的其他加载器都加载不了这个类,所以最终由用户自定义加载器来加载;

情况3

Dog的文件从Classpath中移动到桌面,Cat类文件仍保持在Classpath不变;
按照代码输出的结果为:

DogClass : com.mec.classloader.ClassLoader
CatClass : AppClassloader
异常 : ClassNotFound;

Dog被自定义加载器加载,也是因为将文件移动到桌面,父AppClassloader在Classpath在其路径下找不到文件最终只能由自定义加载器来加载Dog。
由于在Dog类中实例化了Cat对象,相当于对Cat主动使用,就会导致其被初始化,那一定得先被加载,所以会由加载Dog的自定义类加载器去尝试加载Cat类,但由于双亲委派机制的影响,Classpath下有文件,那其父就AppClassloader就可以加载。然后为什么会出异常呢?也就是说System.out.println("getDog : " + Dog.class);这句代码产生异常
即从Cat类中访问Dog类的Class对象访问不到,但是明明Dog类先被加载并产生出Class对象,为啥会找不到呢?
先不急,先把代码中System.out.println("getDog : " + Dog.class);这句注释掉
在看看输出结果:

DogClass : com.mec.classloader.ClassLoader
CatClass : AppClassloader
getCat : class com.mec.classloader.Cat

异常消失了,这里大家会问,为啥Dog能得到Cat的class对象,而反过来Cat却得不到Dog的class对象。

原因是:Cat类由AppClassloader加载
AppClassloader加载器根本就没有加载过Dog类,其命名空间中自然不会保存这个Class对象。所以自然访问不到这个Class对象从而抛异常。
Dog类由自定义加载器加载,而自定义加载器类里就保存有其父AppClassloader的对象,也就是上面源码里面的parent成员,他可以通过该父对象去访问其父的命名空间,也就能得到Cat的Class对象
子可以访问到父的命名空间,因为子中有父对象
而父无法访问子的命名空间,父中并没有子的对象
两个没有任何关系的加载器无法互相访问命名空间

其实从父的角度来看,他根本不知道以后会有那些加载器作为他的子,这对他来说是未来的事情,那他自然不可能去访问一个未来虚无缥缈的一个子。

这是父子命名空间的一个例子,有兴趣的同学可以去尝试平级两个不同的自定义加载器的命名空间问题,看看是不是和我最开始的那个引子结果相同。

情况4

Cat的文件从Classpath中移动到桌面,Dog类文件仍保持在Classpath不变;
按照代码输出的结果为:

DogClass : AppClassloader
异常 : ClassNotFound;

Cat加载出现异常,这是因为Dog类文件仍保持在Classpath中,由于双亲委派他会被AppClassloader去加载,然后AppClassloader又会尝试去加载Cat类,而Cat类的文件在Classpath中找不到,异常了。

在理解了命名空间的概念后,大家也就间接的熟悉双亲委派机制的原理,那么一个问题就产生了,为什么一定要用双亲委派机制?他的优点是什么?

5.双亲委派机制的优点

我们妨先考虑下要是没有双亲委派机制会出现什么问题?
以Obeject类为例,java.lang.object位于核心库中本是由根加载去加载,若没有双亲委派机制,那就可以在自定义加载器可加载的路径中写一个java.lang.object类,不管这个类是不是和真正的java.lang.object完全一样,但是在Object类此时肯定会被不同的加载器加载多次,由于每个加载器的命名空间不同,且其之间没有任何关系,所以当用Object对象时就会出现明明都是Object对象,但是却无法相互使用,即表面一致,本质各是各的。但要是有双亲委派机制,即使自定义类中有java.lang.object这个类,当他要加载这个类时由于双亲委派机制的影响最终由根加载器去加载,所以就不会有问题。
双亲委托机制的优点是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如,javalang .Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java lang .Object类
其实双亲委派机制所允许的毫无关系的不同加载器之间命名空间相互独立是一个很好扩展前提。
不同的类加载可以为相同名称的类创建额外空间,相同类可用不同加载器加载
这相当于在Jvm中创建了一个又一个相互隔离的空间,在许多框架中有实际应用

6.类加载器被加载

相信大家都有一个疑问,类加载器负责去加载其他类,那类加载器的类又是被谁加载的呢?
答:内建于JVM中的启动类加载器会加载java. lang.ClassLoader以及其他的Java平台类,
当JVM启动时。一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,
这块特殊的机器码叫做启动类加载器(Bootstrap,也称为根加载器) .
启动类加载器并不是Java类,而其他的加载器则都是Java类,
启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。
所有类加载器(除了启动类加载器)。都被实现为Java类.不过,总归要有一个组件来加载第一个Java类加截器, 从而让整个加载过程能够顺利
进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。
启动类加载器还会负责加载供 JRE 正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等。

7.数组类型的加载

对于数组实例来说,其类型是由JVM运行期动态生成的,并不是由类加载器加载的,但数组的元素是要被类加载器加载,且数组实例被getClassloader得到的是器元素对应的类加载器,如果数组类型是原生类型,则为getClassloader得到为null
所以可视为实际上就是将数组降低一个维度后的类型去处理即可。

8.获得ClassLoader的途径

获得当前类的ClassLoader
clazz.getClassLoader();
获得当前线程.上下文的ClassLoader
Thread.currentThread).getContextClassLoader()
获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
获得调用者的ClassLoader
DriverManager.getCallerClassLoader()

clazz.getClassLoader();这个是最常用的获取类加载器的方法

ClassLoader.getSystemClassLoader()

获得系统类加载器也就是AppClassloader,JDK提供了一个修改默认的系统类加载器的手段,通过给java.system.class.loader赋值来使用户自定义的加载器作为系统类加载器,该自定义加载器必须得有一个参数为Classloder类型的构造方法,且默认的系统类加载器作为该加载器的父。
部分源码分析:

public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();//安全检查
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {//给系统类加载器赋值
        if (!sclSet) {//sclSet 本类的一个布尔类型成员,用于表示已经设置了系统类加载器
            if (scl != null)//scl 是系统类加载器的实例
                throw new IllegalStateException("recursive invocation");//若并没有设置系统类加载器可是scl实例不为null
                //则证明出异常
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();//证明没有设置系统类加载器,调用Launcher的getgetLauncher方法
           /*
                Launcher类,里有内部类appclassloder和扩展classlodor,还有一个本类的成员 就是Launcher自身
                而调用getgetLauncher方法就会返回该就是Launcher成员,该成员会进行一次Launcher的初始化
                执行构造,而Launcher构造里就是初始化扩展类和默认的系统类加载器,并把默认的系统类加载器赋值给本类的Classloder对象
           */
            if (l != null) {//l不为空则证明上面创建成功了
                Throwable oops = null;
                scl = l.getClassLoader();//取得里面的appClassloder对象给scl
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));//调用底下内部类方法判断是直接使用默认app加载器还是自定义的app加载器
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }

 // The class loader for the system
    // @GuardedBy("ClassLoader.class")
    private static ClassLoader scl;

    // Set to true once the system class loader has been set//一旦设置了系统类装入器,就将其设置为true
    // @GuardedBy("ClassLoader.class")
    private static boolean sclSet;
class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) {//把上面得到的appclaseloader传过来
        this.parent = parent;
    }

    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");//判断"java.system.class.loader"被设置了么
        if (cls == null) {//没有设置则就返回的默认系统类加载器
            return parent;
        }
			//设置了就会用自定义加载器作为系统类加载器
        Constructor<?> ctor = Class.forName(cls, true, parent)
			.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
			 /*若将自定义加载器作为app加载器,则就执行自定义加载器的一个构造,这里解释了为什么想要把一个自定义加载起作为app加载器就必须给一个参数为classloder类型的构造方法,目的是将默认的系统app加载器作为自定义app加载器其父亲
			 */
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });//执行,并将默认系统加载器作为其父
        Thread.currentThread().setContextClassLoader(sys);//将sys设置进线程上下文类加载器
        return sys;
        /* 顺便提一下Class.forName的使用
      1. Class.forName(类名字, 是否初始化该类, 由哪个类加载器去加载该类)其本质是调用了本地方法forName0(类名,是否初始,由谁加载,调用该方法的Class对象)
      2. Class.forName(类名字)本质是调用了本地方法forName0(类名,是否初始,获取调用该方法的class对象的类加载器,调用该方法的Class对象),经常在JDBC使用
       */
    }

获得当前线程上下文的ClassLoader
Thread.currentThread).getContextClassLoader()
线程上下文加载器是重点之一,之后会拿出一篇博文去描述。

9.Clssforname和Classloader.lodeclass的区别

理论分析:
ClassLoader.laodClass(“className”);其实这种方法调运的是ClassLoader.loadClass(name, false)方法: 上面就有源码
他只会把该类的Class文件加载到JVM中,并把加载完得到的Class对象放入加载器的命名空间,在内存中方法区保留该类的元数据。

Class.forName

 public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();//获取调用者的类
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);//获取调用者的类的类加载器
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }
  1. Class.forName(类名字, 是否初始化该类, 由哪个类加载器去加载该类)其本质是调用了本地方法forName0(类名,是否初始,由谁加载,调用该方法的Class对象)
    2. Class.forName(类名字)本质是调用了本地方法forName0(类名,确定初始,获取调用该方法的class对象的类加载器,调用该方法的Class对象),经常在JDBC使用

    所以我们经常反射用的Class.forName(类名字);该方法不仅会加载该类还会导致该类连接并且初始化,为静态变量赋值(从我的另有一篇反编译字节码文件博文可以看出来,给静态变量赋值本质上调用的是Clint方法,Clint方法会把静态成员赋值,和静态块的代码一起运行了)所以还会执行静态块的代码。

static {  
        try {  
            java.sql.DriverManager.registerDriver(new Driver());  
        } catch (SQLException E) {  
            throw new RuntimeException("Can't register driver!");  
        }  
    }

mysql用classforname就会导致初始化,然后执行静态代码块里的内容!

至此,关于类的编译,加载,连接,初始化,实例化,回收,卸载过程中的加载这一部分就详细说完了,学完类加载器后最大的感受就是想问题的时候一定一定要结合类的上面流程,加上字节码文件,和执行时该线程被分到的栈和JVm运行时内存来考虑,保证把问题从根源分析清楚,比较佩服设计者,他们设计了双亲委派模型,但是又能想到双亲委派模型的弊端,然后通过线程上下文加载器去到打破这种模型,分析问题的眼界十分长远,线程上下文类加载器会在接下来的博文中介绍。

今天再次受教,明白了学习能力要比知识本身更重要,知识一直在更新,只有较强的学习能力才能应对不断升华的知识!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM的类加载是由类加载器及其子类实现的。类加载器是Java运行时系统的重要组成部分,负责在运行时查找和加载类文件中的类。在JVM中,类加载器按照一定的层次结构进行组织,每个类加载器负责加载特定位置的类。其中,启动类加载器(Bootstrap ClassLoader)是负责加载存放在<JAVA_HOME>/lib目录中的核心类库,如rt.jar、resources.jar等,同时也可以加载通过-Xbootclasspath参数指定的路径中的类库。启动类加载器是用C语言编写的,随着JVM启动而加载。当JVM需要使用某个类时,它会通过类加载器查找并加载这个类。加载过程会经历连接阶段,包括验证、准备和解析。在验证阶段,JVM会确保加载的类信息符合JVM规范。在准备阶段,JVM会为类变量分配内存并设置初始值,在方法区中分配这些内存。在解析阶段,JVM会根据符号引用替换为直接引用,以便后续的使用。通过类加载器的协同工作,JVM能够在运行时动态加载类,从而实现Java的灵活性和跨平台性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [JVM 的类加载原理](https://blog.csdn.net/ChineseSoftware/article/details/119212339)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [JVM类加载器](https://blog.csdn.net/rockvine/article/details/124825354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值