JVM 学习笔记——类加载

一个类什么时候被加载

在 JVM 规范中,对于一个类什么时候被加载没有强制约束,不同的虚拟机可以自由实现,HotSpot 中的类是按需加载的

可通过如下 JVM 参数监控类的加载证明

-XX:+TraceClassLoading

当通过 new 关键字创建对象,访问类的静态成员、通过反射访问类时,类会被加载 


类加载的过程

类加载分为 7 个阶段,分别为

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

一般也将验证、准备和解析三个阶段统称为链接(Linking)

加载

通过二进制字节流将 class 文件读入到 JVM 内存中,解析二进制数据中的类信息,转化为方法区的运行时数据结构,并在堆中创建对应的 java.lang.Class 对象

Class 类的构造方法是私有的,只有 JVM 可以创建 Class 对象

验证

检验被加载的类是否有正确的内部结构,确保 class 文件的字节流中包含信息符合当前 JVM 要求,不会危害 JVM 自身安全,其主要包括文件格式验证,元数据验证,字节码验证,符号引用验证

准备

为静态变量分配内存,并初始化默认值

解析

将接口,字段和方法的符号引用转换为直接引用

初始化

在准备阶段,静态变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员指定的值去初始化静态变量

初始化阶段还会执行静态代码块(普通代码块在创建对象时执行)

使用

卸载

类卸载的条件如下

  • 该类的所有实例都被 GC
  • 加载该类的 ClassLoader 已经被 GC
  • 该类的 java.lang.Class 对象没有在任何地方被引用

由 JVM 自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载

JVM 自带的类加载器包括根类加载器扩展类加载器系统类加载器

JVM 本身会始终引用这些类加载器,而这些类加载器又会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可达的

只有由用户自定义的类加载器加载的类可以被卸载


类的初始化顺序

类加载时

public class FatherClass {
	public static String fatherStaticField = "父类静态变量";

	public String fatherField = "父类成员变量";

	static {
		System.out.println(fatherStaticField);
		System.out.println("父类静态代码块");
	}

	{
		System.out.println("父类普通代码块");
	}

	public FatherClass() {
		System.out.println("父类构造方法");
	}
}
public class ChildClass extends FatherClass{
	public static String childStaticField = "子类静态变量";

	public String childField = "子类成员变量";

	static {
		System.out.println(childStaticField);
		System.out.println("子类静态代码块");
	}

	{
		System.out.println("子类普通代码块");
	}

	public ChildClass() {
		System.out.println("子类构造方法");
	}

	public static void main(String[] args) {

	}
}

执行 main 方法后,开始加载子类,输出如下:

可以看出,静态初始化时的顺序为

  • 父类静态变量
  • 父类静态代码块
  • 子类静态变量
  • 子类静态代码块

创建对象时

public class FatherClass {
	public static String fatherStaticField = "父类静态变量";

	public String fatherField = "父类成员变量";

	static {
		System.out.println(fatherStaticField);
		System.out.println("父类静态代码块");
	}

	{
		System.out.println(fatherField);
		System.out.println("父类普通代码块");
	}

	public FatherClass() {
		System.out.println("父类构造方法");
	}
}
public class ChildClass extends FatherClass{
	public static String childStaticField = "子类静态变量";

	public String childField = "子类成员变量";

	static {
		System.out.println(childStaticField);
		System.out.println("子类静态代码块");
	}

	{
		System.out.println(childField);
		System.out.println("子类普通代码块");
	}

	public ChildClass() {
		System.out.println("子类构造方法");
	}

	public static void main(String[] args) {
		new ChildClass();
	}
}

执行 main 方法后,创建子类对象,输出如下:

可以看出,静态变量和静态代码块优先,父类优先,总体顺序如下

  • 父类静态变量
  • 父类静态代码块
  • 子类静态变量
  • 子类静态代码块
  • 父类成员变量
  • 父类普通代码块
  • 父类构造方法
  • 子类成员变量
  • 子类普通代码块
  • 子类构造方法

类加载器(ClassLoader)

什么是类加载器

在类加载阶段,通过一个类的全限定类名,获取描述该类的二进制字节流的代码称为类加载器

类加载器可以自定义实现

在 JVM 中,一个类用全限定类名和它的类加载器作为唯一标识

JVM 预定义加载器

JVM 类加载器关系图如下,这里的关系并非继承

继承关系如下,自定义类加载器继承 ClassLoader 类

启动类加载器(Bootstrap ClassLoader)

启动类加载器(Bootstrap ClassLoader)也称根类加载器,是用本地代码(C++)实现的类加载器,负责将 JAVA_HOME/lib/rt.jar、resource.jar、charset.jar 中的所有 class 或 -Xbootclasspath 参数指定的 jar 包等虚拟机识别的类库加载到内存中,由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用

证明如下

System.out.println(BufferedReader.class.getClassLoader());

输出为 null

扩展类加载器(Extension ClassLoader)

扩展类加载器(Extension ClassLoader)由 Java 语言实现,负责加载 JAVA_HOME\lib\ext 目录下,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库

应用程序类加载器(Application ClassLoader)

应用程序类加载器(Application ClassLoader)也由 Java 语言实现,因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,故一般也称为系统类加载器,它负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过类加载器,一般情况下这个就是程序默认的类加载器

双亲委派机制

工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

例如 java.lang.String 类,存放于 rt.jar 中,Application ClassLoader 收到该类的加载请求后,将请求委托给 Extension ClassLoader,而 Extension ClassLoader 收到请求后继续委托给 Bootstrap ClassLoader,Bootstrap ClassLoader 发现该类存在于自己的加载范围中,故进行加载,若加载的类不在自己的加载范围中,则子类加载器收到反馈,尝试加载该类

好处

  • 避免重复加载:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次,保证类的唯一性
  • 安全性:Java 核心类库不会被随意篡改,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class

代码示例

如下,自定义编写一个 Double 类

package java.lang;

public class Double {
	public static void main(String[] args) {
		System.out.println("asd");
	}
}

运行

运行后,Application ClassLoader 通过双亲委派机制,将请求一路传到 Bootstrap ClassLoader 中,Bootstrap ClassLoader 发现已经加载过该类,故在该类中找 main 方法,显然找不到,报错

就算将 Double 改为 MyDouble ,也会报错

因为 JVM 不允许包名与 Java 核心类库包名相同

可以通过自定义类加载器,继承 ClassLoader ,重写 loadClass() 方法打破双亲委派机制

自定义类加载器

自定义类加载器需要继承 ClassLoader

如果不想打破双亲委派机制,只需重写 findClass() 方法

如果想打破双亲委派机制,需重写 loadClass() 方法

loadClass() 方法中,先执行双亲委派的判断,再执行 findClass() 方法

defineClass() 方法将字节码转化为 java.lang.Class 对象


Class.forName() 与 ClassLoader 的区别

public class Test {
	public static void main(String[] args) throws ClassNotFoundException {
		Class.forName("ChildClass");
	}
}

对于 Class.forName() 方法加载类,会对类进行初始化

public class Test {
	public static void main(String[] args) throws ClassNotFoundException {
		Class classTest = ClassLoader.getSystemClassLoader().loadClass("ChildClass");
		//Class classTest = Test.class.getClassLoader().loadClass("ChildClass");
		System.out.println(ClassLoader.getSystemClassLoader() == Test.class.getClassLoader());	//输出true
	}
}

对于 ClassLoader 类加载器加载类,不会对类进行初始化,无输出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值