深入JVM虚拟机(六) JVM类加载机制
1 JVM类加载机制
1.1 类加载机制概述
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶断。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图7-1所示。
类加载机制概述图1-1
1.2 加载(Loading)
加载是“类加载”(Class Loading)过程的一个阶段
1、通过类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成这个类的java.lang.Class对象,作为方法这个类的各种数据的访问入口
1.3 验证(Verification)
验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
1、文件格式验证:验证字节流是否符合Class文件格式的规范;
例如:
1、是否以魔术0xCAFEBABE开头。
2、主次版本号是否在当前虚拟机的处理范围之内。
3、常量池中的常量是否有不被支持的类型。
2、元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;操作对象:方法区中的类或接口的信息
例如:
1、这个类是否有父类,除了java.lang.Object之外。
2、是否继承了final类。
3、非抽象类实现了所有的抽象方法。
4、基本的语法检查。
3、字节码验证:对方法体语句进行语义分析,保证方法运行时不会出现危害JVM安全的事件。操作对象:方法区中的类信息的代码属性
例如:
1、检查操作数栈的数据类型与指令的操作数类型是否兼容。
2、检查跳转指令不会跳转到方法体外的字节码指令上。
3、保证方法体中的类型转换是有效。
4、符号引用验证:对类的符号引用和类的实际信息(类、字段、方法)进行验证,保证符号引用可成功解析为直接引用,并当前类可以成功访问直接引用。操作对象:方法区中的类或接口信息。
例如:
1、通过符号引用中字符串描述的全限定名是否可以在方法区中找到对应的类。
2、通过符号引用中对字段、方法的简单名和描述符是否可以在方法区找到对应的字段和方法。
3、当前实例是否有权限访问符号引用的类、字段和方法。
1.4 准备(Preparation)
准备:准备是阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。此时进行分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
public static intvalue = 1;
在准备阶段中,value值设置为0,在初始化<clinit>()中才会被设置为:1。对于static final类型,在准备阶段就会被赋上正确的值。
public static finalintvalue = 1;
基本数据类型的零值:
数据类型 | 零值 | 数据类型 | 零值 |
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | ’\u0000’ | reference | Null |
byte | (byte)0 |
|
|
1.5 解析(Resolution)
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
1.6 初始化(Initialization)
初始化:初始化是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
子类的<clinit>调用前保证父类的<clinit>被调用,<clinit>是线程安全的。
2 类的装载器ClassLoader
2.1 什么是类的装载器
ClassLoader是一个负责加载Classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,ClassLoader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。ClassLoader是一个抽象类,ClassLoader的实例将读入Java字节码将类装载到JVM中,ClassLoader可以定制,满足不同的字节码流获方式。ClassLoader负责类装过程中的加载阶段。
1、CloassLoader的重要方法:
/**
* 载入并返回一个Class
*/
public Class<?>loadClass(String name) throws ClassNotFoundException
/**
* 定义一个类,不公开调用
*/
protected finalClass<?> defineClass(byte[] b, int off, int len)
/**
* loadClass回调该方法,自定义ClassLoader的推荐做法
*/
protectedClass<?> findClass(String name) throws ClassNotFoundException
/**
* 查找已经加载的类
*/
protected finalClass<?> findLoadedClass(String name)
2.2 JVM预定义类型类加载器
JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:
1、启动类加载器(Bootstrap ClassLoader):引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2、扩展类加载器(ExtensionClassLoader):系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。
3、系统类加载器(AppClassLoader):调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
4、自定义(Custom ClassLoader):自定义ClassLoader,加载的是.class。
ClassLoader体系结构图1-2
5、loadClass实现代码
protectedClass<?> loadClass(String name, boolean resolve) {
synchronized(getClassLoadingLock(name)) {
// 首先检查name对应的Class是否已经被加载
Classc = findLoadedClass(name);
// 如果没有被加载
if (c == null) {
long t0 = System.nanoTime();
// 尝试让parentClassLoader去加载
try {
if (parent != null) {
// 当parent不为null的时候,让parent去loadClass
c = parent.loadClass(name, false);
} else {
// 当parent为null的时候,就调运本地方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 当parentClassLoader没有加载的时候
if (c == null) {
long t1 = System.nanoTime();
// 调运findClass方法去加载
c = findClass(name);
// 定义类装载器;记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.3 双亲委派模式
1、双亲委派模式(Parents DelegationModel):图《ClassLoader体系结构图1-2》展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(ParentsDelegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加器。这里类加载器之间的父子关系一般不会以继承(Inheritance)关系来实现,而是都使用组合(Composition)关系来复用父加载器代码。
2、双亲委派模式过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传到顶层的启动类加载器中,只有当父加载器反馈自己无法完成的这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己加载。
3、双亲委派模式问题:顶层的ClassLoader,无法加载底层的ClassLoader类。如:JNDI已经是JAVA的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service ProviderInterface)的代码,但启动类加载器不可能“认识”这些代码。
4、线程上下文类加载器(Thread ContextClassLoader):这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置。JNDI服务使用这个线程上下文类加载器去加载所需的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。Java中所有涉及SPI的动作基本上都采用这种方式,如JNDI、JDBC、JCE、JAXB和JBI等。
2.4 实现热部署代码
1、实现热部署代码:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.MessageFormat;
/**
* 动态加载class文件
*/
public class CustomClassLoader extends ClassLoader {
// 文件最后修改时间
private long lastModified;
// 加载class文件的classpath
private String classPath;
/**
* 检测class文件是否被修改
* @param filename
* @return
*/
private boolean isClassModified(String name) {
File file = getFile(name);
if (file.lastModified() > lastModified) {
return true;
}
return false;
}
public Class<?> loadClass(String classPath, String name) throws ClassNotFoundException {
this.classPath = classPath;
if (isClassModified(name)) {
return findClass(name);
}
return null;
}
/**
* 获取class文件的字节码
*
* @param name
* 类的全名
* @return
*/
private byte[] getBytes(String name) {
byte[] buffer = null;
FileInputStream in = null;
try {
File file = getFile(name);
lastModified = file.lastModified();
in = new FileInputStream(file);
buffer = new byte[in.available()];
in.read(buffer);
return buffer;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return buffer;
}
/**
* 获取class文件的真实路径
*
* @param name
* @return
*/
private File getFile(String name) {
String simpleName = "";
String packageName = "";
if (name.indexOf(".") != -1) {
simpleName = name.substring(name.lastIndexOf(".") + 1);
packageName = name.substring(0, name.lastIndexOf(".")).replaceAll("[.]", "/");
} else {
simpleName = name;
}
File file = new File(MessageFormat.format("{0}/{1}/{2}.class", classPath, packageName, simpleName));
return file;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteCode = getBytes(name);
return defineClass(null, byteCode, 0, byteCode.length);
}
}
2、实现业务层代码:
package classloader;
public class Hello {
public String sayHello(String name) {
return "Hello." + name;
}
}
3、测试代码:
package classloader;
import java.lang.reflect.Method;
public class TestCustomClassLoader {
public static void main(String[] args) throws Exception {
while (true) {
CustomClassLoader loader = new CustomClassLoader();
Class<?> clazz = loader.loadClass("E:\\Workspace\\classloader\\bin", "classloader.Hello");
Method method = clazz.getMethod("sayHello", String.class);
System.out.println(method.invoke(clazz.newInstance(), "Ken"));
// 每隔1秒钟重新加载
Thread.sleep(1000);
}
}
}
执行结果:
Hello.Ken
Hello.Ken
Hello.Ken
Hello1111.Ken
Hello1111.Ken
Hello1111.Ken
2.5 学习JVM推荐书籍:
1. 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》
3. 《实战Java虚拟机:JVM故障诊断与性能优化》
4. 《HotSpot实战》
5. 《揭秘Java虚拟机:JVM设计原理与实现》
6. 《深入理解JVM & G1 GC》
7. 《Java程序性能优化:让你的Java程序更快、更稳定》
——厚积薄发(yuanxw)