类的加载过程
类的加载过程,主要有以下三步
- 加载:通过ClassLoader加载class文件,生成Class对象
- 连接:该过程分为三步,验证、准备、解析
- 验证:主要是验证class文件的正确性与安全性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:将常量池内的符号引用替换为直接引用
- 初始化:执行类变量的赋值和静态初始化
这里将着重介绍类的加载、以及初始化过程,而不会对连接部分做阐释。
类的初始化
我们先不谈如何加载一个类,先来看看如何使用一个类,也就是类的初始化过程。
有5种情况会对类进行初始化:
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令。常见场景:使用new创建一个对象,操作静态变量或静态方法
Demo demo = new Demo(); // 使用new创建一个对象
Demo.FIELD = 1; // 设置静态变量值
System.out.println(Demo.FIELD); // 使用静态变量
Demo.method(); // 执行静态方法
2)通过反射进行调用
Class clazz = Class.forName("com.dfyang.aop.utils.Demo");
源码分析,涉及到native方法,这里只是通过参数进行了解释
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
// initialize为true表示类将被初始化
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
3)当初始化一个类时,先初始化其父类(这个容易理解,毕竟子类会继承父类)
4)当虚拟机启动时,虚拟机需要初始化主类,也就是程序的入口
public static void main(String[] args) {
System.out.println("程序入口");
}
@SpringBootApplication
public class ZooCommunityApplication {
public static void main(String[] args) {
SpringApplication.run(ZooCommunityApplication.class, args);
}
}
5)java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄(这里仅了解)
这里我们已经知道了我们经常使用的类是如何进行初始化,接下来我们再来看看类的加载过程。
类加载器
首先来介绍以下ClassLoader(类加载器),其主要作用是从系统外部获取Class二进制数据流,所有Class都是由ClassLoader进行加载。
下面引用《深入理解Java虚拟机》表示其重要性。
类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足Java Applet的需求二开发出来的。虽然目前Java Applet技术基本上已经“死掉”,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了Java技术体系的一块重要的基石,可谓是失之东隅,收之东隅 。——《深入理解Java虚拟机》
首先介绍ClassLoader中两个重要的方法
defineClass:该方法将字节数组转化为类的实例(具体转换过程涉及native方法)
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
findClass:这个方法需要我们重写
具体步骤:通过该方法传入类名,再将Class文件转换为字节数组,再通过defineClass方法我们就能够生成类的实例
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
下面来自定义一个类加载器,继承ClassLoader并重写findClass方法。
如下所示,该加载器指定类名,在指定路径下查找对应的class文件,并进行加载。
package com.dfyang.aop;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @Author: 55411
* @Date: 2019/8/14 22:17
* @Description: 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
/** 加载文件路径 */
private String path;
public MyClassLoader(String path) {
this.path = path;
}
/**
* 指定类名生成类对象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] binaryStream = toBinaryStream(name);
return defineClass(name, binaryStream, 0, binaryStream.length);
}
/**
* 转换为字节数组
* @param name
* @return
*/
private byte[] toBinaryStream(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(name);
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
下面是需要被加载的类,静态代码块中的代码将在类初始化时执行。(我是通过javac编译成class文件,放到F盘)
public class Test {
static {
System.out.println("Hello world!");
}
}
执行下面代码,控制台打印:Hello world!
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("F:\\");
Class test = myClassLoader.findClass("Test");
test.newInstance();
}
上面我们时通过将class文件放到指定的目录,然后通过类加载器指定类名生成对应类的实例。
——上面说到Class.forName会对类进行初始化,也就是整个类加载的三步,加载、连接、初始化已完成,ClassLoader加载类与其不同,只完成了加载过程。
双亲委派机制
下面介绍系统提供的3中类加载器:
- BootstrapClassLoade(启动类加载器):加载java核心类库(JAVA_HOME\lib目录下)
- ExtClassLoader(扩展类加载器):加载JAVA_HOME\lib\ext目录下或java.ext.dirs系统变量指定路径的所有类库
- AppClassLoader(应用程序类加载器):加载程序所在目录(ClassPath)
- 还有就是自定义类加载器了
可以看到系统提供的类加载器同样是对指定路径进行类的加载。
接下来介绍类加载器的双亲委派机制:
双亲委派机制的就是一个类加载器收到了类加载请求,首先不会自己尝试加载,而是会委派给父类加载器完成,每一层加载器均是如此,直到最顶层的BootStrapClassLoader(启动类加载器),再由最顶层的BootStrapClassLoader(启动类加载器)进行加载,如果无法进行加载,再委派给子类加载器完成,每一次加载器均是如此。
顺序的证明如下
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("F:\\"); // 自定义类加载器
System.out.println(myClassLoader.getParent()); // AppClassLoader
System.out.println(myClassLoader.getParent().getParent()); // ExtClassLoader
System.out.println(myClassLoader.getParent().getParent().getParent()); // null
}
// 打印日志,由于Bootstrap ClassLoader由c++编写,并未由java编写,所以为null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@6f496d9f
null
类加载过程的部分源码分析
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) {
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();
}
为什么需要双亲委派机制?
为了保证程序的正常运行,试想如果我们使用自定义类加载器加载了我们自己编写的java.lang.Object,那么程序可以正常运行吗?——当然,因为有双亲加载机制,这里是无法进行验证的。总之,双亲委派机制就是为了保证我们程序的正常运行。