Java从诞生时就以平台无关性作为卖点,Java程序步不直接运行在操作系统上的,而是在操作系统上又提供了一层虚拟机。虚拟机为Java程序员提供了一套规范,这套规范与操作系统无关,与操作系统相关的工作就交由Java虚拟机来完成。Sun公司当初在发布Java规范的时候,刻意拆分成《Java语言规范》和《Java虚拟机规范》,以实现让其他语言运行在Java虚拟机上。如今,有一大批运行在Java虚拟机上的语言,如Scala、Jython、Groovy等,Java虚拟机已经开始具有了语言无关性的特点。实现无关性的原因是Java虚拟机在运行程序的时候,只与Class文件打交道。至于这个Class文件的由来,虚拟机并不关心,Class文件可以是javac编译出来的Class文件,或者是groovy编译器编译出来的Class文件,也可以是从网络上获得的。虚拟机在运行程序的时候,首先需要把Class文件加载虚拟机中,这就涉及到虚拟机类加载机制。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制。
知道Java虚拟机的类加载机制对与Java程序员还是很有帮助的,尤其是类加载的准备阶段和初始化阶段,是Java程序员必须了解的,这也是本文的重点。
Java语言里面,类型的加载是在运行期动态进行的,只有在使用到该类的时候才会加载。那么,虚拟机会在哪些条件下去加载一个类呢?
类加载的时机
上图表示了一个类在虚拟机的生命周期,其中加载,验证,准备,解析,和初始化就是类的加载过程。虚拟机并没有规定何时将一个类加载到内存里面,但是规定了何时进行类加载的初始化阶段。当”首次主动使用“一个类时,必须对其进行初始化,虚拟机规范严格规定了有且只有5中情况下属于对类的主动使用:
(1)遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果没有对类进行初始化,则需要先出发其初始化。new最常见的Java代码场景:创建一个类的实例;getstatic:调用类的静态变量;putstatic:设置类的静态变量;invokestatic:调用类的静态方法;
(2)对类反射调用是,如果没有进行过初始化,则需要先触发其初始化;
(3)当初始化一个类时,发现其父类尚未初始化时,先触发父类的初始花;
(4)虚拟机启动时被表明为启动类的类(包含main()方法的类);
(5)使用Java7中的动态语言支持时。
除了以上5种情况外,其他的都属于对类的被动使用,不会触发类的初始化。注意,我们这里所说的是类的初始化,不是对象的初始化,不要跟在堆中创建对象的初始化混淆。
类初始化的特例
关于类初始化的场景上看上去很清晰,其实还是会有些不是那么清晰的情况,有些事由于对主动使用的理解不够深入,有些则属于不能理解的场景,通过举几个例子来说明。
静态常量
import java.util.Random;
class FinalTest1 {
/* 如果x的值是一个常量,也就是说在编译的时候就能确定
* 那么在调用这个静态常量时,不会触发该类的初始化
*/
public static final int x = 6;
static
{
System.out.println("静态常量不会触发类的初始化");
}
}
class FinalTest2 {
/* 如果x的值需要在运行时才能确定,
* 在调用这个静态变量时,就会对这个类进行初始化
*/
public static final int x = new Random().nextInt(100);
static
{
System.out.println("需要在运行时才能确定值,会触发类的初始化");
}
}
public class FinalStatic {
public static void main(String[] args) {
System.out.println(FinalTest1.x);//不会初始化FinalTest1
System.out.println(FinalTest2.x);//会初始化FinalTest2
}
}
被
final修饰的静态常量在编译阶段会存入调用类的常量池中,在使用静态常量时,本质上并没有直接引用定义常量的类,所以不会触发类的初始化;如果需要在运行时才能确定其值,则会触发类的初始化。
子类使用父类的静态变量
class Parent {
public static int a = 3;
static {
System.out.println("Parent static block");
}
}
class Child extends Parent {
static {
System.out.println("Child static block");
}
}
public class ClassExtends {
static {
System.out.println("ClassExtends static block");
}
/*ClassExtends是启动类,虚拟机会首先触发它的初始化
*Child类使用的是父类的静态变量,getinstatic只会触发定义该静态字段的类
*/
public static void main(String[] args) {
System.out.println(Child.a);
}
}
上面一段程序的输出是:ClassExtends static block
Parent static block
3
反射中的.class语法
package com.ssy.classloader;
public class InitClass {
static {
System.out.println("Initclass");
}
public static void main(String[] args) throws ClassNotFoundException {
/*.class语法不会触发类的初始化,至于为什么,似乎不可理解 */
Class<?> clazz = AClass.class;
System.out.println("--------------");
clazz = Class.forName("com.ssy.classloader.AClass");
}
}
class AClass {
static {
System.out.println(".class不会触发类的初始化");
}
}
创建数组package com.understanding.classloader;
public class NotInitClass2 {
public static void main(String[] args) {
OneClass[] array = new OneClass[10];
}
}
class OneClass {
static {
System.out.println("static block");
}
}
运行上面的程序之后会发现,没有输出”static block“语句,说明没有初始化OneClass类。看一下生成的字节码指令:
创建数组的动作由字节码指令newarray触发,它并没有创建10个OneClass对象,而是创建了一个代表数组元素类型为”com.understanding.classloader.OneClass“的一维数组类,也就是”Lcom/understanding/classloader/OneClass“类,这个类由虚拟机生成,继承自Object类。Java数组中的属性和方法(程序员可见的只有length和clone())都封装在这个类中。
注意,在初始化接口时稍有不同:当一个类初始化时,其父类必然已经初始化;但是当初始化一个接口时,并不要求其父接口已经初始化。
上面讲了那么多的初始化,似乎没有提到类加载的加载阶段。因为何时加载一个类,虚拟机规范没有明确说明,由各实现自己确定,但是初始化一个类则是明确了的,在初始化类之前,必然已经加载了该类。
====================================================================================================================================
加载
”加载“是类加载的一个阶段,在加载阶段,虚拟机需要做3件事:
(1)通过类的全额限定名来获取定义此类的二进制字节流;
(2)将字节流所代表的静态存储结构转化为方法去的运行时数据结构;
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
每一个Java类都有一个对应的Class对象,它就像一个“镜子”一样反射出类的所有内容,这也是反射的基础,这个Class对象就是在加载阶段生成的。Java虚拟机没有规定是再Java堆中实例化Class对象,但是对于HotSpot而言,Class对象虽然也是对象,但是它存放在方法区(在Java8中,已经去掉了方法区,所以针对的都是Java7及之前版本,后面提到的方法区,也是如此)。
验证
验证阶段对虚拟机而言非常重要,验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身的安全。前面,我们已经提到了,虚拟机并不区分这个Class文件的来源,通过javac编译的Class文件一般是不会危害虚拟机安全的,但是Class文件的来源是多种多样的,甚至可以用十六制编辑器直接编辑Class文件。虚拟机会进行4个阶段的校验:
(1)文件格式验证:通过这个阶段的验证后,字节流才会进入内存中;
(2)元数据验证;
(3)字节码验证;
(4)符号引用验证。
验证阶段对虚拟机是重要的,但是如果能确保Class文件是安全和正确的,那么就可以关闭虚拟机的大部分校验,以此缩短类加载时间。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值,类变量使用的内存在方法去中分配。这里有两点需要强调:
(1)是类变量(被static修饰的变量),而不是为实例变量,实例变量是在创建类的对象时随对象一起分配在Java堆中;
(2)是设置初始值,而不是程序规定的值。
一个类中定义的类变量:
private static int value = 25;
在准备阶段,value = 0;准备阶段给变量设置的零值(也就是各类型的默认值)。在Java中,类的域(包括实例域和静态域)在使用之前,至少会拥有一个值,其中静态域就是通过类加载的准备阶段设置初始值来完成的;实例域是通过在分配对象时,首先将那块内存清零来完成的。
这里还有一个特殊情况,如果类变量是常量,那么在准备阶段,虚拟机就会把该变量设置为常量值:
private static final int value = 25;
在准备阶段,value的值就已经是25了。
解析
解析阶段是虚拟机将常量池内的符号饮用替换为直接引用的过程。解析阶段的内容还是比较丰富的,具体的可以参考《Java虚拟机规范》第三版。
====================================================================================================================================
初始化
初始化阶段就是为类变量赋予程序显式设定的值,而不再是准备阶段赋予的默认初始值。一般而言,给类变量赋值有两种方式:
//在定义处赋值
static int value = 25;
//或者在静态语句块中
int value;
static {
value = 25;
}
所有的类变量赋值动作和静态语句块中的语句都被Java编译器收集到一起,放到一个特殊的方法中—类构造器<clinit>方法(接口也有这个方法),编译器收集的顺序是由语句在源文件中出现的处顺所决定的。<clinit>方法对程序猿是不可见的,只能被虚拟机调用。换句话说,初始化阶段是执行类构造器<clinit>()方法的过程。初始化一个类包括两个步骤:
(1)如果类存在直接父类,且直接父类还没有初始化,先初始化直接父类;
(2)如果类存在类构造器<clinit>()方法,就执行该方法。
从上面两步来看,我们可以得出<clinit>()方法的特点:
(1)父类总是在子类之前被初始化,这意味着<clinit>()方法不需要显式的调用父类的类构造器,虚拟机会保证子类的<clinit>()方法在执行前,父类的<clinit>()方法已经执行完毕。java.lang.Object是所有类的父类,因此虚拟机中第一个被初始化的<clinit>()方法肯定是Object类的。
(2)如果存在类<clinit>()方法,就执行。也就是说,<clinit>()方法对于类和接口而言并不是必须的的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,编译器就可以不为这个类生成<clinit>()方法。如果一个类定义了类变量,但是没有在定义处或者静态语句块中显式的赋值,编译器也可以不为这个类生成<clinit>()方法。
下面是我在JDK1.7下的测试,第一张图中只定义了静态变量,没有给它显式赋值,javac编译器果然没有给这个类生成<clinit>()方法:
class Clinit {
static int a;
}
使用javap -verbose Clinit.class发现,编译器没有生成<clinit>()方法:
class Clinit {
static int a = 1;
}
显式的给类变量a赋初始值,编译器必须要生成<clinit>()方法:
(3)如果类中仅仅定义了静态常量,形如:
static final int value = 123;
我们知道,常量是在编译阶段存入类的常量池中,value字段也没有被当做类变量,任何使用value字段的类都不会引用定义该常量的类,而是会保存value常量值123的本地拷贝。这也就是我们前面讲的,使用类的静态常量不属于对类的主动使用,不会触发类的初始化。如果仅包含静态常量,那么编译器可以不为该类生成<clinit>(0方法。通过一小段代码来说明上面的内容,Java代码如下:
public class Clinit {
static final int value = 123;
static final double rand = Math.random();
}
通过使用javap -c Clinit.class得到如下内容:
static {};
Code:
0: invokestatic #2 //Method java/lang/Math/random:()D
3: putstatic #3 //Field rand:D
6: return
其中只有对double类型rand的赋值,并没有对int类型的常量value赋值。
(4)关于接口
a.我们在前面说过,初始化一个接口时并不要求必须初始化其父接口。因此,执行接口的<clinit>()方法也不需要先执行父接口的<clinit>()方法。
b.接口中不能使用静态语句块,且接口中的域都自动是public static final的,如果只包含静态常量,则可以不生成<clinit>()方法。如果域的值不是常量,也就是无法在编译期计算,则必须生成<clinit>()方法。
c.接口中的域并不是接口的一部分,它们的值被存储在该接口的静态存储区域。这句话的意思是说,初始化实现该接口的类时,不要求对接口中的域初始化,也就是说不会执行接口的<clinit>()方法。它们在首次被访问时被初始化。
/**
* 如果类中只定义了静态变量,但是没有在定义处或者静态语句块中对其显式赋值,编译器可以不生成<clinit>()方法
* 如果类中值定义了静态常量,编译器可以不生成<clinit>()方法
*/
public class Clinit implements SubInit{
static final int value = 123;
static final double rand = Math.random();
public void f() {
System.out.println("实现的f()方法");
}
public static void main(String[] args) {
/*初始化接口的实现类时不会执行接口的<clinit>()方法*/
System.out.println(rand);
new Clinit().f();
System.out.println("---------------");
/*只有在首次访问域时,才会执行接口的<clinit>()方法*/
Print p = SubInit.sub;
Print p2 = SubInit.sub;
}
}
/**
* 接口中的域都是static final的,而且必须要显式的赋值,不能使用默认值
* 如果接口中的域是静态常量,编译器可以不生成<clinit>()方法
* 接口中不能有static {}
*/
interface SubInit extends BaseInit {
/*sub必须在运行时才能创建,所以该接口要生成<clinit>()方法*/
Print sub = new Print("在SubInit接口中创建的对象");
void f();
}
/*执行接口的<clinit>()方法,不需要求先执行父接口的<clinit>()方法*/
interface BaseInit {
Print base = new Print("在BaseInit接口中创建的对象");
}
/*测试类,只打印一句话*/
class Print {
public Print(String message) {
System.out.println(message);
}
}
程序的输出:
0.3710394328568716
实现的f()方法
---------------
在SubInit接口中创建的对象
通过前两行的输出可以看出,初始化接口的实现类时不会初始化该接口;最后一行输出说明,只有在首次用到时才会对接口进行初始化,也就是对域进行初始化,当然了,也只会初始化一次。输出中并没有来自父接口BaseInit的内容,说明没有执行BaseInit的<clinit>()方法。
(5)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那么就可能造成多个线程阻塞,这种阻塞往往又很隐蔽,造成调试的困难。周志明老师举的一个例子很好,可以拿出来欣赏一下:
public class ClinitDeadLoop {
/**
* 当有多个线程去初始化一个类,只有一个线程会执行类的<clinit>()方法,其他方法需要阻塞等待
*/
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "start");
//对类DeadLoopClass进行初始化,最终会陷入死循环
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread().getName() + "run over");
}
};
Thread thread1 = new Thread(script,"线程1");
Thread thread2 = new Thread(script,"线程2");
thread1.start();
thread2.start();
}
/*执行该类的<clinit>()方法的代价是无穷大*/
static class DeadLoopClass {
static {
//如果不加这个if语句,编译器将提示:Initializer does not complete normally,并拒绝编译
if(true) {
System.out.println(Thread.currentThread().getName() + " 在执行<clinit>()方法时无限循环");
while(true) {
}
}
}
}
}
程序的执行结果:
线程1start
线程2start
线程1 在执行<clinit>()方法时无限循环
我们可以看到,线程1在执行类DeadLoopClass的<clinit>()方法时陷入死循环,线程1永远也没有办法执行完该方法。所有使用DeadLoopClass的线程都必须阻塞等待,在实际应用中必须避免这种情况的发生。
<clinit>()方法是在编译期的语义分析与字节码生成阶段被编译器生成的的。生成<clinit>()方法实际上是一个代码收敛的过程,编译器会把静态语句块(static {})、类变量初始化等操作收敛到<clinit>()方法中,虚拟机会保证父类的<clinit>()方法先执行(对类而言)。
以上的内容着重讲了类加载的初始化阶段,其他阶段太过于抽象和理论化,但是还是必须要理解虚拟机的类加载机制,帮助更好的理解Java中何时执行静态语句块,何时对静态变量赋值等问题。
参考资料:《深入Java虚拟机》第二版,《深入理解Java虚拟机》,《Java虚拟机规范》第三版。转载请注明处处:http://blog.csdn.net/yuhongye111/article/details/30799131
分享到:
相关推荐
什么是虚拟机类加载机制以及加载过程,以及类加载时机
Java虚拟机(JVM)的类加载过程是Java程序运行的基础,它涉及到类加载器、类的生命周期和一系列复杂的步骤。在这个过程中,类加载器主要任务是根据类的全限定名加载二进制字节流并转化为`java.lang.Class`对象。整个...
虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、准备、解析和初始化,终会形成可以被虚拟机使用的Java类型,这是一个虚拟机的类加载机制。Java中的类是动态加载的,只有在运行期间使用到该类的...
类加载的生命周期包括加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中加载、验证、准备、初始化和卸载这五个阶段是确定的,类的加载过程必须按照这种顺序进行。 加载阶段是将.class文件加载到内存中,...
加载过程中,字节码被转换为内存中的数据结构,并在方法区(在JVM规范的早期版本中称为永久代,现代JVM中称为元空间)中存储。同时,会在堆中创建一个`java.lang.Class`对象,作为方法区数据的封装。 2. 连接(Link...
方法区也被称为永久代(PermGen space),主要用来存储已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。值得注意的是,不同虚拟机实现对方法区的具体布局可能有所不同,这些差异取决于...
下面我们将详细探讨WebLogic的类加载过程。 1. **类加载器层次结构** WebLogic的类加载器体系遵循“父类加载器优先”原则,这意味着当一个类加载请求发生时,首先由父类加载器尝试加载,只有在其无法找到相应类时...
#### 三、类加载过程详解 类加载的过程主要包括以下几个步骤: **3.1 加载** 在这个阶段,JVM根据类的全限定名读取对应的二进制数据流,并将其转换为JVM内部表示的`Class`对象。 **3.2 验证** 验证阶段是为了...
JVM类加载过程 JVM(Java Virtual Machine)是Java语言的核心组件之一,它是Java语言的可移植性和跨平台性的基础。JVM主要组成部分包括类加载子系统、执行引擎、本地方法接口和运行时数据区。 类加载子系统是JVM的...
Java 类加载过程是Java程序运行时的关键环节,它涉及到如何将类的字节码加载到JVM中并准备执行。整个过程可以分为五个主要步骤:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和...
本文主要探讨JVM的类加载机制,包括类加载、连接、初始化等关键过程,以及类的主动使用和被动使用的情况。 首先,我们要理解**类加载**的作用。JVM的类加载器(ClassLoader)负责将编译后的`.class`文件加载到内存...
首先,让我们了解类加载的基本过程。当JVM启动时,会触发类加载。这个过程分为三个阶段:加载、链接和初始化。加载阶段,类加载器找到对应的.class文件;链接阶段,字节码被校验并转化为内存中的数据结构;初始化...
【Java虚拟机加载Java类的过程】 Java虚拟机(JVM)加载Java类的过程是一个关键的运行时机制,涉及三个主要步骤:加载、链接和初始化。这个过程确保了类的正确构造和执行。以下是对这些步骤的详细解释: 1. **加载...
在《Java虚拟机类加载机制》一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的后留了一个悬念给各位,这里来揭开这个悬念。建议先看完《Java虚拟机类加载机制》这篇再来看这个,印象会比较深刻...
在这个主题中,我们将详细探讨类加载器的加载过程、双亲委派模型以及自定义类加载器。 一、类加载的时机 类加载通常发生在以下几种情况: 1. 当Java程序首次引用某个类时,JVM会触发该类的加载。 2. 当使用反射API...
类加载过程中可能会遇到一些问题,如类冲突、类循环依赖等。类冲突通常发生在不同加载器加载了相同包名下的类,导致版本不一致。类循环依赖则可能引发无限递归,使得类加载失败。解决这些问题通常需要对类加载器的...
类加载器在类加载过程中扮演重要角色,它负责找到类的字节流并将其转换为运行时可用的形式。系统提供了几个内置类加载器,如Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)和...
Java虚拟机类装载机制是Java运行环境中的核心组成部分,它负责将类的字节码从磁盘、网络等不同来源加载到JVM中,并进行一系列处理以使类能够被正确地使用。类装载机制的目的是为了实现代码的动态加载和运行时的灵活...
加载过程中,JVM会根据类的名字在常量池中查找对应的类定义。 2. **链接(Linking)** 链接阶段将加载的类信息整合到JVM的运行环境中。它包括以下三个子步骤: - **验证(Verification)**:确保字节码的安全性和...