首先理解类加载的总体过程
首先,要想真正的把握类加载的过程及对应的Java源码分析,理解大纲图是非常有必要的,案例代码及原理图如下图:
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
类加载流程
1、加载
思考一个问题,在这个过程中,加载的具体什么,针对我们开发人员,我们写的仅仅是java源文件而已,而Java虚拟机是如何进行加载的呢?
java文件---->class文件(前期编译经过了一系列的操作,如词法分析器–>tokens流–>语法分析器–>语法树/抽象语法树–>语义分析器–>注解抽象语法树–>字节码生成器–>class文件)
加载的过程【class文件 --> JVM内部各种折腾】:首先通过类的全限定名将该类转化为二进制的流文件,但在这个文件依然仅仅是静态的存储结构,将该字节流的静态存储结构转化为方法区运行时数据结构,同时在 Java堆中生成代表该类的Java.lang.Class对象,供外部访问
2、验证
此时我们知道已经生成了class文件,class文件是二进制文件,此阶段是校验字节码文件是否正确,那么仅仅是校验字节码文件吗?那可不是!校验包含文件格式验证、元数据校验、字节码验证、符号引用验证。
文件格式验证:验证字节流是否符合Class文件的规范,并且能被当前版本虚拟机所处理,这阶段的验证是基于二进制字节流进行的,只有经过该阶段验证后,字节流才会进入内存的方法区中存储,后面的验证都是基于方法区的存储结构进行的;如是否以16进制cafebaby开头,版本号是否正确等!
元数据校验:对类的元数据信息进行语义校验,实际上就是对Java语法的校验;如是否有父类,是否继承了final类(我们知道类上有final关键字的类是不能被继承的),一个非抽象类是否实现抽象类的所有方法!
字节码验证:进行数据流和控制流分析,确定程序语义是合法的,符合逻辑的;如运行检查,栈数据类型和操作码操作参数吻合,跳转指令指向合理的位置等!
符号引用验证:它发生在将符号引用转化为直接引用阶段,可以看做是对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验,确保解析动作能正常进行;如常量池中描述类是否存在,访问的方法或者字段是否存在且有足够的权限!
3、准备
为已在方法区中类的静态成员变量分配内存,并将其初始化为默认值。进行分配内存的只是包括类变量(静态变量),而不包括实例变量。
public static int i = 1;
//在准备阶段后的初始化值为0,不为1,这时候只是开辟了内存空间,并没有运行Java代码,
//i赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以i被赋值为1是在初始化阶段才会执行
/*在这里我们知道,初始化的值是通过类构造器获取的*/
这里不包含final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。
private int i = 1;
//这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
ConstantValue属性作用:ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。非static类型的变量的赋值是在实例构造器方法中进行的;static类型变量赋值分两种,在类构造其中赋值,或使用ConstantValue属性赋值。
public final static int i = 2;
/*对于一些特殊情况,如果类字段属性表中存在ConstantValue属性,现在准备阶段变量i就会被初始化为
ConstantValue属性所指的值,只有同时被final和static修饰的字段才有ConstantValue属性,
且限于基本类型和String,那么为什么是这两种类型?
因为从常量池中只能引用到基本类型和String类型的字面量
*/
//总结:编译时Javac将会为i生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为1,我们可以理解为final static常量在编译期就将其结果放入了调用它的类的常量池中,具体的ConstantValue属性,我们可以通过javap -v -p xxx.class反编译得到的结果看到该属性
4、解析
把类的符号引用转化为直接引用
解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程
常量池:字面量(由字母、数字等构成的字符串或者数值常量)、符号引用组成,例如:a=1, a为符号引用,1为字面量,同时符号引用也可以是类的全限定名、方法字段名称等!(想要深入理解常量池,后续会陆续更新)
5、初始化
初始化阶段是执行类构造器的过程
为静态成员变量赋值,赋的是程序员给定的值
6、使用
主动引用
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName(“com.carl.Test”) )
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类
被动引用
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)。
7、销毁
在类使用完后,如果满足下面的条件,类就会被卸载:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的classLoader被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,JVM就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,Java类的整个生命周期就结束了。我们知道卸载类很难,每次进行垃圾回收的时候,元空间几乎没什么变动,因为类加载机制中类加载器几乎不会被回收。
类加载器和双亲委派机制
四种类加载器: bootstrapClassLoader(引导类加载器·),ExtClassLoader(扩展类加载器),appliactionClassLoader(应用程序类加载器),customerClassLoader(自定义类加载器)
含义:在加载(load)阶段,其中第(1)步,通过类的全限定名获取其定义的二进制字节流,需要借助类加载器完成,顾名思义,就是用来加载class文件的。
代码示例:
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println();
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
运行结果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@3764951d
the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc
bootstrapLoader加载以下文件:
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/classes
extClassloader加载以下文件:
D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
appClassLoader加载以下文件:
D:\Program Files\Java\jdk1.8.0_301\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_301\jre\lib\rt.jar;E:\workdir\projects\tuling\jvm\target\classes;D:\work\jars\m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;D:\work\ide\idea\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar
双亲委派机制
原理:首先先找自己是否加载过该类,如果加载过,则直接使用,没有的话,逐级往上层寻找,没有找到的话再逐级往下层去找,Java中具体的源码实现请看Launcher类源码分析。
双亲委派机制只是Java推荐的机制,并不是强制的机制
原理图:
思考一个问题,为什么要设计这种机制呢?可以从它的好处着手。
- 沙箱安全
- 避免类的重复加载
沙箱安全机制可以避免自己写的java.lang.String.class类不会被加载,这样便可以防止核心
API库被随意篡改;避免类的重复加载可以使得当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
深入Java源码理解Launcher类
从全文开篇的第二张图,我们可以知道,Java虚拟机首先通过C和C++创建了一个引导类加载器,这个类加载器是c、c++实现的,我们在Java层面是获取不到的,其次就是通过Launcher类去创建了ExtClassLoader、AppClassLoader,同时指定了系统类加载为AppClassLoader,通过parent属性指定AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是引导类加载器,不是通过继承实现,源码分析如下:
//Launcher类构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
。。。 。。。 //省略一些不需关注代码
}
}
双亲委派的源码实现:ClassLoader_loadClass方法
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) {
//向上委托父类查找
//如果当前加载器父加载器不为空则委托父加载器加载该类
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();
//如果委托父类都没有找到,就自己加载
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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类,如果不打破双亲委派机制,重写findClass(name)方法;打破双亲委派机制,重写loadClass(name)方法
自定义类加载器
自定义类加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass(),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所有我们自定义类加载器主要是重写findClass方法。
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转化为一个class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name,data,0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.jiang.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
打破双亲委派机制
双亲委派这个模型并不是强制模型,而且会带来一些问题,就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。
SPI: JDK提供接口,提供商提供服务,编程人员编码时面向接口编程,JDK能够自动找到合适的实现
OSGI:比如我们的JAVA程序员更加追求程序的动态性,比如代码热部署,代码热替换。也就是机器不用重启,只要部署上就能用。OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块都有一个自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起换掉以实现代码的热替换。
打破双亲委派,重写loadClass()方法
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转化为一个class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name,data,0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
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();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if (!name.startsWith("com.jiang.jvm")) {
c = this.getParent().loadClass(name);
} else
{
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;
}
}
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader("D:/test");
Class<?> clazz = classLoader.loadClass("com.jiang.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}