JAVA虚拟机(JVM)系列:
JAVA虚拟机(JVM)一:了解JAVA体系结构
JAVA虚拟机(JVM)二:JVM工作原理
一、前言
前一节,介绍了JVM的工作原理,其中提及到几个重要的组成部分
1、类装载器(ClassLoader)子系统: 用来装载.class文件,装载具有适合名称的类或接口
2、运行时数据区:JAVA内存区域
3、执行引擎 :Execution Engine执行字节码,或者执行本地方法,负责执行包含在已装载的类或接口中的指令
4、本地接口: Native Interface负责调用本地接口。他的作用是调用不同语言的接口给JAVA用,他会在Native Method Stack中记录对应的本地方法,然后调用该方法时就通过Execution Engine加载对应的本地lib。
其中,本地接口这个已经描述的非常清楚,而且它的实现是面向各OS的底层的,比较难懂,这里不做深入研究,其它三个部分是JVM的基础部分,我们必须要理解的。
对于这三个部分,相信大家都跟我一样,比较雾里看花。故,将进行详细学习分析。
这里,我们首先拿类装载器(ClassLoader)子系统开刀。
前提:本文讲的基本都是以Sun HotSpot虚拟机为基础的
二、类加载器基本概念
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。但第二次实例化一个类时,就从对应Class类newInstance(),不用每次都读取.class文件。
三、 JVM将整个类加载过程划分为了三个步骤:
(1)装载
装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+包名+ClassLoader实例ID。
(2)链接
链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。最后一步为对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如public、private域权限等),会造成NoSuchMethodError、NoSuchFieldError等错误信息。
(3)初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:调用了new;反射调用了类中的方法;子类调用了初始化;JVM启动过程中指定的初始化类。
步骤只做简单介绍,详见另一篇文章 Android热更新一:JAVA的类加载机制
四、类加载器的代理模式(双亲委派模型)
1、JVM两种类装载器包括:启动类装载器和用户自定义类装载器:
启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。
主要分为以下几类:
(1) Bootstrap ClassLoader
这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。
(2) Extension ClassLoader
JVM用此classloader来加载扩展功能的一些jar包
(3) System ClassLoader
JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。
(4) User-Defined ClassLoader
User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录
2、类加载器的代理模式(双亲委派模型)
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
2.1 模型说明
- 上层类加载器 = 下层类加载器的父类加载器;
- 双亲委派模型要求,所有类加载器都有父类加载器,除了顶层的启动类加载器;
- 类加载器之间的父子关系,以组合关系,来复用父类加载器的代码,而不是以继承关系来实现;
- 双亲委派模型设计者推荐给开发者的一种类加载器实现方式,而不是强制的约束模型。
2.2 工作流程讲解
双亲委派模型的工作流程代码实现在java.lang.ClassLoader的loadClass()中
具体如下
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
// 检查需要加载的类是否已经被加载过
if (c == null) {
try {
// 若没有加载,则调用父加载器的loadClass()方法
if (parent != null) {
c = parent.loadClass(name, false);
}else{
// 若父类加载器为空,则默认使用启动类加载器作为父加载器
c=findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若父类加载器加载失败会抛出ClassNotFoundException,
//说明父类加载器无法完成加载请求
}
if(c==null){
// 在父类加载器无法加载时
// 再调用本身的findClass方法进行类加载
c=findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
步骤总结:若一个类加载器收到了类加载请求
把 该类加载请求 委派给 父类加载器去完成,而不会自己去加载该类
每层的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中
只有当 父类加载器 反馈 自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载
2.3 优点
Java类随着它的类加载器一起具备了一种带优先级的层次关系
如:类 java.lang.Object(存放在rt.jar中)在加载过程中,无论哪一个类加载器要加载这个类,最终需委派给模型顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
若没有使用双亲委派模型(即由各个类加载器自行去加载)、用户编写了一个java.lang.Object的类(放在ClassPath中),那系统中将出现多个不同的Object类,Java体系中最基础的行为就无法保证
在讲完系统的类加载器后,下面我将讲解如何根据需求自定义类加载器。
2.4 确立 被加载类 在 Java虚拟机中 的 唯一性
确定 两个类是否 相等 的依据:是否由同一个类加载器加载
若 由同一个类加载器 加载,则这两个类相等;
若 由不同的类加载器 加载,则这两个类不相等。
即使两个类来源于同一个 Class 文件、被同一个虚拟机加载,这两个类都不相等
在实际使用中,是通过下面方法的返回结果(Boolean值)进行判断:
Class对象的equals()方法
Class对象的isAssignableFrom()方法
Class对象的isInstance()方法
当然也会使用instanceof关键字做对象所属关系判定等情况
实例说明
下面我将举个例子来说明:
public class Test {
// 自定义一个类加载器:myLoader
// 作用:可加载与自己在同一路径下的Class文件
static ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (!name.equals("com.carson.Test"))
return super.loadClass(name);
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(fileName);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
public static void main(String[] args) throws Exception {
Object obj = myLoader.loadClass("com.carson.Test");
// 1. 使用该自定义类加载器加载一个名为com.carson.Test的类
// 2. 实例化该对象
System.out.println(obj);
// 输出该对象的类 ->>第一行结果分析
System.out.println(obj instanceof com.carson.Test);
// 判断该对象是否属于com.carson.Test类 ->>第二行结果分析
}
}
<-- 输出结果 -->
class com.carson.Test
false
// 第一行结果分析
// obj对象确实是com.carson.Test类实例化出来的对象
// 第二行结果分析
// obj对象与类com.huachao.Test做所属类型检查时却返回了false
// 原因:虚拟机中存在了两个Test类(1 & 2):1是由系统应用程序类加载器加载的,2是由我们自定义的类加载器加载
// 虽然都是来自同一个class文件,但由于由不同类加载器加载,所以依然是两个独立的类
// 做对象所属类型检查结果自然为false。
五、自定义类加载器
主要是通过继承自ClassLoader类 从而自定义一个类加载器
MyClassLoader.java
// 继承自ClassLoader类
public class MyClassLoader extends ClassLoader {
// 类加载器的名称
private String name;
// 类存放的路径
private String classpath = "E:/";
MyClassLoader(String name) {
this.name = name;
}
MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public Class<?> findClass(String name) {
byte[] data = loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
try {
name = name.replace(".", "//");
System.out.println(name);
FileInputStream is = new FileInputStream(new File(classpath + name
+ ".class"));
byte[] data = new byte[is.available()];
is.read(data);
is.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
下面我将用一个实例来说明如何自定义类加载器 & 使用。
步骤1:自定义类加载器MyClassLoader
MyClassLoader.java
// 继承自ClassLoader类
public class MyClassLoader extends ClassLoader {
// 类加载器的名称
private String name;
// 类存放的路径
private String classpath = "E:/";
MyClassLoader(String name) {
this.name = name;
}
MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public Class<?> findClass(String name) {
byte[] data = loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
try {
name = name.replace(".", "//");
System.out.println(name);
FileInputStream is = new FileInputStream(new File(classpath + name
+ ".class"));
byte[] data = new byte[is.available()];
is.read(data);
is.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
步骤2:定义待加载的类
TestObject.java
public class TestObject {
public void print() {
System.out.println("hello DiyClassLoader");
}
}
步骤3:定义测试类
Test.java
public class Test {
public static void main(String[] args) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
MyClassLoader cl = new MyClassLoader("myClassLoader");
// 步骤1:创建自定义类加载器对象
Class<?> clazz = cl.loadClass("com.carson.TestObject");
// 步骤2:加载定义的测试类:myClassLoader类
TestObject test= (TestObject) clazz.newInstance();
// 步骤3:获得该类的对象
test.print();
// 输出
}
}
// 输出结果
hello DiyClassLoader
六、ClassLoader抽象类提供了几个关键的方法:
loadClass
此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法
findLoadedClass
此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。
findClass
此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
findSystemClass
此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null。
defineClass
此方法负责将二进制的字节码转换为Class对象
resolveClass
此方法负责完成Class对象的链接,如已链接过,则会直接返回。
七、结语
类加载器的工作原理基于三个机制:委托、可见性和唯一性。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。我们自己写的类加载器的时候应该严格遵守这三条机制。
OK, 类加载器子系统介绍到这里,本人能力有限,如果有描述不当的地方,麻烦指出。