前言:虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(java.lang.Class对象),这就是java虚拟机的类加载机制。(摘抄自《深入java虚拟机》,周志明著。)
一、类加载的过程
类的生命周期从加载进内存开始直到卸载出内存为止,期间经历了加载、验证、准备、解析、初始化、使用和卸载七个过程,其中验证、准备、解析三个过程统称为连接。除了使用和卸载之外,java虚拟机在类加载的过程中,会按部就班的“开始”这些过程,注意这里用的是按部就班的“开始”而不是按部就班的“运行”或“完成”,简而言之,这五个过程会顺序开始,但无需等到上一个过程结束下一个过程才能开始运行,实际上他们通常都是相互交叉混合着运行,但这并不妨碍java虚拟机的顺序“开始”。
- 加载
加载就是虚拟机将java字节码文件加载进内存的方法区转化成运行时数据结构,并在内存中创建相应的java.lang.Class对象作为方法区中这个类的数据访问入口。也即是文件静态的存储结构变成运行时数据结构。
注意:数组类本身不通过类加载器创建,而是由java虚拟机直接创建,但是数组的元素类型还是需要类加载器来创建,特别注意的是,如果数据类型是基本数据类型,则将会由根加载器(bootstrap loadClass)来加载。
- 验证
验证的目的是为了确保class文件符合虚拟机规范,且不会危害虚拟机安全。主要包含以下四个方面:
a. 文件格式验证:验证class文件是否符合虚拟机格式,例如前四个字节是否是以魔数0xCAFEBABE开头,第五六个字节是否是次版本,第七八个字节是否是主版本等信息;
b. 元数据验证:从语义级别分析、检查文件内容是否符合java语言规范;
c. 字节码验证:从数据流和控制流级别分析、检查该文件内容是否符合java虚拟机规范;
d. 符号引用验证:这一阶段的验证真正发生的时间是在解析过程中,目的是为了确保解析动作能够正常执行,例如验证根据某一个类的全限定名是否能找到该类。
- 准备
准备阶段是正式为“类变量”(static field)分配内存,并依次赋予初始默认值,“依次”的含义是根据程序声明变量的顺序来赋予默认值。
- 解析
将经过验证阶段验证的符号引用替换成直接引用,即替换成引用的地址值。
- 初始化
初始化是类加载过程的最后一步,到了该阶段,才开始真正的执行类定义中的java程序代码。在准备阶段,将会给静态变量赋予初始值,而在初始化阶段,将会根据程序员的代码内容来初始化类变量以及其他资源的值,即是赋予给定值。
首先明确一点:此处涉及的是类加载的初始化,而不是实例对象的初始化(注意:类初始化和对象初始化不一样),对应着的是clinit()类构造器和init()实例构造器。
该阶段是执行类构造器clinit()方法的过程(注意:不是实例构造器init()),类构造器和实例构造器不同,类构造器是由编译器自动收集类中所有类成员的赋值动作和静态代码块中的语句合并产生,收集顺序和类文件中定义顺序一致。所以,类变量随着类初始化的完成而完成,先于对象存在。
至此,类加载的过程全部完毕,类变量和方法均可以被使用;接下来就是对类进行实例化,生成并使用、销毁对象。
二、类加载器——原理
- 什么是类加载器
package vm;
/**
*
* question: 先运行B类,在运行C类,请问B和C中输出的结果是?
*
*/
public class A {
public static int a = 1;
}
package vm;
public class B {
public static void main(String[] args) {
A.a++;
System.out.println(A.a);
}
}
package vm;
public class C {
public static void main(String[] args) {
System.out.println(A.a);
}
}
b. 虚拟机如何避免对同一个类进行重复加载
- 类加载器简介
package class_loader;
import java.net.URL;
public class BootStrapDemo {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL i : urls)
System.out.println(i);
}
/*
* 打印结果: file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/resources.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/rt.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/sunrsasign.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jsse.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jce.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/charsets.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/lib/jfr.jar
* file:/C:/Program%20Files/Java/jdk1.8.0_11/jre/classes
*/
}
package class_loader;
public class ExtensionDemo {
public static void main(String[] args) {
ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extensionClassLoader);
System.out.println("扩展类加载器路径: " + System.getProperty("java.ext.dirs"));
}
/*
* 输出结果: sun.misc.Launcher$ExtClassLoader@7852e922
* <span style="font-family: Arial, Helvetica, sans-serif;">扩展类加载器路径:</span><span style="font-family: Arial, Helvetica, sans-serif;">C:\Program Files\Java\jdk1.8.0_11\jre\lib\ext;C:\Windows\Sun\Java\lib\ext</span>
*/
}
package class_loader;
public class AppClassLoader {
public static void main(String[] args) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);
}
/*
* 输出结果:
* sun.misc.Launcher$AppClassLoader@2a139a55
*/
}
- 类加载机制
a. 缓存机制:同批次虚拟机中,被相同类加载器加载过的类将不会再次被加载进内存。
package overall;
/**
* 全盘负责:类加载器加载某一个类时,该类中所依赖和引用的其他类,也将由此类加载器加载。
* @author reliveIT
*
*/
public class A {
public A(){
System.out.println("A: "+A.class.getClassLoader());
}
}
package overall;
public class B {
private A a;
public B(A a) {
System.out.println("B: "+B.class.getClassLoader());
this.a = a;
}
public static void main(String[] args) {
B b = new B(new A());
}
/*
* 输出结果:
* A: sun.misc.Launcher$AppClassLoader@73d16e93
* B: sun.misc.Launcher$AppClassLoader@73d16e93
*/
}
package test;
/**
* @author reliveIT
*
*/
public class Demo {
public static void main(String[] args) {
System.out.println(Demo.class.getClassLoader().getClass().getName());
}
}
package java.lang;
public class Object {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
}
/*
* 输出结果:
* 错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args) 否则 JavaFX应用程序类必须扩展javafx.application.Application
*/
}
至此,完全可以证明双亲委托机制是向上加载优先,且全部传递到根类加载器。
- 双亲委托模型的优点
- 双亲委托模型的实现
查看java.lang.ClassLoader的实现代码,思想如下:如果该类已经被加载过了并且在虚拟机中存在缓存,则立刻返回;否则查看其是否有父类,如果有父类则让父类来加载(父类中的双亲委托模型实现代码也一致,最终会传递给跟加载器),否则直接让跟加载器来加载(没有父类,说明该加载器要么是扩展类加载器,要么就直接是根类加载器);要是这样加载也失败,说明父类加载器路径下不存在这个文件,此时交还给该类自己的加载器,调用自己的findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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;
}
}
三、类加载器——实践(自定义类加载器)
- 自定义类加载器
当我们在网络通信中需要传输相关的字节码的时候,往往会对传输的内容进行加密。这时候接收到的内容是经过加密的,就需要自定义一个类加载器来实现解码。
从类加载机制的双亲委托模型原理来看,可以重写父类的loadClass(String name)来实现自定义类加载器,也可以通过重写findClass(String name)方法来实现自定义类加载器。二者实现方式的区别在于,如果重写loadClass方法,则有可能破坏双亲委托模型中向上加载优先的机制,否则就需要实现更多的代码从而增加编码负担。因此建议尽量重写findClass方法,而且在JDK1.2以后也首推重写findClass方法来实现自定义类加载器。
- 代码示例
场景:通过自定义的类加载器加载Test.class文件,如果发现Test类还未编译,则先编译后加载。
package myClassLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 自定义类加载器:根据全限定类名来加载Demo类的字节码文件,如果该类还未被编译,则先编译后加载字节码
* 附注:仅供参考,如果错误,烦请指正,不胜感激!
* @author reliveIT
*
*/
public class MyAppClassLoader extends ClassLoader {
//编译文件
private boolean compileFile(String name){
Process p = null;
name = name.replaceAll("\\.", File.separator+File.separator)+".java";
try {
p = Runtime.getRuntime().exec("javac " + name);
p.waitFor();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("编译完成!");
return p.exitValue() == 0;
}
//加载字节码
private byte[] getClassData(String name) throws FileNotFoundException {
String fileName = name.replaceAll("\\.", File.separator+File.separator)+".class";
File classFile = new File(fileName);
if(!classFile.exists()){
if(!compileFile(name))
throw new FileNotFoundException("没有发现文件!");
}
FileInputStream fis = new FileInputStream(classFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bArray = null;
try {
bArray = new byte[fis.available()];
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int hasRead = -1;
try {
while((hasRead = fis.read(bArray)) != -1){
baos.write(bArray, 0, hasRead);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
System.out.println("已经将字节码转换成二进制数据流写入内存!");
return baos.toByteArray();
}
@Override
protected Class<?> findClass(String name) {
// TODO Auto-generated method stub
byte[] classData = null;
try {
classData = getClassData(name);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(classData == null){
throw new RuntimeException("类加载失败!");
}
return defineClass(name.substring(name.indexOf(".")+1), classData, 0, classData.length);
}
}
package myClassLoader;
public class Main {
public static void main(String[] args) {
MyAppClassLoader app = new MyAppClassLoader();
Class<?> c = null;
try {
c = app.loadClass("src.myClassLoader.Test");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(c.getName());
}
}
package myClassLoader;
public class Test {
}
结果:在存放Test.java文件的文件夹下生成字节码文件Test.class。
附注:
本文遗漏、错误之处,烦请指正,不胜感激!