Java类加载机制
类加载过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载
类加载过程的第一步,主要完成下面3件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
验证
- 文件格式验证:主要验证Class文件是否规范等。
- 元数据验证:对字节码描述的信息语义分析等。
- 字节码验证:确保语义是ok的。
- 符号引用验证:确保解析动作能执行。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
- 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了
public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111
,那么准备阶段 value 的值就被复制为 111。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()
方法的过程。
对于 <clinit> ()
方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>()
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化:
- 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 使用
java.lang.reflect
包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。
类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载
%JAVA_HOME%/lib
目录下的jar包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载目录
%JRE_HOME%/lib/ext
目录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
在了解了类加载的过程和类加载器后,我们不得不了解的还有类加载的方式:双亲委派机制
双亲委派机制
双亲委派机制的流程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的优点:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
自定义类加载器
自定义步骤
1、编写一个类,这个类继承ClassLoader抽象类
2、覆写他的findClass()
方法
3、在findClass()
方法中调用defineClass()
。
ps:这里的defineClass()
就是吧字节码转化为Class
自定义ClassLoader
首先我们先编写一个测试用的类文件,
package ReWriteClassLoaderTest.tt;
public class Apple {
public String name;
}
然后把这个类编译成class文件放在很远很远的地方,我这里放在了f盘的根目录下
然后我们构建自定义的ClassLoader
package ReWriteClassLoaderTest.tt;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
/**
* <h3>firstIdeaProject</h3>
* <p></p>
*
* @author : Nicer_feng
* @date : 2020-09-10 14:57
**/
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("F:/Apple.class");
//这里放哪了 就写哪里的路径
try {
FileInputStream fileInputStream = new FileInputStream(file);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//每次读取的大小是4kb
byte[] b = new byte[4 * 1024];
int n = 0;
while ((n = fileInputStream.read(b)) != -1) {
outputStream.write(b, 0, n);
}
//将Apple.class类读取到byte数组里
byte[] classByteArray = outputStream.toByteArray();
//调用defineClass 将byte 加载成class
Class<?> aClass = this.defineClass(name, classByteArray, 0, classByteArray.length);
return aClass;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
这里我们自定义的ClassLoader(),让它继承ClassLoader,然后我们重写了findClass()方法,让重写后的逻辑就是自己的指定的目录,也就是f盘下找到Apple.class文件 然后读取到一个byte数组中,然后调用defineClass() 加载这个类。
最后来一个简单的测试类Demo
public class Demo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> aClass = Class.forName("ReWriteClassLoaderTest.tt.Apple",
true, myClassLoader);
Object o = aClass.newInstance();
System.out.println(o);
System.out.println(o.getClass().getClassLoader());
}
}
可以看到这里的Class.forName方法里有三个参数,后面两个意思是true代表类重新加载,myClassLoader就是我们自定义的类加载器
运行后显示
这里并没有达到我们想要的结果,还是走了AppClassLoader,是因为我们的编译器过于智能,直接把我的java文件编译,编译完了还放在了classpath下, 我们要去target(或者out)目录下找到对应的文件并删除,再次运行,如下
如果要有更深刻的理解可以去
https://blog.csdn.net/briblue/article/details/54973413
问题
1、讲讲双亲委派机制,为什么要用它?
双亲委派模型过程:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
双亲委派模型的优点:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
2、为什么要自定义类加载器?
简单的原因就是因为系统提供的类加载器无法满足实际某些场景的应用,
类似javaWeb服务器,可能在一个服务器上部署了多个网站,每个网站会用到一些相同的类库,如果只利用系统的类加载器则只能存在一种类库,对于不同版本的类库则不能同时存在;jsp热替换的支持, JSP最终要编译成.class文件才能由虚拟机执行,但JSP运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待,这些系统提供的加载器都无法完全满足,需要自己定义加载器。
还有网上几种常见的答案:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
3、普通Java类的类加载过程和Tomcat的类加载过程是否一样?区别在哪?
见
https://blog.csdn.net/dreamcatcher1314/article/details/78271251
4、什么是类加载器
类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
类加载器是Java语言的一项创新,最开始是为了满足Java Applet的需求而设计的。类加载器目前在层次划分、程序热部署和代码加密等领域经常使用。
5、类加载器分为哪几类?
JVM为我们默认提供了系统类加载器(JDK1.8),包括:
Bootstrap ClassLoader(系统类加载器)
Extension ClassLoader(扩展类加载器)
Application ClassLoader(应用程序类加载器)
Customer ClassLoader(自定义加载器)