类加载机制

之前我们了解过class文件的存储格式,本文将介绍虚拟机如何加载class文件。

类的生命周期

一般来说,类的生命周期被划分为加载、验证、准备、解析、初始化、使用和卸载几个阶段,其中验证、准备和解析被统称为连接阶段。解析的过程顺序不确定,可能在初始化开始之后才开始,这常常出现在Java的晚绑定(动态绑定)中。

类加载过程

加载

“加载”和“类加载”听起来十分相似,但是加载只是类加载的一个阶段。加载阶段的三个任务:

  • 通过类的全限定名获取类的二进制字节流。
  • 将二进制字节流转化成运行时数据结构。
  • 在内存中生成这个类的java.lang.Class对象,作为方法区数据结构的访问入口。

对于第一个任务,Java虚拟机规范并没有明确要求从字节码文件中获取二进制字节流,因此一般可以从以下途径获取:

  • 从压缩包获取,包括zip、jar、war格式等
  • 从网络中获取,如web applet
  • 运行时计算生成,较多的使用场景是动态代理技术
  • 从数据库中获取
  • 从加密文件中获取

非数组类型的加载阶段是程序员可控制性最强的阶段,程序员可以通过自定义类加载器来控制字节流的获取方式。
数组类型本身并不是通过类加载器进行创建,而是通过JVM在内存中直接动态创建。但数组类的元素类型(去掉所有数组维度的类型)最终还是要靠类加载器加载,数组类的创建过程遵循下列规则:

  • 如果数组的组件类型(去掉一个维度的类型,区别于元素类型)是引用类型,就递归加载这个组件类型,数组会被标识在加载该组件类型的类加载器的类名称空间上。
  • 如果数组的组件类型不是引用类型,JVM会将数组标记为与引导类加载器关联。
  • 数组类的可访问性和组件类型的可访问性一致,如果组件类型不是引用类型,那么它的数组类的可访问性默认是public,所有的类和接口都可以访问。

验证

验证阶段保证class文件的字节流符合Java虚拟机规范,不会损害Java虚拟机的安全。验证阶段主要包括四个验证动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证

文件格式验证是要验证字节流是否符合class文件格式规范,可以验证的地方主要包括:

  • 开头是否是魔数0xCAFEBABE
  • 主次版本号能否被Java虚拟机接受
  • 常量池的常量中是否有不被支持的常量类型
  • class文件本身及各个部分是否有被删除或附加的其他信息
元数据验证

主要包括以下验证点:

  • 这个类是否有父类(除了Object类以外的所有类都有父类)
  • 这个类的父类是否继承了final修饰的类
  • 如果这个类不是抽象类,是否实现了父类或接口要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾
字节码验证

前面的元数据验证主要是用于数据类型校验,字节码校验是进行类的方法体校验,包括以下内容:

  • 任意时刻操作数栈的数据类型和指令代码序列都能配合工作,不会出现类似于“在操作数栈中放了一个int类型的数据,使用时按照long类型来载入本地变量表”的情况。
  • 任何指令都不会跳转到方法体外的字节码指令上。
  • 保证数据类型转换的合法性,子类对象可以赋值给父类数据类型,但反过来则不行,更不能将对象赋值给一个毫不相关的数据类型。
符号引用验证

符号引用验证主要是看该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。主要验证下列内容:

  • 符号引用中能否通过全限定名找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的字段和方法。
  • 符号引用中的类、字段和方法的可访问性是否可以被当前类访问。

准备

这个阶段是为静态变量分配内存并设置初始值的阶段,一般来说是应该在方法区上进行操作的,在JDK7之前,HotSpot使用永久代来实现方法区,但JDK8开始,永久代被移除,静态变量就和class对象一起被存放在了堆空间中。因此,静态变量是在逻辑上的方法区。
准备阶段的初始值并不是代码中赋予的值,而是“赋零值”的过程,一起看一下下面的代码:

public static int value = 1;

尽管代码里面写的是给value赋1,但准备阶段应该是将0赋给value,真正将1赋给value的阶段应该是后面的初始化的阶段。下面列出了各种数据类型的零值。

数据类型零值
int0
long0L
short(short)0
char‘\u0000’
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

然而并不是所有情况都是赋零值的过程,当静态变量被final修饰时,例如下面这种情况:

public static final int value = 1;

这种情况下,value在编译阶段就已经生成了ConstantValue属性,所以在准备阶段就会把1赋给value。

解析

解析阶段就是将常量池中的符号引用转换为直接引用的过程。前面提到了符号引用相关的内容,这里简要说明一下符号引用和直接引用的概念。

  • 符号引用:符号引用是一组用符号来描述所引用的目标,这些符号可以无歧义地定位到目标。在前面讲类文件结构时就提到过符号引用如CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info等。

  • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

初始化是类加载过程的最后一个阶段,前面的准备阶段我们说这是一个给静态变量赋零值的阶段,而到了初始化阶段就是真正给Java代码中的静态变量赋初始值的时候了。在初始化阶段,javac编译器会生成一个<clinit>()方法,用来给静态变量和静态语句块赋值。<clinit>()必须保证在多线程场景下被正确加锁同步,所以多个线程去执行<clinit>()方法时可能会遇到耗时操作导致阻塞。下面的代码进行了这种情况的演示。

public class TestDeadLoopClass {
	static class DeadLoopClass {
		static {
			//这里不加if语句会无法编译
			if (true) {
				System.out.println(Thread.currentThread() + " init DeadLoopClass");
				while (true) {
				}
			}
		}
	}
	
	public static void main(String[] args) {
		Runnable script = new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread() + " start");
				DeadLoopClass dlc = new DeadLoopClass();
				System.out.println(Thread.currentThread() + " run over");
			}
		};
		
		Thread thread1 = new Thread(script);
		Thread thread2 = new Thread(script);
		thread1.start();
		thread2.start();
	}
}

运行结果如下,一个线程卡死了,另一个线程一直阻塞:

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] init DeadLoopClass

接下来看一下初始化的几种情况:

1.遇到new、getstatic、putstatic或invokestatic时触发初始化:

  • 当执行new指令时会初始化一个类,即创建一个类的实例对象。
  • 当执行getstatic指令时会初始化一个类,即访问类的静态变量。
  • 当执行putstatic指令时会初始化一个类,即给静态变量赋值。
  • 当执行invokestatic指令时会初始化一个类,即调用类的静态方法。
    2.使用java.lang.reflect方法对类型进行反射调用的时候,如果类型没有初始化则先进行初始化。
    3.当子类的父类没有初始化,先触发父类的初始化。
    4.虚拟机启动时,需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
    5.当使用JDK7加入的动态语言支持,如果java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,且方法句柄对应的类没有被初始化,则先进行初始化。
    6.当一个接口定义JDK8加入的默认方法(被default修饰的接口方法),当接口的实现类初始化,则接口要在实现类之前初始化。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值