Java虚拟机类加载过程

文章摘自:深入理解Java虚拟机 第二版 周志明著 

本文主要讲解一个Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。

  • 加载

首先,加载是类加载Class Loading过程的一个阶段

然后,在加载阶段,虚拟机需要完成以下3件事:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

可以看出虚拟机 规范 的这3点要求并不算具体,如第一条并没有指明二进制字节流要从一个Class文件中获取,这样的好处可想而知,可以让充满创造力的开发人员在这个规范上玩各种各种的花样,许多Java技术都建立在这个基础上,如:从ZIP包获取,最终成为日后JAR、WAR格式的基础;运行时计算生成,这种使用最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator。generateProxyClass来为特定接口生成形式为‘$Proxy’代理类的二进制字节流;由其他文件生成,典型是JSP,即由JSP文件生成对应的Class类;由数据库中读取等等……

加载阶段既可以使用系统提供的引导类加载器完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadCalss()方法)

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。

加载阶段与连接阶段部分内容交叉进行,即加载阶段未完成,连接阶段可能已经开始。

  • 验证

验证是连接阶段第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。因为Class文件可以使用任何途径产生,所以安全性需要保证,验证则是虚拟机对自身保护的一项重要工作。

对于虚拟机的类加载机制来说,验证阶段是一个非常重要但是不一定必要的阶段,如果所运行的代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none参数关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中n进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。而这里所说的初始值通常是数据类型的零值。  假设一个类变量public static int value = 123;那么变量value在准备阶段过后初始值为0,不是123。而把value赋值为123是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段执行。

基本数据类型的零值:

boolean        false

char              '/uoooo'(null)

byte              (byte)0

short             (short)0

int                  0

long               0L

float               0.0f

double           0.0d

reference       null

上述提到通常为零值,其实还有一些特殊情况:如果类字段的字段属性表存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue所指定的值,  假设一个类变量public static final int value = 123;那么准备阶段虚拟机会根据ConstantValue的设置将vlaue赋值为123.

  • 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化

类初始化阶段真正开始执行类中定义的Java程序代码。

准备阶段变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序指定的主观计划去初始化类变量和其他资源,或者可以这样说:初始化阶段是执行类构造器<client>()方法的过程。

<client>()方法 与类的构造方法(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此 在虚拟机中第一个被执行了<clinit>方法的类肯定是java.lang.Object

由于父类的<client>()方法先执行,也就意味着父类中定义的静态语句块要有优先于子类的变量赋值操作。

package com.gary.test.clinit;

public class Test {
	static class Parent {
		public static int A = 1;
		static {
			A = 2;
		}
	}
	
	static class Sub extends Parent {
		public static int B = A;
	}

	public static void main(String[] args) {
		System.out.println(Sub.B);
	}

}
2

<client>()方法对于类和接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<client>()方法。

接口中不能使用静态语句块,但是仍然有变量初始化的赋值操作,因此接口与类一样都会生成<client>()方法。但接口与类不同的是,执行接口的<client>()方法不需要先执行父接口的<client>()方法。只有当父接口中定义的变量使用时,父接口才回初始化。另外,接口的实现类在初始化时也一样不会执行接口的<client>()方法。

虚拟机会保证一个类的<client>()方法在多线程环境中被正确的枷锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程会去执行这个类的<client>()方法;其他线程都需要阻塞等待,直到活动线程执行<client>()方法完毕。

package com.gary.test.clinit;

public class Test2 {
	static class DeepLoopClass {
		static {
			if (true) {
				System.out.println(Thread.currentThread() + "init DeepLoopClass");
				while (true) {
				}
			}
		}
	}

	public static void main(String[] args) {
		Runnable runnable = new Runnable() {
			public void run() {
				System.out.println(Thread.currentThread() + "start");
				DeepLoopClass dlc = new DeepLoopClass();
				System.out.println(Thread.currentThread() + "run over");
			}
		};
		Thread thread1 = new Thread(runnable);
		Thread thread2 = new Thread(runnable);
		thread1.start();
		thread2.start();
	}

}

可以看出另外一条线程一直在阻塞。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值