类的加载篇
1、Loading 加载
就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型: 类模板对象。
loading 过程
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
- 创建
java.lang.Class
类的实例,作为方法区这个类的各种数据的访问入口
什么是类模板对象?
类模板对象,就是 Java 类在 JVM 内存中的一个快照, JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样 JVM 在运行期间便能通过类模板而获取到 Java 类中的任何信息;反射的机制也是基于这一基础才能实现。
二进制流的获取方式
- 虚拟机通过文件系统读入一个 class 后缀的文件
- 读入 jar、zip等归档数据包,提取类文件
- 事先存放在数据库中的类的二进制数据
- 在运行时生成一段 Class 的二进制信息等
Class 实例的位置在哪?
说明:Class 类的构造方法是私有的,只有 JVM 能够创建
数组类的加载有什么不同?
数组类本身不是由类加载器加载的,类加载器负责加载数组中存放元素的类型:
1、如果数组的元素类型是引用类型,那么就递归加载和创建数组的元素类型
2、JVM 使用指定的元素类型和数组维度来创建新的数组类
3、如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定;否则数组类的可访问性就是缺省定义为 public
2、Linking 链接
链接过程
- 验证(
verifaication
):验证是否符合 JAVA 语法规范 - 准备(
preparation
): 为类的静态变量分配内存,并将其赋默认值 - 解析(
resolve
): 将类、接口、字段和方法的符号引用转换为直接引用
3、Initialization 初始化
初始化阶段,简言之,为类的静态变量赋上正确的初始值 (显示初始化)。
到了初始化阶段,才开始真正执行类中定义的 Java 程序代码,初始化阶段的重要工作是执行类的初始化方法: <clinit>()
方法
<clinit>()
方法只能由 JAVA 编译器生成,并由 JVM 调用;只有在给类中的static
变量显示赋值或在静态代码块中赋值了,才会生成此方法<init>()
方法一定会出现在 Class 的 method 表中,因为每个 Class 都必定有构造器方法
public class InitializationTest {
//非静态字段不会生成 <clinit>
public int num = 1;
//静态字段没有显示赋值也不会生成
public static int num1;
//静态常量也不会生成
public static final int num2 = 1;
//但这种情况会生成 <clinit> 方法,因为调用了方法需要 <clinit> 支撑
public static final int num3 = Integer.valueOf(100);
}
子类加载前先加载父类吗?
是的,在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 <clinit>
总是在子类 <clinit>
前被调用,也就是说父类的 static 块优先级高于子类
<clinit>
的使用会导致死锁吗?
有可能导致死锁,因为 <clinit>
方法是带锁线程安全的,如果两个类互相引用,就会导致死锁
哪些情况会触发类的加载
Java 程序对类的使用分为 主动使用 和 被动使用
主动使用
Class 只有在必须要首次使用(主动使用)的时候才会被装载,JVM 不会无条件地装载 Class 类型。
1、当创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反序列化
2、当调用类的静态方法时,即当使用了字节码 invokestatic
指令
3、当使用类、接口的静态字段时(final 修饰符特殊考虑),比如,使用 getstatic
或 putstatic
指令
4、当使用 java.lang.reflect
包中的方法反射类的方法时,比如 Class.forName("com.tt.Test")
;
5、当初始化子类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
6、如果一个接口定义了 default
方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前初始化
7、当虚拟机启动时,用户指定的主类 (main() 方法的那个类),虚拟机会先初始化这个主类
被动使用
被动使用不会引起类的初始化,也就是说,并不是在代码中出现的类就一定会被加载或者初始化,如果不符合主动使用条件,类就不会初始化。
1、当访问一个静态字段时,比如通过子类去访问父类的静态字段,但不会触发子类的初始化,只会触发父类的初始化
public class Parent{
static int num = 10;
static {
System.out.println("Parent init");
}
}
public class Child extends Parent {
static {
System.out.println("Child init");
}
}
public static void main(String[] args) {
//通过子类去访问父类静态字段时,不会触发子类的初始化
System.out.println(Child.num); // Parent init
}
2、通过数组定义类引用,不会触发此类的初始化
//定义一个 Parent 的数组,此时并不会触发 Parent 的初始化
Parent[] parents = new Parent[10];
3、引用常量不会触发此类或接口的初始化,因为常量在链接阶段就被显示赋值了
4、调用 ClassLoader
的 loadClass()
方法不会触发初始化
public class T {
//静态 没有 final 修饰,在 <clinit> 执行
public static int k = 0;
public static T t1 = new T("t1");
public static T t2 = new T("t2");
public static int i = print("i");
public static int n = 99;
static {
print("静态块");
}
//显示赋值、非静态代码块、构造方法 由 <init> 方法来执行
//=============================
public int j = print("j");
{
print("构造块");
}
public T(String str) {
System.out.println((++k) + ":" + str + "i=" + i + " n=" + n);
++n;
++i;
}
//===============================
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
}
}
//执行 main()方法,控制台结果
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
类的 Using (使用)
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化三个阶段。一旦一个类型成功经历这三个阶段后,那么这个类型就进入使用阶段,此时这个类型已经可以正常使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(静态字段、静态方法),或者使用 new 关键字为其创建对象实例
类的 Unloading 卸载
类、类的加载器、类的实例之间的关系
一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass() 方法,这个方法返回代表对象所属类的 Class 对象的引用。
此外,所有 JAVA 类都有一个静态属性 class,它引用代表这个类的 Class 对象。
何种情况类会被卸载
在类的加载器的内部实现中,用一个 Java 集合来存放所加载类的引用,因此要卸载一个类,就得卸载相对应的类的加载器,又得卸载这个类的加载器加载的其他类,因此只有自定义的类的加载器才有可能被卸载
方法区的垃圾回收
方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
不再使用的类型的判定条件很苛刻:
- 该类的所有实例被回收
- 加载该类的类加载器被回收
- 该类对应的 Java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
类的加载器
何为类的唯一性?
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确认其在 JVM 中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
类加载器的基本特征
- 双亲委派模型。但不是所有类加载器都遵守这个模型,有时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如 JDK 内部的 ServiceProvider/ServiceLoader 机制,用户可以在标准 API 框架上提供自己的实现。
- 可见性。子类加载器可以访问父加载器加载的类型,但反过来不允许。
- 单一性。父加载器加载过的类型不会在子加载器中重复加载;但同一类型可以被邻居加载器加载多次,因为互相并不可见。
类加载器的分类
类加载器总体分为两大类:引导类加载器和自定义类加载器
引导类加载器
- 引导类加载器 (
Bootstrap ClassLoader
)使用 C/C++ 语言实现,嵌套在 JVM 内部 - 它用来加载 Java 的核心库 (
JAVA_HOME/jre/lib/rt.jar
或sun.boot.class.path
路径下的内容)。用于提供 JVM 自身需要的类 - 并不继承
java.lang.ClassLoader
,没有父加载器 - 处于安全考虑, Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类
- 加载扩展类加载器和应用类加载器,并指定为它们的父类加载器
使用 -XX:+TraceClassLoading
参数可以得到
自定义类加载器
引导类加载器以外的的所有类加载器都归为自定义类加载器,继承于 java.lang.ClassLoader
。
扩展类加载器
- Java 语言编写,由
sun.misc.Launcher$ExtClassLoader
实现 - 父类加载器为启动类加载器
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从 JDK 的安装目录的jre/lib/ext
子目录下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载
系统类加载器
- 父类加载器为扩展类加载器
- 负责加载环境变量 classpath 或者系统属性
java.class.path
指定路径下的类库 - 应用程序中的类加载器默认是系统类加载器
- 它是用户自定义类加载器的默认父加载器
- 通过
ClassLoader.getSystemClassLoader()
可获取
用户自定义类加载器
- 在 java 日常应用当中,类的加载几乎由上述 3 种类加载器互相配合完成,在必要时,我们可以自定义类加载器
- 通过类加载器可以实现非常绝妙的插件机制
- 自定义类加载器可以实现应用隔离
classLoader 源码分析
双亲委派机制
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 {
//直到最上层,尝试去用 bootstrap 类加载器加载
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
方法,否则双亲委派机制就失效了,推荐去重写 findClass
方法,需要调用 defineClass
方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
当我们重写 loadClass
方法试图去覆盖源码的时候,还有一个机制可以防止源码被篡改
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
自定义类加载器实现
public class UserDefineClassLoader extends ClassLoader{
private String rootPath;
public UserDefineClassLoader(String rootPath) {
this.rootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//转换以文件路径表示的文件
String filePath = classToFilePath(name);
//获取指定路径的 class 文件对应的二进制流数据
byte[] data = getBytesFromPath(filePath);
//自定义 classLoader 调用 defineClass 方法
return defineClass(name, data, 0, data.length);
}
private String classToFilePath(String name) {
return rootPath + "\\" + name.replace(".","\\") + ".class";
}
//为了代码精简不作异常处理
private byte[] getBytesFromPath(String filePath) {
FileInputStream fileInputStream = new FileInputStream(filePath);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fileInputStream.read(buffer)) != -1){
byteArrayOutputStream.write(buffer,0,len);
}
return byteArrayOutputStream.toByteArray();
}
public static void main(String[] args) throws Exception{
UserDefineClassLoader classLoader = new UserDefineClassLoader("E:\\venti\\venti-java\\src");
Class<?> aClass = classLoader.findClass("com.ttdxg.jvm.T");
System.out.println(aClass);
}
}
双亲委派机制
优势:
- 避免类的重复加载,确保一个类的全局唯一性,当父加载器加载了时,就没必要子加载器再加载一次
- 保护程序安全,防止核心 API 被篡改
弊端:
- 顶层的类加载器无法访问底层的类加载器加载的类,因此应用类访问系统类没问题,但系统类访问应用类就有问题
jdbc 破坏双亲委派机制
tomcat 破坏双亲委派机制
- 一个 web 容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 (WebApp 类加载器)
- 部署在同一个 Web 容器中相同的类库相同的版本可以共享 (Shared 类加载器)
- Web 容器也有自己的依赖的类库,不能与应用程序的类库混淆。(Catalina 类加载器)
- Web 容器要支持在不重启的情况下对 JSP 的修改 (JSP 类加载器加载)
tomcat 使用默认的类加载机行不行?
不行,默认的类加载机制无法加载相同类库的不同版本。
tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?
可以使用线程上下文类加载器,让父类加载器请求子类加载器去完成类加载
tomcat 应用需要使用到某个类时的加载顺序
1 使用 bootstrap 引导类加载器加载
2 使用 system 系统类加载器加载
3 使用应用类加载器在 WEB-INF/classes 中加载
4 使用应用类加载器在 WEB-INF/lib 中加载
5 使用 common 类加载器在 CATALINA_HOME/lib 中加载
jdk9 中类加载结构的新变化
1、jdk9 时基于模块化进行构建,此时的 Java 类库天然满足可扩展需求,原来的扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器 PlatformClassLoader
。
2、启动类加载器、平台类加载器、应用程序类加载器全部继承于 jdk.internal.loader.BuiltinClassLoader