Java类加载器详解
JVM知识点划分:
以下结构实际是按照 Java 虚拟机实战的逻辑架构进行划分的 ;另外Java 虚拟机实战这本书是从中级走向高级的必经之路!!!
- JVM运行时数据区划分;
- 垃圾回收区和垃圾收集算法
- class文件结构
- 类加载器ClassLoader;
- Java虚拟机栈
类加载的定义:
类加载机制: 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制;
在Java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,这种策略给Java提供了高度灵活性,Java天生可以动态扩展的语言特性就是依赖运行期间的动态加载和连接这个特点实现的;
那么,从上面描述中可以发现以下问题(仅限于类加载相关问题);
类加载机制加载的是谁?
类加载机制加载的是class文件,即我们Java源码通过编译后的class文件
** 类加载机制加载的对象从哪里来?**
类加载的来源有很多,比如:本地磁盘(代码在本地运行时就是加载编译过的target目录下的class文件)、网络下载.class文件、在war包、jar包下加载、从专门的数据库中读取class文件(较少)、将Java源文件动态编译成class文件(Java动态代理中的jdk代理);jsp会被转换成servlet,而我们的servlet是一个Java文件,会被编译成class文件;
类加载机制通过什么加载对象到哪里去?
通过类加载器加载对象到内存中,即JVM的方法区中;那么JVM方法区的具体划分或者说这个过程是什么样的呢,请看下图:
由上图所示:本地的class文件(当然也可以是其他的比如war包,比如网络上的class文件)被类加载器加载到了JVM内存区域的方法区(jdk1.8之后改名叫元空间)中;生成相应的Class对象,且对象的引用指向数据区中的该对象的实际存储的数据结构;
在代码中获取一个对象的Class对象:
//通过对象获取;
Class<?> clazz1 = PersionClass.class;
//通过对象实例获取
PersionClass persionClass = new PersionClass();
Class<?> clazz2 = persionClass.getClass();
类加载器分类
类加载器主要有四类:
- 启动类加载器(BootstrapClassLoader):通过C语言实现,加载$JAVA_HOME/jre/lib/rt.jar(当然不止于此);
- 扩展类加载器(ExtensionClassLoader):通过Java语言实现,加载的是$JAVA_HOME/jre/lib/ext 下的相关jar包(也不止于此);
- 应用(或者叫系统)类加载器(AppClassloader);通过Java语言实现,加载工程目录的classpath下的相关jar包;;
- 自定义类加载器:通过Java语言实现,加载自定义目录下的class文件;
他们之间的具体关系如下图:
对于各个不同的类加载器加载的内容,请看如下代码:
public class MainClass {
public static void main(String[] args) {
bootClassLoaderLoadingPath();
extClassLoaderLoadingPath();
appClassLoaderLoadingPath();
}
//Bootstrap类加载器加载的jar包
public static void bootClassLoaderLoadingPath(){
String bootStrapLoadingPath = System.getProperty("sun.boot.class.path");
List<String> list = Arrays.asList(bootStrapLoadingPath.split(";"));
System.out.println("启动类的类加载器加载jar包路径如下");
//启动类的类加载器中会有一个“C:\Program Files\Java\jdk1.8.0_211\jre\classes”路径,
//这个路径表示你自定义的class文件放到上述路径中就可以被启动类加载器加载;
list.forEach(s ->System.out.println(s));
}
//扩展类加载器加载的jar包
public static void extClassLoaderLoadingPath(){
System.out.println("-----------------");
System.out.println("扩展类类加载器加载jar包路径如下");
String bootStrapLoadingPath = System.getProperty("java.ext.dirs");
List<String> list = Arrays.asList(bootStrapLoadingPath.split(";"));
list.forEach(s ->System.out.println(s));
}
//应用类加载器加载的jar包
public static void appClassLoaderLoadingPath(){
System.out.println("-----------------");
System.out.println("应用类类加载器加载jar包路径如下:");
String bootStrapLoadingPath = System.getProperty("java.class.path");
List<String> list = Arrays.asList(bootStrapLoadingPath.split(";"));
list.forEach(s ->System.out.println(s));
}
}
//输出如下:
启动类的类加载器加载jar包路径如下
C:\Program Files\Java\jdk1.8.0_211\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfr.jar
//有个jre/classes文件夹,可以将相关的class添加到这个文件夹下,有启动类加载器加载该对象;
C:\Program Files\Java\jdk1.8.0_211\jre\classes
-----------------
扩展类类加载器加载jar包路径如下
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext
C:\windows\Sun\Java\lib\ext
-----------------
应用类类加载器加载jar包路径如下(太多了就不写了):
C:\Program Files\Java\jdk1.8.0_211\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\cldrdata.jar
................
D:\software\gitRespository\springbootdemo\springbootdemo-test\target\classes
................
C:\Program Files\JetBrains\IntelliJ IDEA 2019.3\lib\idea_rt.jar
Process finished with exit code 0
类的初始化
在某些情况下,会触发初始化操作,即执行类的加载操作;具体的场景如下:
- getStatic();
- invokeStatic()
- main方法;
- new 一个对象的时候;
- Class.forName()反射的时候;
- 子类初始化一定会触发父类的初始化;
- 注意:初始化一定会有类加载操作;类加载不一定初始化;类的初始化就是执行Java静态代码块的过程
原因:Java中对象的生命周期:加载、验证、准备、解析、初始化、使用、卸载七个阶段;类的加载包括前5个过程:加载、验证、准备、解析、初始化;其中加载表示将class文件加载到内存的过程,验证、准备、解析可以合并统称为连接,加载和连接操作是由Java虚拟机执行的,跟开发人员自己写的代码无关;到初始化阶段才真正开始执行开发人员编写的代码(执行静态代码块中的内容);因此想要初始化一个对象,必须经过类的加载、连接过程;但是在加载一个类的时候,如果一个类没有静态代码块,那么这个类的加载过程只需要执行到类的解析阶段,并没有类的初始化阶段;因此类的初始化一定有类的加载操作,类的加载操不一定有类的初始化过程
双亲委派模型的运行机制
双亲委派模型:自底向上检查,自顶向下加载;具体请看下图:
双亲委派模型的具体实现及源码解析(重点,需要认真看)
双亲委派模型的具体过程请看下图:
双亲委派模型的源码如下:
//包名:package java.lang;
//类名:ClassLoader
//方法名:loadClass
源码:
//执行loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//同步锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded(第一步,判断该class是否被加载过)
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//判断父类加载器是否为null
if (parent != null) {
//如果父类加载器不是null,则调用父类加载器执行类加载过程
c = parent.loadClass(name, false);
} else {
//循环判断,最后达到最顶层的bootstrap类加载器进行加载class
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(如果父类加载器没有加载成功,则调用当前加载器进行加载class)这个是循环加载的过程;
// 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
1)、写一个loadClassData():把.class文件变成byte数组;
2)、重写我们的findClass方法;
findClass(){
loaderClassDate();
defineClass();
}
官网demo如下:
* 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
* . . .
* }
* }
类加载器的命名空间
类加载器的命名空间:是由类加载器本身以及所有父加载器所加载出来的binary name (full class name)组成;类加载器的命名空间有如下约束条件:
- 在同一个命名空间里,不允许出现第二个完全一样的binary name;
- **在不同的命名空间中,可以出现两个相同的binary name,但是两者对应的Class对象是相互不能感知的,也就是说Class对象的类型是不一样的;**通俗的语言表示就是说,两个类加载器加载相同的一个Class对象(假设对象为Person);那么这两个对象被加载之后是无法进行类型转换的,如果进行强制转换,则会报出类型转换异常;所以,两个不同的类加载器加载相同的一个Class对象,这两个对象之间无法进行强制转换;
- 子加载器的命名空间中的binary name对应的类中可以方位父加载器命名空间中的binary name对应的类,反之则不行; 通俗的讲:最顶层的加载器BootstrapClassLoader加载器加载String类对象,应用类加载器加载Person对象,那么Person对象是可以访问String对象的,且在Person对象中也可以使用String对象,但是反之,String对象中是不可以访问Person对象,也是不可以使用Person对象的;
命名空间是由该类加载器以及其父类加载器所构成的,其中父类加载器加载的类对其子类可见,但是反过来子类加载的类对父类不可见,同一个命名空间中一定不会出现同一个类(全限定名一模一样的类)多个Class对象,换句话说就是在同一命名空间中只能存在一个Class对象,所以当你听别人说在内存中同一类的Class对象只有一个时其实指的是同一命名空间中,当然也不排除他压根就不知道这个概念。
双亲委派模型的好处
当同一个Person.class文件被我们不同的类加载器去加载的时候,我们的JVM内存中会生成两个对应的Person的class对象,而且这两个Class对象是相互不可见的的(通过Class对象反射创建的实例对象是不能兼容的,不能进行转换)这也很好的解释了双亲委派模型的好处;