- 类加载运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
我这边简单写了一个类来演示:
package com.springli.microservice.gateway.jvm.part1;
import lombok.Data;
import java.io.Serializable;
/**
* @program: micro-service-frame
* @ClassName User
* @description: 用户类
* @author: SpirngLi
* @create: 2020-08-27 16:54
**/
@Data
public class User implements Serializable {
private static final long serialVersionUID = -3340708260836763597L;
private Long id;
private String name;
private String phone;
public static int data=110;
public User initData(){
User user=new User();
user.setId(1L);
user.setName("张三");
user.setPhone("1234567");
System.out.println("-----初始化完毕-----");
return user;
}
public static void main(String[] args) {
System.out.println("-----开始执行main方法----");
User u=new User();
User userData = u.initData();
System.out.println("调用初始化方法成功,用户名称:"+userData.getName());
}
}
通过Java命令执行代码的大致流程如下图所示:
其中loadClass的类加载过程有如下几步:
加载–>验证–>准备–>解析–>初始化–>使用–>卸载。
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载。例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象来作为方法区这个类的各种数据的访问入口。
- 验证:校验字节码文件的正确性。
- 准备:给类的静态变量分配内存,并赋予默认值。
- 解析:将符号引用替换为直接引用(即内存中真正的地址),该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引入)
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块。
注意:主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
package com.springli.microservice.gateway.jvm.part1;
/**
* @program: micro-service-frame
* @ClassName DynamicLoad
* @description: 动态加载
* @author: SpirngLi
* @create: 2020-08-28 10:41
**/
public class DynamicLoad {
static {
System.out.println("*************load DynamicLoad************");
}
public static void main(String[] args) {
new A();
System.out.println("*************分割线************");
//这边不会执行,只有调用的时候才会加载
B b = null;
}
}
class A{
static{
System.out.println("*************Load A**************");
}
public A(){
System.out.println("*************initial A************");
}
}
class B{
static{
System.out.println("*************Load B**************");
}
public B(){
System.out.println("*************initial B************");
}
}
这边再说下类的静态变量、静态代码块、代码块、构造方法等加载顺序。这个直接参考网上的文章即可:静态代码块、构造代码块、构造函数加载顺序详解
- 类加载器和双亲委派机制
上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器:
@ 引导类加载器:负责加载支撑JVM运行(位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar),此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的,是虚拟机自身的一部分。
@ 扩展类加载器:负责加载支撑JVM运行(位于JRE的lib目录下的ext扩展目录中的JAR类包),开发者可以直接使用这个类加载。
@ 应用程序类加载器:负责加载ClassPath路径下的类包,一般我们编写的java类都是由这个类加载器加载,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。一般情况下这就是系统默认的类加载器。
@ 自定义加载器:负责加载用户自定义路径下的类包。
package com.springli.microservice.gateway.jvm.part1;
import sun.misc.Launcher;
import java.net.URL;
/**
* @program: micro-service-frame
* @ClassName JdkClassLoaderDemo
* @description: JDK类加载Demo
* @author: SpirngLi
* @create: 2020-08-28 15:05
**/
public class JdkClassLoaderDemo {
public static void main(String[] args) {
//这个为null的原因是因为是BootStrap加载的,而BootStrap是由C++调用的
System.out.println(String.class.getClassLoader());
//这个类是JRE/lib/ext里面的类,所以由Ext加载
System.out.println(
com.sun.crypto.provider.DESKeyFactory
.class.getClassLoader().getClass().getName()
);
//这个类是CLASS_PATH下的类,所以由App加载
System.out.println(
JdkClassLoaderDemo.class.getClassLoader().getClass().getName()
);
System.out.println("-------------------------------------");
//获取app加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
//获取app的父加载器
ClassLoader extClassloader = appClassLoader.getParent();
//获取ext的父加载器
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(URL url:urLs){
System.out.println(url);
}
System.out.println("extClassloader加载以下文件:");
String[] extList = System.getProperty("java.ext.dirs").split(";");
for(String ext:extList){
System.out.println(ext);
}
System.out.println("appClassLoader加载以下文件:");
String[] appList = System.getProperty("java.class.path").split(";");
for(String app:appList){
System.out.println(app);
}
}
}
双亲委派模型,如下图:
一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其它父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就返回成功。倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合的关系来复用父类加载器的代码。
- ClassLoader源码分析
所有类加载器都会继承自java.lang.ClassLoader类(除了类启动加载器是C++实现的)。双亲委派模型的实现主要是java.lang.ClassLoader#loadClass(java.lang.String, boolean)和java.lang.ClassLoader#findClass两个方法。
ClassLoader.java #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 {
//先判断parent是否为空
//如果不为空,先尝试用父类加载器加载
//如果为空,则先尝试用类启动加载器加载(除了拓展类加载器parent为空,一般parent都不会为空)
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
}
//如果父类加载器没加载成功,
//才尝试调用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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
再来看下findClass的实现:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看到,findClass的默认实现是抛出ClassNotFoundException异常,也就是说父类加载器加载不成功,就抛出了异常。
- AppCladdLoader和ExtClassLoader源码分析
可以看出AppClassLoader和ExtClassLoader继承自URLClassLoader。URLClassLoader主要实现通过url路径加载类的功能。从另外一个侧面也反映出jdk的双亲委派模型是基于组合而非继承来实现的。AppClassLoader和ExtClassLoader的实现都在sun.misc.Launcher类上。Launcher主要是在程序启动的时候加载主要的类加载器(Ext和APP类加载器)。我们看下Launcher加载的源码:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//(1)初始化扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader");
}
try {
//(2)初始化应用类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader");
}
//(3)线程上下文加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if(var2 != null) {
SecurityManager var3 = null;
if(!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
;
} catch (InstantiationException var6) {
;
} catch (ClassNotFoundException var7) {
;
} catch (ClassCastException var8) {
;
}
} else {
var3 = new SecurityManager();
}
if(var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
(1)处注释初始化了扩展类加载器,而真正的调用代码再静态内部类ExtClassLoader.class的getExtClassLoader()中。
ExtClassLoader初始化过程解析:
getExtClassLoader()内部通过调用该类的构造器,返回一个ExtClassLoader的实例,这里构造器传入的参数var0实际上就是ExtClassLoader加载目录的文件对象。第二个红线说明创建ExtClassLoader的实例实际上还是得调用其父类URLClassLoader的构造器。其中有3个参数,第二个参数最为重要,该参数表示ExtClassLoader对象的父加载器是谁。
URLClassLoader.java
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
ucp = new URLClassPath(urls, factory);
acc = AccessController.getContext();
}
这里URLClassLoader会调用其父类的构造器,最终止于ClassLoader。这里我们无需太关心,有兴趣的可以自行看源码,只需要记住ExtClassLoader.class的父类加载器为null。
(2)第二个注释初始化应用类加载器,源码中将(1)处创建的ExtClassLoader的实例var1传递给应用类加载器getAppClassLoader()方法。
由上图可得AppClassLoader也走了ExtClassLoader的老路,那么这里的var2就是parent扩展类加载器ExtClassLoader了。至此ClassLoader父子关系在JDK源码中是如何做的也就清楚了。
- 双亲委派机制如何实现
由于类加载器的双亲委派机制,因此我们可以通过任何一个类加载器加载类的代码来梳理整个类加载器的加载流程,这里我们选择应用类加载器AppClassLoader,双亲委派机制的具体逻辑存在于类加载器的loadClass方法中。
红色处代码调用AppClassLoader父类的loadClass方法,由前面的UML图可知,AppClassLoader的父类为URLClassLoader和ClassLoader,再跟踪下代码如下:
上图就是ClassLoader类加载的核心方法,红色框内的代码即是实现委派机制的代码,逻辑很简单:如果当前类加载器的父类加载器不为空,就先让父类加载器加载name所对应的类。这就解释了ExtClassLoader的父类加载器传递的是null,就会执行else的逻辑,调用findBootstrapClassOrNull(),而该方法最终为native方法。
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);
实际上就是调用jdk中的BootstrapClassLoader的实现去加载该类。
- 为什么要设计双亲委派机制
(1)沙箱安全机制:比如自己写个java.lang.String.class类不会被加载,这样可以防止核心API库被随意篡改。
package java.lang;
/**
* @program: micro-service-frame
* @ClassName String
* @description:
* @author: SpirngLi
* @create: 2020-08-28 14:56
**/
public class String {
public static void main(String[] args) {
System.out.println("---执行自定义的String----");
}
}
(2)避免类的重复加载:当父亲已经加载了该类时,就没必要子ClassLoader再加载一次,保证被加载类的唯一性。
- 全盘负责委托机制
“全盘负责”是指一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoader,该类所依赖及引用的类也是由这个ClassLoader载入。 - 自定义类加载器(双亲委派机制的)
package com.springli.microservice.gateway.jvm.part1;
import cn.hutool.core.io.FileUtil;
import java.lang.reflect.Method;
/**
* @program: micro-service-frame
* @ClassName MyClassLoaderDemo
* @description: 自定义类加载器(双亲委派机制)
* @author: SpirngLi
* @create: 2020-08-28 15:28
**/
public class MyClassLoaderDemo {
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath=classPath;
}
/**
* 重写findClass
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//加载类返回字节
byte[] data=loadByte(name);
///defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name,data,0,data.length);
}
/**
* 读取文件返回字节
* @param name
* @return
*/
private byte[] loadByte(String name) {
name=name.replaceAll("\\.","/");
byte[] bytes = FileUtil.readBytes(classPath + "/" + name + ".class");
return bytes;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader=new MyClassLoader("E:/test");
Class<?> clazz = classLoader.loadClass("com.springli.microservice.gateway.jvm.part1.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("initData");
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
情况1:自己定义的加载器路径下有指定类,同时CLASS_PATH下也有该类的情况。
此时执行main方法输出的类加载器会是什么?
很显然,肯定是AppClassLoader。因为双亲委派机制
情况2:只在自己定义的加载器路径下有指定类
- 自定义类加载器(打破双亲委派机制)
打破双亲委派机制,则需要重写loadClass方法。
package com.springli.microservice.gateway.jvm.part1;
import cn.hutool.core.io.FileUtil;
import java.lang.reflect.Method;
/**
* @program: micro-service-frame
* @ClassName MyClassLoaderDemo
* @description: 自定义类加载器(双亲委派机制)
* @author: SpirngLi
* @create: 2020-08-28 15:28
**/
public class MyClassLoaderDemo {
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath=classPath;
}
/**
* 重写findClass
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//加载类返回字节
byte[] data=loadByte(name);
///defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name,data,0,data.length);
}
/**
* 读取文件返回字节
* @param name
* @return
*/
private byte[] loadByte(String name) {
name=name.replaceAll("\\.","/");
byte[] bytes = FileUtil.readBytes(classPath + "/" + name + ".class");
return bytes;
}
/**
* 重写类加载方法,实现自己的加载逻辑,打破双亲委派,不委托给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
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;
}
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader=new MyClassLoader("E:/test");
Class<?> clazz = classLoader.loadClass("com.springli.microservice.gateway.jvm.part1.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("initData");
method.invoke(obj,null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
执行上述代码会报错,知道为什么吗?
因为上面的代码是用自己的加载器进行加载,并不会有父类加载器,所以User里面的其它类都会找不到,除非把里面涉及的类全部拷贝到指定的目录下,这样显然很麻烦。那如何解决呢?
其实也比较简单,就是自己指定的包路径下用自己的加载器,其它的还用原来的双亲委派即可,所以上面的loadClass方法可以稍微修改下即可:
if(name.startsWith("com.springli.microservice.gateway.jvm")){
c = findClass(name);
}else{
c=this.getParent().loadClass(name);
}
那么现在是否有一个这样的想法,既然可以打破双亲委派,不让父类去加载,那么我是否可以去写个核心库的类,比如java.lang.String类,我加载我写的这个String类,可以吗?
答案其实想一想都知道肯定是不可能,不可能去修改得了核心包,否则就非常不安全。 可以实验一下:
package java.lang;
public class String {
public String() {
}
public int init() {
System.out.println("------执行了自定义init方法-----");
return 10;
}
public static void main(String[] args) {
System.out.println("------执行了自定义String类-----");
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader=new MyClassLoader("E:/test");
Class<?> clazz = classLoader.loadClass("java.lang.String");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("init");
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}