JVM虚拟机中类加载的过程:加载、连接(验证、准备、解析)、初始化三个步骤.
盗用网上的图:
加载
加载是类装载的第一步,首先通过class文件的路径读取到二进制流,并解析二进制流将里面的元数据(类型、常量等)载入到方法区,在java堆中生成对应的java.lang.Class对象。
连接
- 验证
确保Class文件是否复合JVM虚拟机的要求,不会威胁到虚拟机的安全,还有对版本号进行一些验证,比如JDK8的环境的代码是否在JDK6虚拟机使用等等.具体验证的过程相对复杂,查阅资料,具体有以下4种验证:
a)文件格式验证:验证字节流是否符合Class文件的规范,并且能被当前版本的虚拟机处理。
b)元数据验证:对字节码描述的信息进行语义分析,确保描述的信息符合java规范的要求。
c)字节码验证:进行数据流和控制流的分析。确保被验证类的方法在运行时不会做出危害虚拟机安全的行为。
d)符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行
- 准备
准备过程就是分配内存(方法区),给类的一些字段设置初始值,
这里的变量指的是被static 修饰的类变量,不包括实例变量,实例变量将会在对象被实例化的时候进入堆内存中
如: public static int num = 10;
这段代码在准备阶段num 的值就会被初始化为0,只有到后面类初始化阶段时才会被设置为10。
但是对于static final(常量),在准备阶段就会被设置成指定的值,例如:
public static final int num = 1;
这段代码在准备阶段num的值就是1
- 解析
解析过程就是将符号引用替换为直接引用,例如所有的类都继承java.lang.Object,符号引用记录的是“java.lang.Object”这个符号,但是凭借这个并不能找到继续java.lang.Object这个对象在哪里?而直接引用就是要找到继续java.lang.Object所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。
初始化
主要包括执行类构造方法、static变量赋值语句,staic{}语句块,与现实情况一样,先有父才有子,如果一个子类进行初始化,那么会先初始化其父类,保证父类在子类之前初始化。所以初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。public static int num = 10,在这个阶段才真正的把num的值赋为10.
类加载器
类加载器几个重的方法:
1.public Class<?> loadClass(String name) throws ClassNotFoundException 载入类,并返回calss
2.protected final Class<?> defineClass(byte[] b, int off, int len) 定义一个类
3. protected Class<?> findClass(String name) throws ClassNotFoundException 查找类
4.protected final Class<?> findLoadedClass(String name) 查找已经加载过的类
- 双亲委托机制
Java 类加载器使用的是双亲委托机制,也就是一个类加载器在加载一个类时候会首先尝试让父类加载器来加载。
首先从App ClassLoader中调用findLoadedClass方法查看是否已经加载,如果没有加载,则会交给父类,Extension ClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 Bootstrap ClassLoader到 App ClassLoader依次尝试加载。
值得注意的是即使两个类来源于相同的class文件,如果使用不同的类加载器加载,加载后的对象是完全不同的,这个不同反应在对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。
双亲委派机制的优势:一是避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。二是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.String的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.String,而直接返回已加载过的String.class,这样便可以防止核心API库被篡改.
双亲委派机制的问题: 顶层ClassLoader无法访问底层ClassLoader的类.比如:javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface SPI 位于rt.jar 即接口在启动BootstrapClassLoader中。而SPI的实现类,在AppLoader。这样就无法用BootstrapClassLoader去加载SPI的实现类。解决方式:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器.
双亲委派机制的破坏:双亲模式是默认的模式,但不是必须这么做;Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent;OSGi的ClassLoader形成网状结构,根据需要自由加载Class。
package com.lyb269.jvm.classLoader;
import org.junit.Test;
/**
* 类加载体系
*/
public class ClassLoaderSystemTest {
@Test
public void test(){
System.out.println(Thread.currentThread().getContextClassLoader());
//输出: sun.misc.Launcher$AppClassLoader@18b4aac2 使用的系统加载器 AppClassLoader
System.out.println(ClassLoaderSystemTest.class.getClassLoader());
//输出: sun.misc.Launcher$AppClassLoader@18b4aac2 使用的系统加载器 AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
//输出: sun.misc.Launcher$AppClassLoader@18b4aac2 使用的系统加载器 AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
//输出: sun.misc.Launcher$ExtClassLoader@4a574795 AppClassLoader加载器的父类为ExtClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
//输出: null AppClassLoader加载器的父类为ExtClassLoader,ExtClassLoader的父加载器为BootstrapLoader,BootstrapLoader是native底层实现的,所以打印为null
System.out.println(System.class.getClassLoader());
//输出: null System类是系统提供的,在rt.jar里面,由BootstrapLoader加载,BootstrapLoader是native底层实现的,所以打印为null
//双亲委派机制,自下往上查下
// BootstrapLoader
// |
// |
// ExtClassLoader
// |
// |
// AppClassLoader
// 加载顺序:根据双亲委派机制,自下AppClassLoader开始查找,没有从ExtClassLoader尝试查找,
// 还是没有从BootstrapLoader查找,如果仍然没有自顶向下开始尝试
}
}