参考博客:
https://blog.csdn.net/weixin_40236948/article/details/88072698
https://blog.csdn.net/xuemengrui12/article/details/82707473
https://blog.csdn.net/SEU_Calvin/article/details/52301541
类加载的过程:加载 > 验证 > 准备 > 解析 > 初始化
类的加载机制
类被加载到虚拟机内存包括加载、链接、初始化几个阶段。其中链接又细化分为验证、准备、解析。
这里需要注意的是,解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定。各个阶段的作用整理如下:
1. 类的加载
总的来说加载是类加载的第一个阶段,通过类的全限定名来找到对应的class文件,将此class文件生成一个class对象。
加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义类加载器去控制字节流的获取方式。
加载是类加载过程的一个阶段,这两个概念一定不要混淆。在加载阶段, 虚拟机需要完成以下三件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载class文件;
- 从一个ZIP、 JAR、 CAB或者其他某种归档文件中提取Java class文件,JDBC编程时使用到的数据库驱动就是放在JAR文件中,JVM可以直接从JAR包中加载class文件;
- 通过网络加载class文件,这种场景最典型的应用就是 Applet;
- 把一个java源文件动态编译、并执行加载
- 运行时计算生成, 这种场景使用得最多的就是动态代理接术, 在 java.lang.reflect.Proxy中 , 就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
2. 类的链接
当类被加载后,系统为之生成一个对应的Class对象,接着会进入连接阶段,连接阶段将会负责把类的二进制文件合并到JRE中。类连接分为如下三个阶段:
- 验证:验证阶段用于检验被加载的类是否有正确的内部结构,要验证比如class文件格式规范、这个类是否继承了final类、不能把一个父类对象赋值给子类数据类型等等;
- 准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值,所有原始类型的值都为0。如float为0f、 int为0、boolean为0、引用类型为null;
- 解析:将类的二进制数据中的符号引用替换成直接引用(符号引用是用一组符号描述所引用的目标;直接引用是指向目标的指针)
3. 初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化。
初始化过程会被触发的条件汇总:
(1)使用new关键字实例化对象、访问一个类的静态字段、静态方法的时候。
(2)对类进行反射调用的时候。
(3)当初始化子类时,如果发现其父类还没有进行过初始化,则进行父类的初始化。
类加载的三种方式
(1)由 new 关键字创建一个类的实例。
(2)调用 Class.forName() 方法,通过反射加载类。
(3)调用某个ClassLoader实例的loadClass()方法。
三者的区别汇总如下:
(1)方法1和2都是使用的当前类加载器。方法3是由用户指定的类加载器加载。
(2)方法1是静态加载,2、3是动态加载。
(3)对于两种动态加载,如果程序需要类被初始化,就必须使用Class.forName(name)的方式。
什么是类加载器?
执行以上类加载过程的就是类加载器。系统给我们提供的类加载器有三种:启动类加载器、扩展类加载器和系统类加载器。
启动类(Bootstrap)加载器:
它是由C++实现的本地方法,不属于Java类范畴,不能够被直接引用,主要被用于加载java所需的核心jar包,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,属于顶级类加载器。
扩展类(Extension)加载器:
java编写,加载扩展库,如classpath中的jre ,javax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。
系统类(System)加载器:
它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
双亲委派机制
什么是双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。
工作原理
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果弗雷能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。
优势
采用双亲委派模式的好处就是Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网路传递一个名为java.lang.Integer的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer.而之际返回已经加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在calsspath路径下自定义一个名为java.lang.SingInteger?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,需要访问权限,强制加载将会报出如下异常。
为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。