JVM 类加载整体流程
整体的类加载流程如下图所示:
例如我想要加载一个Demo 类:
- 首先会先启动 JVM 虚拟机(JVM 虚拟机底层是由 C++ 来实现的,在 windows 下启动即为 java.exe 方法调用 jvm.dll 创建 JVM)
- 由 JVM 虚拟机创建 引导类加载器(同样底层为 C++ 实现)
- 该引导类加载器会去创建 JVM 启动类 sun.misc.Launcher 类实例。
- 通过调用 sun.misc.Launcher.getLauncher() 方法,该方法会返回 Launcher 类内部静态变量: private static Launcher launcher = new Launcher()。( 在 new Launcher() 的过程中会创建运行类自己的类加载器。这里可以直接理解为 AppClassLoader 应用类加载器。)
- 通过 launcher.getClassLoader() 方法获取该类加载器 。
- 通过调用 classLoader.getClass("com.test.Demo") 方法加载 Demo 类实例。
- 类加载成功后由 JVM 发起调用,调用要执行类的主方法入口执行程序逻辑
- 执行结束, 销毁 JVM。
类加载过程
以上流程作为一个总括的了解,这里重点讲一下,classLoader.loadClass("com.test.Demo") 这个类加载过程。这个为重点部分,相信了解过的同学都知道双亲委派模型,便是这里了。
首先还是一张图来直观的看一下类的加载过程:
一个类的加载过程分为:加载、验证、准备、解析、初始化 这 5 步
- 加载:将类信息从磁盘加载到内存中
- 验证:验证 Java 字节码的有效性
- 准备:为静态变量赋予初始默认值(该默认值为 java 定义,这里注意静态变量 static 修饰,而 final 为常量会直接赋值)
- 解析:可以理解为解析过程就是静态链接的过程即:将符号引用转变为直接引用的过程,该阶段会将一些静态方法如 main() 方法(符号引用)转变为指向数据所存的内存地址或者句柄(直接引用)。而有些方法不会在这个阶段去解析,其会在运行时进行该转换,我们称之为动态链接。
类加载器
本篇重点,类加载器来了,以上的类加载过程都是通过类加载器来完成的。 Java 的类加载器分一下几种:
- 引导类加载器:主要负责加载 jre lib 目录下 jar 包 (加载 java 核心 jar 文件 rt.jar ,chartsets.jar )
- 扩展类加载器:主要负责加载 jre lib/ext 目录下 jar 包
- 应用类加载器:主要负责加载类路径 classPath 下类包(.class 文件)
- 自定义类加载器:加载自定义路径下类包
类加载器在 Launcher 类构造方法中进行了初始化,这里截取部分源码:
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
// init FileSystem machinery before SecurityManager installation
sun.nio.fs.DefaultFileSystemProvider.create();
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
}
这里重点关注以下几行代码:
内部 static launcher 实例。单例
private static Launcher launcher = new Launcher();
类加载器
private ClassLoader loader;
获取luancher
public static Launcher getLauncher() {
return launcher;
}
获取类加载器
public ClassLoader getClassLoader() {
return loader;
}
创建扩展类加载器
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
这里可以看到 loader 最终赋值为 AppClassLoader 也就是应用类加载器。
通过上面的加载流程我们可知所有的类都是通过调用,loader.loadClass()方法来加载类
所以我们写的程序(未指定特殊加载方式的)都是通过应用类加载器开始加载
loader = AppClassLoader.getAppClassLoader(extcl);
双亲委派模型
类加载器加载类过程如下图所示:
一般类的加载流程是首先通过 AppClassLoader 应用类加载器去加载(这个由上述源码就可看出),而这个类加载过程存在一个双亲委派机制,即应用类加载器在尝试加载时会先判断是否已经加载过该类,如果之前加载过则直接返回类信息,若没有则委托给 parent 类加载器去加载(注意这里的 parent 类加载器并不是他的父类,且这时应用类加载器并没有执行加载),此时 parent 类加载器即扩展类加载器会执行相同的操作,最终委托给引导类加载器进行加载,引导类加载器加载到该类后会直接返回,若没有加载到则会返回给子类加载器去尝试加载。重复此过程直到加载成功或者失败。
现在我们尝试从源码的级别来看一下 java 底层究竟做了什么。
AppClassLoader 继承关系如下图
AppClassLoader.loadClass() 方法:
/**
* Override loadClass so we can checkPackageAccess.
*/
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
return (super.loadClass(name, resolve));
}
重点关注最后一行代码,上面的校验逻辑代码可以先略过, 我们可以理解为应用类加载器经过一系列校验判断会走到最后一行代码去调用父类加载器的类加载方法:
super.loadClass(name,resolve);
由上面的继承关系可知 AppClassLoader 的父类最终会继承抽象类 ClassLoader 。这里最终会调用 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();
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;
}
}
首先会先检查是否已经加载过该类,如果加载过则直接返回
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
如果没有找到则会判断 parent 类加载器是否为 null 如果不为 null 则委托 parent 类加载器去加载,否则则使用引导类加载器加载 findBootstrapClassOrNull 最终是一个本地 C++ 实现的方法,会使用引导类加载器去尝试加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
这里整体就串起来了,ExtClassLoader 同样最终会调用 ClassLoader 实现的 loadClass 方法
这个流程就是向上委托流程。而如果委托 parent 未加载到则会调用自己实现的 findClass() 方法尝试自己加载:
if (c == null) {
// If still not found, then invoke findClass in order
// 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();
}
这就是类加载器双亲委派的全部流程。
如何自定义类加载器
不管是 AppClassLoader 还是 ExtClassLoader 都会继承 ClassLoader 这一根类。所以我们如果想自定义自己的加载器只需要实现一个类并且继承 ClassLoader 类即可,同时重写他的 findClass 方法即可。 Talk is Cheap Show me the Code
package think_in_classloader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.reflect.Method;
public class MyTestClassLoader extends ClassLoader{
private String classPath;
private MyTestClassLoader(String classPath){
System.out.println("初始化:"+classPath);
this.classPath=classPath;
}
private byte[] loadByte(String name) throws Exception {
name=name.replaceAll("\\.","/");
FileInputStream fileInputStream=new FileInputStream(classPath+"/"+name+".class");
int len=fileInputStream.available();
byte[] data=new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data=loadByte(name);
return defineClass(name,data,0,data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
MyTestClassLoader myTestClassLoader=new MyTestClassLoader("D:\\test");
Class clazz=myTestClassLoader.loadClass("test.Demo");
Object object=clazz.newInstance();
Method method=clazz.getDeclaredMethod("sayHello",String.class);
method.invoke(object,"Word j");
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
为什么设计双亲委派模型?
说了这么多底层原理,我想大家应该都很好奇。为什么要设计这样一个双亲委派机制呢? 我想大概可以从两方面理解:
- 沙箱安全机制:防止 Java 核心类库被随意修改 (自己写的 Java.long.String.class 不会被加载)
- 避免类的重复加载:当父类加载器已经加载类后子类加载器没必要再次加载,保证了类加载的唯一性。
如何打破双亲委派模型
在说如何打破双亲委派模型前,大家可能更加好奇我们为什么要打破双亲委派模型? 举个例子,对于 tomcat 来说,我们都知道一个 tomcat 是可以部署多个 war 包的,那如果我们部署的多个 war 包其依赖的类库是不同版本的,比如一个依赖 Spring 4 一个依赖 Spring 5 ,这个时候根据双亲委派机制,Spring 4 首先加载进来的那么另一个依赖 Spring 5 的 war 包在加载时就不会再去加载 Spring 5 因为同名的原因会直接给他返回已加载过的 Spring 4 。这个时候就存在了版本不一致的问题。所以对于 tomcat 来说他就需要自己实现类加载器来打破双亲委派模型,并给每一 war 包去生成一个自己对应的类加载器
那么到底如何打破双亲委派模型呢?很简单我们只需要重写 loadClass 方法就可以了示例代码如下:
package think_in_classloader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.reflect.Method;
public class MyTestClassLoader extends ClassLoader{
private int num=9;
private String classPath;
private MyTestClassLoader(String classPath){
System.out.println("初始化:"+classPath);
this.classPath=classPath;
}
private byte[] loadByte(String name) throws Exception {
name=name.replaceAll("\\.","/");
FileInputStream fileInputStream=new FileInputStream(classPath+"/"+name+".class");
int len=fileInputStream.available();
byte[] data=new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data=loadByte(name);
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 still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
System.out.println(name);
if (name.startsWith("test")){
c = findClass(name);
}else {
c=getParent().loadClass(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 Exception {
MyTestClassLoader myTestClassLoader=new MyTestClassLoader("D:\\test");
Class clazz=myTestClassLoader.loadClass("test.Demo");
Object object=clazz.newInstance();
Method method=clazz.getDeclaredMethod("sayHello",String.class);
method.invoke(object,"Word j");
System.out.println(clazz.getClassLoader().getClass().getName());
}
}