我们的代码是如何被加载到JVM执行的?(一)
流程:1.xxx.java -> 2.javac编译 -> 3.xxx.class -> 4.ClassLoader -> 5.字节码解释器、JIT -> 6.执行引擎执行
java文件编译成class文件
我们对java文件是熟悉的,但是对class文件是陌生的。JVM运行的是class文件,只要文件能够解释成class文件,那么都能在JVM上运行,将语言和执行代码分开(适配器模式思想),这也是“一次编译,到处运行”的由来。我们可以一起来了解下class文件的构成,对我们理解后续的过程是有帮助的。
上面这个明显很难看,在IDEA中可以使用插件“jclasslib Bytecode Viewer”进行辅助查看。
插件将描述被自动转成中文了,所以看下面的文件结构以及代表的含义:
ClassFileFormat(文件结构)
- Magic Number -- 魔数:代表本文件是.class文件 -- cafe babe
- Minor Version -- 次版本号,向下兼容
- Major Version -- 主版本号,向下兼容
- constant_pool_count -- 常量池个数
- constant_pool(长度为constant_pool-conunt-1的表) -- 常量池
- access_flags -- 访问标志(可进行位运算)
- this_class -- 当前类
- super_class -- 父类
- interfaces_count -- 实现几个接口
- interfaces -- 具体接口
- fields_count -- 属性个数
- fields -- 具体属性
- methods_count -- 方法个数
- methods -- 具体方法
- attributes_count - u2 -- 附加属性有哪些
- attributes -- 具体附加属性
以上是class文件的阅读,接下来将描述第四步ClassLoader的内容。
ClassLoader类加载器
列举常见的类加载器(级别由高到低):Bootstrap ClassLoader,Extension ClassLoader,App ClassLoader,Custom ClassLoader...
类加载器的工作步骤
1. Loading -- 加载
类加载器的加载机制:双亲委派机制(如下图)。当加载一个class文件时,如果自定义类加载器(A)已经加载过则直接返回Class对象,如果没有则通过类加载器(A)的parent属性找父类加载器(B),如果父类加载器(B)加载过则返回Class对象,没有则继续访问父类加载器(B)的parent属性找父类加载器(C)有没有加载过,直到Bootstap(最顶级的)也没加载过时,则按照这个class文件的路径依次向下询问是否为该类加载器的加载范围,如果是则进行加载,如果都不是,则由自定义类加载器加载出来。
双亲委派机制的优点:避免类重复加载、核心类被篡改等安全情况,主要是为了安全考虑。
双亲委派机制的缺点:如果某个类已经被某个类加载器加载出来(特别是Bootstrap),其他任何类加载器都不能对其进行修改,也不会重复加载其他的接口实现,但是开发者又确实需要修改这个接口的实现,那么则需要使用“SPI( 服务提供者接口)”机制,实现策略模式和热插拔效果。
/**
* 类加载器层级关系
*/
public class JVMTest {
public static void main(String[] args) {
ClassLoader classLoader1 = JVMTest.class.getClassLoader();
ClassLoader classLoader2 = classLoader1.getParent();
ClassLoader classLoader3 = classLoader2.getParent();
System.out.println("classLoader1 == " + classLoader1); // classLoader1 == sun.misc.Launcher$AppClassLoader@14dad5dc
System.out.println("classLoader2 == " + classLoader2); // classLoader2 == sun.misc.Launcher$ExtClassLoader@19469ea2
System.out.println("classLoader3 == " + classLoader3); // classLoader3 == null -->由于Bootstrap是由C++实现,java并没有该类,所以返回Null
}
}
ClassLoader源码解析
上面是理解ClassLoader是什么帮助大家,理解脉络。下面我们主要看下loadClass的源码,验证上面的脉络是否说的一致。
当要加载一个class时,入口方法是loadClass,下面是loadClass的伪代码。
protected Class<?> loadClass(String name, boolean resolve){
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查这个class是否已经被加载过
Class<?> c = findLoadedClass(name);
// 没有,则走if里面
if (c == null) {
// 先从父类加载器开始加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 如果父类加载器加载不出来则调用findClass查找,并将class对象生成出来
c = findClass(name);
}
}
if (resolve) {
// 如果resolve=true则用resolveClass()处理类
resolveClass(c);
}
return c;
}
}
resolveClass():链接指定的类。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接。
URLClassLoader类中的findClass()实现
protected Class<?> findClass(final String name){
final Class<?> result;
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
// 生成class对象
return defineClass(name, res);
} else {
return null;
}
}
}, acc);
return result;
}
2.Linking -- 链接
- Verification -- 验证 校验class文件,class文件需符合当前虚拟机的要求(例如上面class文件的开头“cafe babe”),保证class文件加载正确。
- Preparation -- 准备 静态变量赋默认值,对类中的静态字段分配内存空间,基础类型赋默认值(例如long=0L,int=0),引用类型默认为null。
- Resolution -- 解析
我们看class文件可知,类的很多信息的引用都放在了常量池里,如果使用的时候每次都去常量池里取,也比较低效,所以解析过程为类、接口、方法、成员变量的符号引用定位直接引用。
3.Initializing -- 初始化
静态变量赋值为初始值
下期预告
- 自定义ClassLoader
- 十分特殊的"上下文类加载器"
- 使用自定义ClassLoader实现热插拔式功能 JVM之自定义ClassLoader(二)