JVM 是 Java Virtual Machine(Java虚拟机)的缩写,它是一种规范,HotSpot VM是其最主流的实现(其他实现),通常我们讨论JVM如果没有特意说明是何种实现,便指的是HotSpot VM。JVM也并非仅支持Java语言,任何可编译为字节码的编程语言能可以运行在JVM上,例如前不久谷歌在 I/O 2017宣布将作为 Android 开发 First-Class 语言的 Kotlin。理解Class文件的构成,类是通过何种机制被加载进JVM,这有助于我们更进一步的理解JVM,希望本文能使你对Class文件与类加载机制有一个初步的认识。
Class文件
Class 文件指的是以.class
为后缀的文件,它包含可被JVM执行的字节码,通常由JVM平台编程语言源代码文件(例如.java、.kt、.groovy
文件等)编译而来,也可通过字节码工具生成(例如ASM),当然如果你想手写字节码我也不会拦你。
一个Class文件通常由上图中这10个部分组成,本文不再敖述每部分所代表的意思,相信你看完下面这两篇文章应该会对其有一个大致的理解。
类加载机制
-
ClassLoader
ClassLoader 就是类加载器,它的唯一职责就是将Class文件加载到JVM中,通常开发者并不需要自己创建ClassLoader,但在框架、中间件中自定义ClassLoader 非常常见,Tomcat便极具代表性,通过自定义的Tomcat Classloader体系 实现应用的相互隔离。
在 Java 中默认提供了三个类加载器,分别是
BootstarapClassLoader
、ExtClassLoader
、AppClassLoader
,它们各自只负载加载规定目录内的Class文件,结构关系及目录见上图。public class Test { public static void main(String[] args) { System.out.println(Test.class.getClassLoader()); System.out.println(Test.class.getClassLoader().getParent()); System.out.println(Test.class.getClassLoader().getParent().getParent()); /* 输出: sun.misc.Launcher$AppClassLoader@3da997a sun.misc.Launcher$ExtClassLoader@4921a90 null*/ } }
注:
AppClassLoader
和ExtClassLoader
由 Java 编写并且都是java.lang.ClassLoader
的子类,而BootstarapClassLoader
并非由 Java 实现而是由C++
实现,所以打印结果为null
。 -
双亲委派制
简单的来说,双亲委派制就是当加载一个Class文件时会先交由上层ClassLoader来加载,如果发现已加载则直接返回,如果没有加载则去当前ClassLoader 的classes目录寻找该Class文件,找到则加载,找不到则交由下层ClassLoader来继续加载,如果直到最下层加载器都无法加载(找不到该Class文件)则抛出
ClassNotFoundException
异常。下面通过解读java.lang.ClassLoader
的loadClass(String name, boolean resolve)
源码来进一步了解该机制是如何运转的,代码如下所示。protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 首先,检查该Class是否已经被加载,如果已加载直接返回。 Class c = findLoadedClass(name); // 没有被加载 if (c == null) { long t0 = System.nanoTime(); try { //是否存在上层加载器,如果存在交由上层加载器加载 if (parent != null) { c = parent.loadClass(name, false); } else {//如果不存在继续向上委派给BootstarapClassLoader加载 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; } }
-
类的加载过程
Class Lifecycle
加载
在加载阶段,虚拟机需要完成以下3件事情:- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注:虚拟机规范的这3点要求其实并不算具体,因此虚拟机实现与具体应用的灵活度都是相当大的
相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器区完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。
对于数组类而言,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器任然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害JVM自身的安全。比如验证“魔数”是否为0xCAFEBABE
、Class文件编译版本号是否符合当前JVM等。Java语言本身是相对安全的语言,使用纯粹的Java代码无法做到注入访问数组边界意外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。
但前面已经说过,Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生Class文件。在字节码语言层面上,上述Java代码无法做到的事情都是可以实现的,至少语义上是可以表达出来的。
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
准备
准备阶段将为静态变量申请内存,并赋予初始值(基本类型为其默认值,引用类型为null
),假设有如下代码:public static int value = 123;
在该阶段
value
的值将根据其类型int
初始化为0
。而将value
赋值为123
的动作在初始化阶段才会执行(调用<clinit()>
方法,执行putstatic
指令)。解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。初始化
在初始化阶段会调用类的初始化方法<clinit()>
为静态变量赋予实际的值(例如将value
赋值为123
)、执行静态代码块。如果你觉得上的面描述太过臃肿,下面是我总结出的一个脑图(查看源文件),供你参考。
-
实现一个自定义ClassLoader
在实际开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名 Java 类的隔离等。创建自己的类加载器并不是一件复杂的事情,只需要继承自
java.lang.ClassLoader
类并覆写对应的方法即可。java.lang.ClassLoader
中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:- defineClass():这个方法用来完成从Java字节码数组到
java.lang.Class
对象的转换,由本地方法实现,通常不会去覆写该方法。 - findLoadedClass():这个方法用来根据名称查找已经加载过的Class。一个类加载器不会重复加载同一名称的Class。
- findClass():这个方法用来根据名称查找并加载Class。
- loadClass():这个方法用来根据名称加载Class,并且实现了双亲委派制。
- resolveClass():这个方法用来链接一个Class。
通常我们实现一个自定义ClassLoader 只需继承
java.lang.ClassLoader
并覆写protected Class<?> findClass(String name)
即可,如果你要打破双亲委派制则需要同时覆写protected Class<?> loadClass(String name, boolean resolve)
,下面将展示如何实现一个自定义ClassLoader。首先创建一个测试类并编译为Class文件以供稍后测试使用,本文将该Class文件放置在E:\classes下。
public class Test { public void say (){ System.out.println("Hello"); } }
实现自定义ClassLoader
public class CustomClassLoader extends ClassLoader { private final String classesDir; public CustomClassLoader(String classesDir) { this.classesDir = classesDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String fileName = name; if (fileName.indexOf('.') > 0) { fileName.replaceAll(".", "\\"); } fileName = fileName + ".class"; try { try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(classesDir + fileName))) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int read = bin.read(buffer); while (read != -1) { out.write(buffer); } byte[] data = out.toByteArray(); return defineClass(name, data, 0, data.length); } } } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); } public static void main(String[] args) throws Exception { //加载Test.class ClassLoader classLoader = new CustomClassLoader("E:\\classes\\"); Class<?> clazz = classLoader.loadClass("Test"); //通过反射调用say()方法 Object instance = clazz.newInstance(); Method method = clazz.getMethod("say", null); method.invoke(instance);//Hello } }
- defineClass():这个方法用来完成从Java字节码数组到
- 参考
- 《深入理解Java虚拟机(第2版)》
本文对你有帮助?欢迎扫码加入后端学习小组微信群:
