JVM系列之:关于JVM类加载的那些事
在上一篇文章中我们知道 Java 语言的类型可以分为两大类:基本类型(primitive types)和引用类型(reference types)。比如 Java 的基本类型,它们是由 Java 虚拟机预先定义好的。
Java 引用类型主要分为四种:
类、接口、数组类和泛型参数
。由于泛型参数会在编译过程中被擦除,因此 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流。
说到字节流,最常见的形式要属由 Java 编译器生成的 class 文件。Java 使用编译器将源码文件编译得到 class 文件时,会严格按照 Java 虚拟机规范进行校验,关于这部分内容在《Java虚拟机规范》一书中的第四章节做了详细介绍,这里就不介绍了。
无论是直接生成的数组类,还是非数组的类或接口,都要经过一系列步骤后才能被 JVM 直接使用,其中包括如下步骤:对数据进行校验、转换解析、初始化等等,这个说来简单但实际复杂的过程叫做
JVM 的类加载机制
。
Class 文件中的“类”从加载到 JVM 内存中,到卸载出内存过程有七个生命周期阶段。类加载机制包括了前五个阶段。
如下图所示:
其中,加载、验证、准备、初始化、卸载的开始顺序是确定的,注意,只是按顺序开始,进行与结束的顺序并不一定。解析阶段可能在初始化之后开始。
另外,类加载无需等到程序中“首次使用”的时候才开始,JVM 预先加载某些类也是被允许的。(类加载的时机)
加载
我们平常说的加载大多不是指的类加载机制,只是类加载机制中的第一步加载。加载是指查找字节流,并且据此创建类的过程。在这个阶段,JVM 主要完成三件事:
1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过 jar包、war 包、网络中获取、JSP 文件生成等方式。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。
对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。
Java 虚拟机支持两种类加载器:Java 虚拟机提供的引导类加载器和用户自定义的类加载器。每个用户自定义的类加载器应该是抽象类 ClassLoader 的某个子类的实例。
关于 JVM 提供的类加载器,介绍如下:
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader,其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
- SystemClassLoader(应用程序类加载器) :面向我们用户的加载器,它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
类加载器虽然只用于实现类的加载动作,但其作用不止于此,还可以用来比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,如果两个类被同一个类加载器加载,那么再进行更深一步的比较;但是加载这两个类的类加载器不同,那么这两个类必定不相等。
这里说的“相等”,包括代表类的 Class 对象的 equals()方法、 isAssignableFrom()方法、 isInstance()方法的返回结果, 也包括了使用instanceof 关键字做对象所属关系判定等各种情况。
我们通过如下案例来看看不同的类加载器生成的类对象是否“相等”。
1、定义一个实体类 Product
package com.msdn.java.hotspot.classLoader;
public class Product {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2、测试
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Class<?> cl = myLoader.loadClass("com.msdn.java.hotspot.classLoader.Product");
System.out.println(cl.getC