JVM进阶之类加载器详解
Posted ProChick
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM进阶之类加载器详解相关的知识,希望对你有一定的参考价值。
类加载器详解
一、类加载器概述
1.作用
- 类加载器是JVM执行类加载机制的前提,它是Java的核心组件,所有的类都是由类加载器进行加载的。
- 类加载器负责通过各种方式将类信息的二进制数据流读入JVM内部,转换为一个与目标类对应的Class对象实例,然后交给Java虚拟机进行链接、初始化等操作。
- 类加载器最早出现在Java1.0版本中,那个时候只是单纯地为了满足Java Applet应用而被研发出来,但如今类加载器在OSGi、字节码加解密等领域也有很大作用。
2.分类
- 类的加载一般可分为显式加载和隐式加载两种,在日常开发两种方式一般会混合使用。
- 显式加载指的是在代码中通过调用ClassLoader加载来加载类对象,比如:通过
Class.forName()
或this.getClass().getClassLoader().loadClass()
方法加载类对象。 - 隐式加载则是不直接在代码中调用ClassLoader加载类对象,而是通过Java虚拟机自动加载到内存中,比如:在加载某个类时,该类中引用了另外一个类的对象,此时另外这个类将通过JVM自动加载到内存中。
3.重要性
- 只有了解类加载器的加载机制才能够在出现相关异常的时候,快速地根据错误异常日志定位问题和解决问题。
- 当需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要类加载器帮助了。
- 当开发人员想要实现一些自定义的处理逻辑时,可以在程序中编写自定义类加载器来重新定义类的加载规则。
4.命名空间
- 对于任意一个类,都需要由加载它的类加载器和这个类本身共同保证其在Java虚拟机中的唯一性。
- 每个类加载器都有自己的命名空间,命名空间由该类加载器以及父加载器所加载的类组成。
- 在同一命名空间中,不会出现两个具有完整类限定名相同的两个类。但是在不同的命名空间中,可以出现具有完整类限定名相同的多个类。
- 当比较两个类是否相等时,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这个类被同一个Java虚拟机所加载,但加载所使用的类加载器不同,那这两个类就必定不相等。
5.特性
- 可见性,子加载器可以访问父加载器加载的类型,但是反过来是不允许的。
- 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是对于不同的类加载器而言,同一类型可以被加载多次,因为它们之间互相隔离。
二、类加载器分类
1.基本概述
从JVM规范上来讲,类的加载器可以分为
引导类加载器(Bootstrap ClassLoader)
和自定义类加载器(User Defined ClassLoader)
两种。
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是在Java虚拟机规范中,将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
- 除了顶层的启动类加载器外,其余的类加载器都应当拥有自己的“父类”加载器,这里的拥有指的是包含关系,而不是继承关系。
2.引导类加载器
-
也叫作启动类加载器,是由C/C++语言编写的,嵌套在JVM内部。
-
通常用它来加载Java的核心类库,用于提供JVM自身需要的类,只加载包名为
java、javax、sun
等开头的类。 -
它没有父加载器,但会做为扩展类加载器和应用程序类加载器的父加载器,也就是程序启动时,它会加载这两个类加载器。
3.扩展类加载器
-
是由Java语言编写的,并通过
sun.misc.Launcher$ExtClassLoader
实现。 -
它继承于ClassLoader类,父类加载器是启动类加载器。
-
它主要加载从
java.ext.dirs
这个系统属性所指定的目录中加载类库,或者从JDK安装目录的jre/lib/ext
的子目录下加载类库。也就是说,如果用户创建的JAR文件也放在此目录下,那么就会自动由扩展类加载器进行加载。
4.系统类加载器
- 是由Java语言编写的,并通过
sun.misc.Launcher$AppClassLoader
实现。 - 它继承于ClassLoader类,父类加载器是扩展类加载器。
- 它是应用程序中的默认的类加载器,是用户自定义类加载器的父加载器。
- 它主要加载环境变量
classpath
或者系统属性java.class.path
指定路径下的类库。
5.自定义类加载器
- 在日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。不过在必要的时侯,我们还可以自定义类加载器,来执行一些特定的加载操作。
- 自定义类加载器通常需要继承于ClassLoader类。
- 开发者可以通过自定义类加载器来实现类库的动态加载,为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
- 开发者可以通过自定义类加载器来实现非常绝妙的插件机制,比如:著名的SGI组件框架、Eclipse的插件等。
- 开发者可以通过自定义类加载器来实现应用的隔离,比如:Tomcat、Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。
三、类加载器测试
1.获取类加载器方式
// 获得当前类的ClassLoader
clazz.getClassLoader()
// 获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
// 获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
2.获取类加载器示例
// 获取系统类加载器(能够获取)
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // 打印sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取扩展类加载器(能够获取)
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); // 打印sun.misc.Launcher$ExtClassLoader@1540e19d
// 获取引导类加载器(不能够获取,因为底层不是由Java语言实现的)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // 打印null
// 获取String类的类加载器(是引导类加载器)
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); // 打印null
// 获取自定义类的类加载器(是系统类加载器)
ClassLoader classLoader1 = Class.forName("com.me.MyClass").getClassLoader();
System.out.println(classLoader1); // 打印sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取MyClass类型数组的类加载器(是系统类加载器,与元素类型有关)
MyClass[] arr1 = new MyClass[10];
System.out.println(arr1.getClass().getClassLoader()); // 打印AppClassLoader@18b4aac2
// 获取String类型数组的类加载器(是引导类加载器,与元素类型有关)
String[] arr2 = new String[10];
System.out.println(arr2.getClass().getClassLoader()); // 打印null
// 获取基本数据类型数组的类加载器(不需要类加载器,因为基本数据类型是虚拟机预先定义的,不需要加载)
int[] arr3 = new int[10];
System.out.println(arr3.getClass().getClassLoader()); // 打印null
四、类加载器解析
1.ClassLoader关继承关系图
SecureClassLoader
扩展了ClassLoader,新增了对代码源的位置及其证书的验证方法和权限定义类的验证方法,一般我们不会直接跟这个类打交道。URLClassLoader
实现了ClassLoader,为findClass、findResource等方法提供了具体的实现,并且新增了URLClassPath类来协助取得Class字节码流等功能。- 拓展类加载器
ExtClassLoader
和系统类加载器AppClassLoader
都是sun.misc.Launcher的静态内部类,程序启动的时候,它们都是由sun.misc.Launcher创建的。
2.ClassLoader中的核心方法
-
getParent
public final classLoader getParent();
返回该类加载器的父加载器
-
loadClass
public Class<?> loadclass(String name) throws ClassNotFoundException
加载全类名为name的类,返回该类的实例。如果找不到此类,则抛出
classNotFoundException
异常。加载的过程使用了双亲委派机制,源码分析如下:
protected Class<?> loadClass(String name, boolean resolve){ // 由于类只能被加载一次,所以加上锁,避免多线程问题 synchronized (getClassLoadingLock(name)) { // 在缓存中查看是否已经加载此类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 判断是否存在父加载器 if (parent != null) { // 使用父加载器进行加载 c = parent.loadClass(name, false); } else { // 使用引导类加载器进行加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { ... } // 如果该类仍然没有被加载 if (c == null) { // 调用当前类加载器的findClass方法进行获取 long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 判断是否要对该类进行解析 if (resolve) { resolveClass(c); } return c; } }
-
findClass
protected Class<?> findclass(String name) throws ClassNotFoundException
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类的加载。
- 在JDK1.2之后,已不再建议用户去重写loadClass方法,而是建议把自定义的类加载逻辑写在findClass方法中。
- findClass方法是一个受保护的方法,所以需要子类去调用,所以自定义类加载器一定要重写该方法。
- findClass方法是在loadClass方法中被调用的,主要就是获取类的二进制数据,然后再通过defineClass方法生成类实例。
-
defineClass
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
该方法是用来将类的字节流解析成JVM能够识别的Class对象,主要是根据findClass方法传递过来的字节数组进行解析。
-
resolveClass
protected final void resolveclass(Class<?> c)
使用该方法可以使用类的Class对象创建完成的同时也被解析。
-
findLoadClass
protected final Class<?> findLoadedClass(String name)
3.Class.forName和ClassLoader.loadClass的区别
Class.forName
是一个静态方法,根据传入类的全限定名返回一个Class对象,该方法在将Class文件加载到内存的同时,会执行类的初始化操作。ClassLoader.loadClass
是一个实例方法,该方法在将Class文件加载到内存的时侯,并不会执行类的初始化操作,而是直到这个类第一次被使用时才进行初始化。
以上是关于JVM进阶之类加载器详解的主要内容,如果未能解决你的问题,请参考以下文章