一篇文章搞定JVM如何加载.class文件
文章目录
一、Java虚拟机
JVM是一个内存中的虚拟机,主要运用内存存储,所有类、类型、方法,都是在内存中,这决定着我们的程序运行是否健壮、高效。
二、.class文件加载执行流程图
- Class Loader(类加载器):依据特定格式,加载class文件到内存
- Execution Engine:(执行引擎)对命令进行解析,解析完毕之后就可以提交到操作系统里面执行了
- Native Interface:融合不同的开发语言的原生库为Java所用,性能并不如c/c++高,主流的JVM也是由c/c++实现的
- Runtime Data Area:JVM内存空间结构模型,所写的程序都会被加载到这里
三、一个类从编译到加载再到执行的流程
这里我们用一个例子来进行解释
package com.mtli.reflect;
/**
* @Description:
* @Author: Mt.Li
* @Create: 2020-04-25 13:55
*/
public class Robot {
private String name;
public void sayHi(String helloSentence){
System.out.println(helloSentence + " " + name);
}
private String throwHello(String tag){
return "Hello " + tag;
}
}
- 编译器将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转换为JVM中的Class对象
- JVM利用Class对象实例化为Robot对象
3.1、ClassLoader(类加载器)
-
ClassLoader 在Java中有着非常重要的作用,主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流加载进系统,然后交给Java虚拟机进行连接、初始化等操作。
-
它的种类如下
-
还有自定义ClassLoader:Java编写,定制化加载(加载的可以不是class,java,可以是其他类型的)
-
想要实现自定义ClassLoader,我们需要重写两个关键函数
- findClass(根据名称或者位置加载.class字节码,然后使用defindClass)和defineClass(生成.class)
-
可能有人说咋还要生成.class啊,因为findClass引入进来的可能是java语言编译成的.class也有可能是其他语言编译成的,甚至可能是自定义的,我们需要用defineClass将字节数组流解密之后,将该字节流数组生成JVM能够识别的字节码文件。
3.2、自定义CLassLoader
package com.mtli.reflect;
import java.io.*;
/**
* @Description:自定义ClassLoader
* @Author: Mt.Li
* @Create: 2020-04-25 15:37
*/
public class MyClassLoader extends ClassLoader{
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName){
this.path = path;
this.classLoaderName = classLoaderName;
}
// 重写findClass用于寻找类文件
// defineClass还是用Java自己的
@Override
public Class findClass(String name){
// 以二进制字节流传递进来,然后交给defind
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// 用于加载类文件
private byte[] loadClassData(String name) {
name = path + "/" + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try{
in = new FileInputStream(new File(name));
// 通过ByteArrayOutputStream解密
out = new ByteArrayOutputStream();
int i = 0;
// 没读完继续读
while((i = in.read()) != -1){
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 返回字节码
return out.toByteArray();
}
}
我们自定义一个Java类
public class Wali{
static{
System.out.println("Hello Wali");
}
}
用javac对其进行编译生成Wali.class文件
写一个测试类:
package com.mtli.reflect;
/**
* @Description:
* @Author: Mt.Li
* @Create: 2020-04-25 17:08
*/
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("C:/Users/Taogege/Desktop", "myClassLoader");
Class c = m.loadClass("Wali");
// 创建实例
c.newInstance();
}
}
执行结果:
Hello Wali
3.3、类加载器的双亲委派机制
上面我们在自定义ClassLoader中重写了它的findClass方法,对于实际应用中,我们也有必要了解findClass和loadClass的执行。
jdk1.8中的loadClass()源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先我们检查要加载的类是否加载过
Class<?> c = findLoadedClass(name);
// c为null,则递归该方法,到当前加载器的父级继续查询
if (c == null) {
// native方法
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
}
// 递归回来后,到这里说明根加载器也没有加载过,则开始检测能否
// 自己寻找并加载class
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;
}
}
先要说的
上面我们说了ClassLoader有多种,不同的ClassLoader加载类的方式和路径有所不同,为了实现分工,各自负责各自的区块,使得逻辑更加明确,加载类的时候各司其职,当然存在一个机制,让他们相互协作,形成一个整体,这个机制就是双亲委派机制。
由上图,我们看到左边是由下而上检测是否曾经加载过这个类,比如先检测用户自定义类加载器
是否加载过这个类,有则提交给JVM使用,节省时间,节省内存空间,没有则继续向它的父级查询,重复步骤,如果没有继续向上直到根加载器,如果还是没有,则会像如图右边,当前的类加载器会自己去加载当前的类,如果不能加载,则向下,看子加载器能否加载,直到最下边,如果还是不能加载,则抛出ClassNotFoundException 异常。这跟我们上边放出的源码流程是差不多的。
为什么要用双亲委派机制去加载类呢
内存空间是宝贵的,我们没有必要保存多份同样的类,所以每次加载类进行查询是否曾经加载过,也就是避免多个同样字节码的加载。
四、类的加载方式
- 隐式加载:new
- 这个 方式大家都知道,直接创建类
- 显式加载:loadClass,forName
4.1、loadClass和forName的区别
- Class.forName也是可以对类进行加载的,内部实际调用的方法是 Class.forName(className,true,classloader);
注意它的第二个参数为true,这个参数表示是否进行初始化,默认为true,它会让jvm对指定的类执行加载、连接、初始化操作。 - ClassLoader.loadClass:从上边的加载分析可以知道,它只负责连接,不会进行初始化等操作,所以static这样的静态代码是不会进行初始化的
文章的最后附上一张类的装载过程便于自己查看:
上一篇: 哨兵集群模式