`
喻红叶
  • 浏览: 39164 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

虚拟机类加载过程

 
阅读更多

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虚拟机类加载顺序

    Java虚拟机类加载顺序,Java虚拟机类加载顺序,Java虚拟机类加载顺序

    什么是虚拟机类加载机制以及加载过程,以及类加载时机.xmind

    什么是虚拟机类加载机制以及加载过程,以及类加载时机

    Java虚拟机----类的加载过程.docx

    Java虚拟机----类的加载过程.docx

    Java虚拟机类加载机制浅谈

     虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、准备、解析和初始化,终会形成可以被虚拟机使用的Java类型,这是一个虚拟机的类加载机制。Java中的类是动态加载的,只有在运行期间使用到该类的...

    Java虚拟机类加载机制?案例分析

    在《Java虚拟机类加载机制》一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的后留了一个悬念给各位,这里来揭开这个悬念。建议先看完《Java虚拟机类加载机制》这篇再来看这个,印象会比较深刻...

    虚拟机内存图以及加载类的执行过程

    Java内存图

    第23讲请介绍类加载过程,什么是双亲委派模型1

    这里可进一步细分为三个步骤:极客时间 | Java核心技术36讲验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Ja

    Java虚拟机之类加载机制

    后在运行的时候,虚拟机把描述类的信息从class文件加载到内存,然后再进行校验、解析和初始化等过程,后形成可以被java虚拟机“读懂”的java类型。那么从class——&gt;java虚拟机能“读懂”的java类型是本文要讲解的...

    深入理解java类加载机制

    我们将详细介绍Java虚拟机中类的生命周期并探讨类加载时的各种问题和应急措施。此外,我们还会探讨Java程序的类加载器和双亲委派机制,以及自定义类加载器和类卸载的实现原理和应用方法。 总的来说,本资源将为Java...

    详解JAVA类加载机制(推荐)

    JAVA源码编译由三个过程组成: 1、源码编译机制。 2、类加载机制 ...系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行

    java虚拟机规范高清中文版本(java SE 8版本)

    第1章 :简单地介绍了Java虚拟机的历史并... 第5章:定义了Java虚拟机启动以及类和接口的加载、链接和初始化的过程; 第6章:定义了Java虚拟机指令集; 第7章:提供了一张以操作码值为索引的Java虚拟机操作码助记表。

    java虚拟机知识点整理

    虚拟机类加载机制 编译期编译优化 运行期优化 高效并发-java内存模型与线程 线程安全与锁优化 1 标记-清除算法:首先标记所有需要回收的对象(引用计数或可达性分析算法标记),在标记完成后统一回收所有被标记的对象...

    java虚拟机规范 jdk8.

    第5章定义Java虚拟机启动以及类与接口的加载、链接和初始化过程;第6章阐释并列举Java虚拟机指令集;第7章提供一张以操作码值为索引的Java虚拟机操作码助记符表。  《Java核心技术系列:Java虚拟机规范(Java SE 8...

    深入探讨 Java 类加载器

    类加载器(class ...本文首先详细介绍了 Java 类加载器的基本概念,包括代理模式、加载类的具体过程和线程上下文类加载器等,接着介绍如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。

    深入理解Java类加载.docx

    虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。 在Java语言里面,类型的加载、连接和初始化过程都是在...

    Java虚拟机规范.Java SE 8版

    《Java核心技术系列:Java虚拟机规范(Java SE 8版)...第5章定义Java虚拟机启动以及类与接口的加载、链接和初始化过程;第6章阐释并列举Java虚拟机指令集;第7章提供一张以操作码值为索引的Java虚拟机操作码助记符表。

    深入探讨 Java 类加载器.pdf

    类加载器(class ...本文首先详细介绍了 Java 类加载器的基本概念,包括代理模式、加载类的具体过程和线程上下文类加载器等,接着介绍如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。

    【JVM】类加载器与双亲委派模型

    有关类加载的全过程,可以先参考我的另外一篇文章类的奇幻漂流——类加载机制探秘 类加载器的类型 类加载器有以下种类: 启动类加载器(Bootstrap ClassLoader) 扩展类加载器(Extension ClassLoader) 应用类...

    Java虚拟机

    第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测...

Global site tag (gtag.js) - Google Analytics