谈谈Java虚拟机2——类加载器
类从被加载到虚拟机内存中开始,到卸装出内存为止,它的整个生命周期包括了:加载,验证,准备,解析,初始化,使用和卸载七个阶段。其中验证、准备和解析三个部分称为连接,也就是说,一个Java类从字节代码到能够在JVM中被使用,需要经过加载、链接和初始化这三个步骤 。我们看一看Java虚拟机的体系结构。
Java虚拟机的体系结构如下图所示:
- 类装载器子系统,它根据给定的完整类名来装载类或接口
- 执行引擎,它负责执行那些包含在被装载类的方法中的指令。
- 方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后,它把这些类型信息入到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
- Java栈是由许多栈帧或者帧组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时。虚拟机压入一个新的栈帧到该线程的Java栈中;当该方法返回时,这个栈帧就会从Java栈中被弹出或者抛弃。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑。同时也使于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外,Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。
我们将按照类的生命周期来谈谈Java虚拟机的工作,本篇我们先谈谈加载:
Java类加载的全过程,是加载、验证、准备、解析和初始化这五个阶段的过程。而加载阶段是类加载过程的一个阶段。在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
在这三件事情中,通过一个类的全限定名来获取定义此类的二进制字节流这个动作是在Java虚拟机外部来实现的,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。
站在Java虚拟机的角度讲,只存在两种不同的类加载喊叫:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(只限于Hot Spot,像MPR、Maxine等虚拟机,整个虚拟机本身都是由Java编写的,Bootstrap ClassLoader自然也是由Java语言实现的),是虚拟机自身的一部分;另外一种用户自定义类装载器。前者是Java虚拟机实现的一部分,后者是Java程序的一部分。由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中。从Java开发人员的角度来看,类加载器还可以划分的更细致一些,分为系统提供的类加载器与用户自定义的类加载器。
系统提供的类加载器:
系统提供的类装载器主要由下面三个:
- 启动类加载器(bootstrap classloader):它用来加载 Java 的核心库,是用原生代码(本地代码,与平台有关)来实现的,并不继承自java.lang.ClassLoader。 这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟 机识加的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java 程序直接引用。
- 扩展类加载器(extensions classloader):扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量java.ext.dir 指定位置中的类库加载到内存中
- 应用程序类加载器(application classloader):系 统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,由于这个类加载器是ClassLoader 中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序默认的类加载器。
用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader类。ClassLoader中定义的方法为程序为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其它对象一样,用户自定义的类装载器以有Class类的实例都放在内存中的堆区,而装载的类型信息则都放在方法区。
下面介绍一下java.lang.ClassLoader类:
为了完成加载类的这个职责,
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。ClassLoader提供了一系列的方法,比较重要的方法如下表所示:
方法 | 说明 |
---|---|
getParent() |
返回该类加载器的父类加载器。 |
loadClass(String name) |
加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) |
查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) |
查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) |
把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) |
链接指定的 Java 类。 |
类装载器的特征
Java 类加载器有两个比较重要的特征:层次组织结构和代理模式。这两个特征也就是我们平时说的类加载器的双亲委派模型。层次组织结构指的是除了顶层为启动类加载 器之外,其余的类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结 构。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。代理模式则指的是一个类加载器既可以自己完成 Java类的定义工作,也可以代理给其它的类加载器来完成。
- 类加载器的树状组织结构
在系统提供的类加载器中,除了启动类加载器之外,所有的类加载器都有一个父类加载器。通过
getParent()
方 法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是启动类加载器;对于开发人员编写的类加 载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根 节点就是引导类加载器。
下面代码演示了类加载器的树状组织结构
/**
* @param args
*/
public static void main(String[] args) {
ClassLoader loader = ClassloaderTree.class.getClassLoader();
while(loader!=null){
System.out.println(loader);
loader = loader.getParent();
}
}
}
打印出来的结果为:
sun.misc.Launcher$ExtClassLoader@ad3ba4
第一个输出的是 ClassLoaderTree
类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader
类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()
方法返回 null
代理模式
代 理模式说的是双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一 个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找 到所需的类)时,子加载器才会尝试自己去加载。
那 么为什么要使用代理模式呢,每个类加载器都由自己加载不是很好吗,搞得这么复杂干吗?要解释这个,就得首先说明Java虚拟机是如何判定两个类是相同的。 在Java虚拟机中,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,通俗来说,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加 载器加载之后所得到的类,也是不同的。这个说的相同,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、 isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等情况。
下面我们通过编写自定义的类加载器来分别加载同一个class文件,进而确定得到的类是否相同。
在 上表中列出的 java.lang.ClassLoader
类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)
方法即可。java.lang.ClassLoader
类的方法loadClass()
封装了前面提到的代理模式的实现(loadClass的具体实现如下所示)。该方法会首先调用 findLoadedClass()
方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()
方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()
方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()
方法,而是覆写findClass()
方法。
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在本例中,我们为了说明两个类加载器加载同一个class会得到不同的类,所以重写loadClass方法,如果只重写findClass方法,会由双亲来加载class文件,得不到想到的效果。
自定义的ClassLoader代码如下:
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if( is == null){
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
}
代码很简单,就是加载与MyClassLoader同目录下的class类文件。
写一个辅助类
public class Sample {
private Sample instance;
public void setSample(Object obj){
this.instance = (Sample)obj;
}
}
写一个测试的类:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
/**
* @param args
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws NoSuchMethodException
* @throws SecurityException
* @throws InvocationTargetException
* @throws IllegalArgumentException
*/
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
MyClassLoader loader1 = new MyClassLoader();
MyClassLoader loader2 = new MyClassLoader();
Class class1 = loader1.loadClass("com.xiaoruoen.test.Sample");
Class class2 = loader2.loadClass("com.xiaoruoen.test.Sample");
Object obj1 = class1.newInstance();
Object obj2 = class2.newInstance();
Object obj3 = loader1.loadClass("com.xiaoruoen.test.ClassLoaderTest");
System.out.println(obj3);
System.out.println(obj3 instanceof com.xiaoruoen.test.ClassLoaderTest);
Method method = class1.getMethod("setSample", java.lang.Object.class);
method.invoke(obj1, obj2);
}
}
上面代码运行的结果为:
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.xiaoruoen.test.ClassLoaderTest.main(ClassLoaderTest.java:29)
Caused by: java.lang.ClassCastException: com.xiaoruoen.test.Sample cannot be cast to com.xiaoruoen.test.Sample
at com.xiaoruoen.test.Sample.setSample(Sample.java:8)
... 5 more
从给出的结果来看:
给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException
异常。虽然两个对象 obj1
和 obj2
的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。同样,我们使用了MyClassLoader加载了一个名为com.xiaoruoen.test.ClassLoaderTest 的类,并实例化了这个类的对象。两行输出结果中,从第一句可以看到这个对象确实是类 com.xiaoruoen.test.ClassLoaderTest实例化出来的对象,但从第二句可以发现这个对象与类com.xiaoruoen.test.ClassLoaderTest 做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,一个是由 我们自定义的类加载器加载的,虽然都是来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然是false。
了解了这一点之后,就可以理解代理模式的设计动机了。
代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object
类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object
类(由上面的例子就可以看出来),而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread
中的方法 getContextClassLoader()
和setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前
面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service
Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和
JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers
包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的
Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory
类中的newInstance()
方法用来生成一个新的 DocumentBuilderFactory
的实例。这里的实例的真正的类是继承自javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线 程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。