java类的加载、链接和初始化(摘自infoq的java深度历险)

   一个Java类从字节代码到能够在JVM中被使用,需要经过 加载、链接和初始化
三个步骤。这三个步骤中,对开发人员直接可见的 是Java类的加载,通过使用Java类
加载 器(class loader)可以在运行时刻动态的加载一个Java类;而链接和初始化则
是在使用Java类之前会发生的动作。
本文会详细介绍Java类的加载、链接和 初始化的过程。
Java 类的加载
   Java类的加载是由类加载器来完成的。一般来说,类加载器分成两类: 启动类加载
器(bootstrap)用户自定义的类加载器(user-defined)。两者的区别在于
启动类加载器是由JVM的原生代码实现的,而用户自定义的类加载器都继承自Java中
的java.lang.ClassLoader类。在用户自定义类加载器的部分,一般JVM都会提供一些
基本实现。应用程序的开发人员也可以根据需要编写自己的类加载器。JVM中最常
使用的是系统类加载器(system),它用来启动Java应用程序的加载。通过
java.lang.ClassLoader的getSystemClassLoader()方法可以获取到该类加载器对象。
类加载器需要完成的最终功能是定义一个Java类,即把Java字节代码转换成JVM中的
java.lang.Class类的对象。但是类加载的过程并不 是这么简单。Java类加载器有两
个比较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都
有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代
的方式组织在一起,形成树状层次结构。代理模式则指的是一个类加载器既可以自
己完成Java类的定义工作,也可 以代理给其它的类加载器来完成。由于代理模式的
存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是
一个。前者称为初始类加载器, 而后者称为定义类加载器。两者的关联在于:一个
Java类的定义类加载器是该类所导入的其它Java类的初始类加载器。比如类A通过
import导入了类 B,那么由类A的定义类加载器负责启动类B的加载过程。一般的类
加载器在尝试自己去加载某个Java类之前,会首先代理给其父类加载器。当父类加
载器找不到的时候, 才会尝试自己加载。这个逻辑是封装在java.lang.ClassLoader
类的loadClass()方法中的。一般来说,父类优先的策略就足够好了。
      在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候
再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是Servlet规范推
荐的做法。比如,Apache Tomcat为每个Web应用都提供一个独立的类加载器,使用
的就是自己优先加载的策略。IBM WebSphere Application Server则允许Web应用选
择类加载器使用的策略。
类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,
判断两个类是否相同,不仅是根据该类的二进制名称,还需要根据两个类的定义类
加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java
字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试
图在两个类的对象之间进行赋值操作,会抛出java.lang.ClassCastException。这
个特性为同样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求
同一名称的Java类的不同版本在JVM中可以同时存在。通过类加载器就可以满足这种
需求。这种技术在OSGi中得到了广泛的应用。
Java 类的链接
Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在链
接之前,这个类必须被成功加载。类的链接包括验证、准备和解析等几个步骤。验
证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误
的话,会抛出java.lang.VerifyError错误。准备过程则是创建Java类中的静态域,
并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对
其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回
值的Java类等。解析的过程就是确保这些被引用的类能被正确的找到。解析的过程
可能会导致其它的 Java类被加载。
不同的JVM 实现可能选择不同的解析策略。一种做法是在链接的时候,就递归的把
所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引用真正需
要的时候才进行解析。也就是说如果一个Java 类只是被引用了,但是并没有被真正
用到,那么这个类有可能就不会被解析。考虑下面的代码:
public class LinkTest 
{
	public static void main(String[] args) 
	{
		ToBeLinked toBeLinked = null;
		System.out.println("Test link.");
	}
}


类 LinkTest 引用了类ToBeLinked,但是并没有真正使用它,只是声明了一个变量,
并没有创建该类的实例或是访问其中的静态域。在 Oracle 的JDK 6 中,如果把编译
好的ToBeLinked 的Java 字节代码删除之后,再运行LinkTest,程序不会抛出错误。
这是因为ToBeLinked 类没有被真正用到,而Oracle 的JDK 6 所采用的链接策略使得
ToBeLinked 类不会被加载,因此也不会发现ToBeLinked 的Java 字节代码实际上是不
存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相
同的方法运行,就会抛出异常了。因为这个时候ToBeLinked 这个类被真正使用到了,
会需要加载这个类。
Java 类的初始化
当一个Java 类第一次被真正使用到的时候,JVM 会进行该类的初始化操作。 初始化
过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它
的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。
在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静
态域。考虑下面的代码:
public class StaticTest 
{
	public static int X = 10;
	
	public static void main(String[] args) 
	{
		System.out.println(Y); //输出60
	}
	static
	{
		X = 30;
	}
	
	public static int Y = X * 2;
}

在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到
下依次执行。因此变量X 的值首先初始化成10,后来又被赋值成30;而变量Y 的
值则被初始化成60。
Java 类和接口的初始化只有在特定的时机才会发生,这些时机包括:
创建一个 Java 类的实例。
MyClass obj = new MyClass()
调用一个 Java 类中的静态方法。
MyClass.sayHello()
给 Java 类或接口中声明的静态域赋值。如
MyClass.value = 10
访问 Java 类或接口中声明的静态域,并且该域不是常值变量。
int value = MyClass.value
在顶层 Java 类中执行assert 语句。
通过Java 反射API 也可能造成类和接口的初始化。需要注意的是, 当访问一个Java
类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑
下面的代码:
class B 
{
	static int value = 100;
	static 
	{
		System.out.println("Class B is initialized."); //输出
	}
}
class A extends B 
{
	static 
	{
		System.out.println("Class A is initialized."); //不会输出
	}
}
public class InitTest 
{
	public static void main(String[] args) 
	{
		System.out.println(A.value); //输出100
	}
}


在上述代码中,类InitTest 通过A.value 引用了类B 中声明的静态域value。由于value
是在类B 中声明的,只有类B 会被初始化,而类A 则不会被初始化。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值