一、Java类加载机制
在讲类加载机制之前先放上一张JVM物理结构图,有助于理解类加载机制:
![](https://i-blog.csdnimg.cn/blog_migrate/6957f3ec1bc3d0dd35bf27b083192c9a.png)
1、类加载机制概念
当程序主动使用某个类时,如果该类还没被加载到内存中,则JVM会通过加载、连接、初始化 3 个步骤来对类进行初始化。如果没有意外,JVM将会连续完成这 3 个步骤,所以有时也把这 3 个步骤统称为类加载或类初始化。
2、类加载过程
工作机制:类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。
在Java中,类装载器把一个类装入JVM中,要经过以下步骤:
(1) 类的加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象,即当程序中使用任何类时,JVM都会 为之创建一个描述类结构的java.lang.Class对象。通过该java.lang.Class对象,可以获知类结构信息,如 构造器、属性和方法。
(2) 类的连接:把类的二进制数据合并到JRE中。类的连接又可以分为如下三个阶段:
a.校验:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
b.准备:给类的静态变量分配存储空间,并设置默认值;
c.解析:将类的二进制数据中的符号引用替换成直接引用;
(3) 类的初始化:对类的静态变量,静态代码块执行初始化操作。
3、类初始化时机
当Java程序首次通过下面6种方式来使用某个类或接口是,系统就会初始化该类或接口
(1)创建类的实例:使用new操作符来创建实例;通过反射来创建实例;通过反序列化的方式来创建实例;
(2)调用某个类的类方法(静态方法);
(3)访问某个类或接口的类变量,或为该类变量赋值;
注意:对于一个final型的类变量,如果该变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。 Java编译 器会在编译时直接把这个类变量出现的地方替换成它的值,那么程序在其他地方使用该类变量时,实际上并没有使用该类 变量。所以,即使程序使用该静态类变量,也不会导致该类的初始化。
class MyTest{
static {
System.out.println("静态初始化块...");
}
//final修饰的类变量compileConstant的值在编译时确定下来
static final String compileConstant = "编译时可以确定值";
//final修饰的类变量compileConstant在编译时不能确定其值
//static final String compileConstant= System.currentTimeMillis() + "";
}
public class CompileConstantTest {
public static void main(String[] args) {
//final修饰的类变量compileConstant的值在编译时确定下来
//因此compileConstant会被当成“宏变量”处理
//程序中使用compileConstant的地方都会在编译时将其替换成它的值,即下面一行代码不会导致MyTest类初始化
System.out.println(MyTest.compileConstant);
}
}
(4)使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
//如果Person类还没有初始化,则会初始化Person类,并返回Person类对应的java.lang.Class对象
Class.forName("Person");//forName()方法的参数为完整的类名
当使用ClassLoader类的loadClass()方法来加载某个类是,该方法只是加载该类,并不会执行该类的初始化
ClassLoader cl = ClassLoader.getSystemClassLoader();
cl.loadClass("Tester");//只是加载Tester类,并不会执行Tester类的初始化
(5)初始化某个类的子类:当初始化某个类的子类时,该子类的所有父类都会被初始化;
(6)直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。
4、类加载器
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存上,并为之生成对应的java.lang.Class对象。
4.1.类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个.class文件,并且被同一个类加载器加载,这两个类才相等。
例如,如果在pg包中国有一个名为Person的类,被类加载器ClassLoader的实例k1负责加载,则该Person类对应的Class对象在JVM中表示为(Person 、pg 、k1)。这意味着两个类加载器加载的同名类:(Person、pg、k1)和(Person、pg、k2)是不同的、他们所加载的类也是完全不同、互不兼容的。
当JVM启动时,会形成由 3 个类加载器组成的初始类加载器层次结构
(1)根加载器(Bootstrap ClassLoader)
Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath选线或使用-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类。
Bootstrap ClassLoader不是java.lang.ClassLoader的子类,它由JVM自身(非Java语言)实现。下面程序可以获取Bootstrap ClassLoader所加载的核心类库:
import java.net.URL;
public class BootstrapTest {
public static void main(String[] args) {
//获取根加载器所加载的全部URL数组
URL[] urls1 = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//遍历、输出根类加载器加载的全部URL
for (URL url : urls1) {
System.out.println(url.toExternalForm());
}
}
}
运行上面程序,在我的机器上会看到如下结果:
从上面结果我们可以明白我们的程序为什么可以使用String、System这些核心类库--因为这些核心类库都在jar包C:/Program%20Files/Android/Android%20Studio/jre/jre/lib/rt.jar中。
(2)扩展类加载器(Extension ClassLoader)
Extension ClassLoader负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类。通过这种方式,就可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放进JAVA_HOME/jre/lib/ext路径即可。
(3)系统类加载器 / 应用程序类加载器(System ClassLoader / Application ClassLoader)
System ClassLoader负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。
我们的应用程序都是由这 3 类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。如果没有特别指定,则自定义的类加载器都以ClassLoader作为父加载器。
4.2.类加载机制
类加载器之间的层次结构如下图所示:
类加载机制要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
JVM的类加载机制主要有如下三种:
(1)全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
(2)父类委托:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种机制来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该机制的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
(3)缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象是,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class之后,必须重新启动JVM,程序所做的修改才会生效的原因。
在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:
protected synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先检查该name指定的class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
} else {
// parent为null,则调用BootstrapClassLoader进行加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果仍然无法加载成功,则调用自身的findClass进行加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。
4.3自定义类加载器
JVM中除了跟加载器之外的所有类加载器都CLassLoader子类的实例,我们可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
ClassLoader类有如下两个关键方法:
1)loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
2)findClass(Stirng name):根据指定名称来查找类。
通过重写以上两个方法可以是先自定义的ClassLoader。通常推荐重现findClass()方法,而不是重写loadClass()方法。loadClass()方法的执行步骤如下:
a.用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回;
b.在父类加载器上调用loadClass方法,如果父类加载器为null,则使用根类加载器来加载;
c.调用findClass(String) 方法查找类。
从上面loadeClass()方法的执行步骤可以看出,重写findClass(String)可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
ClassLoader里还有一个核心方法:
Class defineClass(String name , byte[] b , int off , int len):该方法负责将指定类的字节码文件(即Class文件,如果Hello.class)读入字节数组byte[] b 内, 并把它转换为Class对象。defineClass()管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并检验有效性等。该方法有final修饰,不能重写。
ClassLoader里还包含如下一些普通方法:
findSystemClass(String name) :从本地文件系统转入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换类;
static getSystemClassLoader:静态方法, 返回系统类加载器;
getParent():获取该类加载器的父类加载器;
resolve(Class<?> c):链接指定类。类加载器可以使用此方法来链接类c;
findLoadClass(String name):如果此JVM已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。
下面程序实现了自定义ClassLoader。该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行java之前先编译该程序的目标,这样即可通过该ClassLoad直接运行Java源文件。
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
public class CompileClassLoader extends ClassLoader {
//读取一个文件的内容
private byte[] getBytes(String filename) throws IOException{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int)len];
try(FileInputStream fin = new FileInputStream(file)){
//一次读取Class文件的全部二进制数据
int r = fin.read(raw);
if(r != len){
throw new IOException("无法读取全部文件:" + r + " !" + len);
}
return raw;
}
}
//定义编译指定Java文件的方法
private boolean compile(String javaFile) throws IOException{
System.out.println("CompileClassLoader:正在编译 " + javaFile + "...");
//调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try{
//其他线程都在等待这个线程完成
p.waitFor();
}catch (InterruptedException ie){
System.out.println(ie);
}
//获取javac 线程的退出值
int ret = p.exitValue();
//返回编译是否成功
return ret == 0 ;
}
//重写ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassCastException{
Class clazz = null;
//将包路径中的点(.)替换成斜线(/)
String fileStub = name.replace("." , "/");
String javaFilename = fileStub + ".java";
String classFliename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFliename);
//当指定Java源文件存在,且Class文件不存在
//或者Java源文件的修改时间比Class文件的修改时间更晚时,重新编译
if(javaFile.exists() && (!classFile.exists()) || javaFile.lastModified() > classFile.lastModified()){
try {
//如果编译失败,或者该Class文件不存在
if(!compile(javaFilename) || !classFile.exists()){
throw new ClassCastException("ClassNotFoundException:" + javaFilename);
}
}catch (IOException ex){
ex.printStackTrace();
}
}
//如果Class文件存在,系统负责将该文件转换成Class对象
if(classFile.exists()){
try {
//将Class文件的二进制数据读入数组
byte[] raw = getBytes(classFliename);
//调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = defineClass(name , raw , 0 , raw.length);
}catch (IOException ie){
ie.printStackTrace();
}
}
//如果clazz为null,表明加载失败,则抛出异常
if(clazz == null){
throw new ClassCastException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception{
//如果运行该程序时没有参数,即没有目标类
if(args.length < 1){
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
//第一个参数是需要运行的类
String proClass = args[0];
//剩下的参数将作为运行目标类的参数
//将这些参数复制到一个新数组中
String[] progArgs = new String[args.length - 1];
System.arraycopy(args , 1 , progArgs , 0 ,progArgs.length);
CompileClassLoader cc1 = new CompileClassLoader();
//加载需要运行的类
Class<?> clazz = cc1.loadClass(proClass);
//获取需要运行的类的主方法
Method main = clazz.getMethod("main" , (new String[0]).getClass());
Object argsArray[] = {progArgs};
main.invoke(null , argsArray);
}
}
接下来随意提供一个简单的朱磊,该主类无序编译就可以使用上面的CompileClassLoader来运行它
public class Hello {
public static void main(String[] args) {
for (String arg : args) {
System.out.println("运行Hello的参数:" + arg );
}
}
}
在cmd中进入源文件所在的目录后运行java CompileClassLoader.java Hello FengJunjie,运行结果如下:
本例仅仅提供了在运行之前先编译Java源文件的功能。实际上,使用自定义的类加载器,可以实现以下常见功能:
1)执行代码前自动验证数字签名;
2)根据用户提高的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件;
3)根据用户需求来动态地加载类;
4)根据应用需求把其他数据以字节码的形式加载到应用中。