一、类加载机制:
在《深入理解Java虚拟机》一说阐述为虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。类的加载机制属于类的生命周期的一部分,如下图:
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。从图中我们可以了解到类的加载机制覆盖了类的大部分生命周期过程。而需要注意的是类加载的过程类加载的过程并不是按部就班的时序进行,而是相互交出混合进行,比如加载的同时,验证就已经开始了。
步骤解说:
加载:简单的说是就是JVM把编译好的.class文件通过二进制字节流按照虚拟机的所需的格式存储在方法区中,作为方法区中访问这个类各种数据的一个入口。
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中(HotSpot方法区中)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注意:
1、全限定类名获取的途径有很多,比如:从ZIP、JAR、EAR、WAR中读取、通过动态代理技术获取代理类的二进制字节流、从网络中获取(Applet)、JSP文件生成的class类、从数据库中读取等等
2、数据是比较特殊,数组本事是由Java虚拟机创建的,而不是通过类加载器创建的,但是数据的元素类型最终还是依靠类加载器去创建,如引用类型。
3、加载接口与连接阶段的部分是可以交叉进行的,加载和连接只是开始时间保持着固定的先后顺序。
连接阶段:
验证:验证的主要目的是为了确保class文件中字节流信息包含的信息符合当前虚拟机的要求,也是为了保护虚拟机的安全。对于不符合要求的代码编译器拒绝编码,这也是Java区别于C/C++相对安全的重要区别之一。同时呢class文件的来源也不一定Java文件,只要符合虚拟机规范的,以使用任何途经场景,还记得JVM的特点,“一次编译,到处运行”吧,验证的过程,就是一个检查点。验证大概会有四个阶段:
文件格式验证:是否以魔数0xCAFEBABE开头;主、次版本号是否在当前虚拟机处理范围之内等等
元数据验证:是否有父类、父类是否继承了不被允许继承的类(final修饰);类中的字段是否与父类有矛盾(不符合规范的重载)等等
字节码验证:程序语义的合法性、符合逻辑
符号引用验证:根据全限定名能否找到类,符号引用中饿类、字段、方法的访问性,比如:当找不到对应的字段或者方法时就会抛出java.lang.IncompatibleClassChangeError异常的子类,如jjava.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
不过一般情况写代码比较细心的童鞋,这块的东西是不需要考虑的。
准备:准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这些变量所使用的内存都将在方法区中分配,这些变量只包括类变量(static修饰),不包括实例变量,实例变量依然将会随着对象实例化时随对象一起分配到Java堆中。
比如: public static int a=5,在准备阶段的a的值其实是为0而不是5,需要注意的是,常量是在准备接口赋值的,如:
public static final int b=10,在准备接口b的值就被赋值为10了。
解析:是虚拟机将常量池内的符号引用替换为直接引用的过程。其中符号引用(Symbolic References)以一组符号来描述所引用的目标,符号可以是任何形式的字面量。比如就一个类引用另一个类,而这层引用关系是需要在class文件来维护的,符号引用就是这个目的。而直接引用是指目标在内存中的位置,直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。换句话说如果有了直接引用,那引用的目标必定已经在内存中存在。而直接引用替换符号引用使得两个类真正建立联系,字段解析,方法解析的道理都是类似的。
初始化:了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码),这个阶段会再次跟进程序初始化变量和其他资源,从另一个交代角度来说,初始化是执行类构造器方法
对于初始化阶段,虚拟机有5种情况必须立即对类进行初始化(加载、验证、准备自然在之前执行):
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5、当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
使用和卸载:不是属于类加载的过程,而使用比如说new一个对象的时候,类的卸载也通常在运行的项目中发生的几率也不大。法区通常也会对方法区进行回收,但是频率相当的低。
二、类加载器的层次结构:
在类加载的机制中,.class文件通过二进制字节流按照虚拟机的所需的格式存储在方法区中class对象的过程是类加载的一个重要过程,而类加载器这个动作的执行者。
分类:都继承自抽象类java.lang.ClassLoader
加载器类型 |
加载内容 | 使用 | ||
启动类加载器 Bootstrap ClassLoader |
启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可 | |||
扩展类加载器(Extension ClassLoader) | 加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载 |
可以直接使用 | ||
应用程序类加载器(Application ClassLoader) | 由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库 | 可以直接使用 | 默认使用 | |
自定义扩展类加载器 | 实现ClassLoader,加载 | 自建 |
图例:
双亲委派模型:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
loadClass代码实现:
protected synchronized Class> loadClass(String name, boolean resolve) throws
ClassNotFoundException
{
//首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
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;
}
双亲委派模型具有的层级关系保证了每个类只被加载一次,避免了重复加载;同时有效避免了系统级代码被恶意篡改,如果开发者自己定义了一个java.lang.Object类,能被编译,但是却不能被加载。
三、自定义类加载器:
在编写一个自定义的类加载过程中,有三个方法是值得注意的:
findClass(String name):根据二进制名称来查找类
loadClass(String name, boolean resolve):ClassLoader的入口点,根据指定的二进制名称来加载类
defineClass(String name, byte[] b, int off, int len),负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。
在自定义类加载器实现过程中,主要重写findClass方法再调用defineClass方法。loadClass是ClassLoader的入口点,重写会破坏双亲委派模型,所以不建议去重写。
代码示例:
package cn.com.userClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
public class UserClassLoader extends ClassLoader{
//读取一个文件路径
private byte[] getBytes(String filename) throws IOException{
File file = new File(filename);
long length = file.length();
byte [] raw = new byte[(int) length];
FileInputStream fin = new FileInputStream(file);
int read = fin.read(raw);
if(read!=length){
throw new IOException("无法读取文件"+"length="+length+" raw="+raw);
}
return raw;
}
//定义编译指定java文件的方法
private boolean compile(String javaFile) throws IOException{
//调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac "+javaFile);
//其他线程都等着这线程完成
try {
p.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取javac线程的退出值
int ret = p.exitValue();
//返回编译是否成功
return ret==0;
}
//重写ClassLoader的findClass方法
protected Class> findClass(String name)throws ClassNotFoundException
{
Class clazz=null;
// 将包路径中的点(.)替换成斜线(/)
String fileStub=name.replace("." , "/");
String javaFilename=fileStub + ".java";
String classFilename=fileStub + ".class";
File javaFile=new File(javaFilename);
File classFile=new File(classFilename);
// 当指定Java源文件存在,且Class文件不存在,或者Java源文件
// 的修改时间比Class文件的修改时间更晚时,重新编译
if(javaFile.exists() && (!classFile.exists()
|| javaFile.lastModified() > classFile.lastModified()))
{
try{
// 如果编译失败,或者该Class文件不存在
if(!compile(javaFilename) || !classFile.exists())
{
throw new ClassNotFoundException( "ClassNotFoundExcetpion:" + javaFilename);
}
} catch (IOException ex){
ex.printStackTrace();
}
}
// 如果Class文件存在,系统负责将该文件转换成Class对象
if (classFile.exists()){
try {
// 将Class文件的二进制数据读入数组
byte[] raw=getBytes(classFilename);
// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz=defineClass(name,raw,0,raw.length);
} catch(IOException ie){
ie.printStackTrace();
}
}
// 如果clazz为null,表明加载失败,则抛出异常
if(clazz==null){
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
public static void main(String[] args) throws Exception{
// 如果运行该程序时没有参数,即没有目标类
if (args.length < 1) {
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java UserClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass=args[0];
// 剩下的参数将作为运行目标类时的参数
// 将这些参数复制到一个新数组中
String[] progArgs=new String[args.length-1];
System.arraycopy(args , 1 , progArgs , 0 , progArgs.length);
UserClassLoader ccl=new UserClassLoader();
// 加载需要运行的类
Class> clazz=ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main=clazz.getMethod("main" , (new String[0]).getClass());
Object argsArray[]={progArgs};
main.invoke(null,argsArray);
}
}
package cn.com.userClassLoader;
public class Test {
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
}
注:在IDEA编写的代表在黑窗口执行前要去掉包路径,不然会找不到要执行类。
自定义类加载器的编写是不是很简单,类加载器的应用越来越广,目前在很多插件化的开发,动态部署、热修复技术都是基于类加载器来展开的,自定义类加载器可以实现的功能可供参考:
1、执行代码前自动验证数字签名。
2、根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译Class文件。
3、根据用户需求来动态地加载类。
4、根据应用需求把其他数据以字节码的形式加载到应用中