java 类的加载,链接,初始化
本篇的话题,讨论Java类的加载、链接和初始化。Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象。一个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则不会被初始化。
创建自己的类加载器
在 Java应用开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名 Java类的隔离等。创建自己的类加载器并不是一件复杂的事情,只需要继承自java.lang.ClassLoader类并覆写对应的方法即可。 java.lang.ClassLoader中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:
- defineClass():这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
- findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
- findClass():这个方法用来根据名称查找并加载Java类。
- loadClass():这个方法用来根据名称加载Java类。
- resolveClass():这个方法用来链接一个Java类。
这里比较 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到过,在Java类的链接过程中,会需要对Java类进行解析,而解析可能会导致当前Java类所引用的其它Java类被加载。在这个时候,JVM就是通过调用当前类的定义类加载器的loadClass()方法来加载其它类的。findClass()方法则是应用创建的类加载器的扩展点。应用自己的类加载器应该覆写findClass()方法来添加自定义的类加载逻辑。 loadClass()方法的默认实现会负责调用findClass()方法。
前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是封装在loadClass()方法中的。如果希望修改此策略,就需要覆写loadClass()方法。
下面的代码给出了自定义的类加载的常见实现模式:
public class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null; //查找或生成Java类的字节代码
return defineClass(name, b, 0, b.length);
}
}
参考资料
- Java语言规范(第三版)- 第十三章:执行
- JVM规范(第二版) - 第五章:加载、链接和初始化
- 深入探讨Java类加载器
java 类的加载,链接,初始化的更多相关文章
- Java类的加载 链接 初始化
原文地址 Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码到能够 ...
- Java 类的加载与初始化
本文结构: 1.先看几道题 2.类的加载于初始化 (1)类的加载 (2)类的初始化 (a)会发生类的初始化的情况 (b)不会发生类的初始化的情况 首先看几道题. 解析可在看完讲解后再看 Demo1 p ...
- java类的加载以及初始化顺序
类的加载和初始化的了解对于我们对编程的理解有很大帮助,最近在看类的记载方面的问题.从网上查阅了若干文章,现总结如下: 我们通过一段代码来了解类加载和初始化的顺序: package com.classl ...
- java类的加载与初始化
https://blog.csdn.net/u013349237/article/details/71076617 1在命令行启动虚拟机jvm进行加载, 2用class.forname()方法进行动态 ...
- Java类的加载及初始化
每个类的编译代码都存在于它自己的独立文件中,该文件在需要使用该程序代码时才会被加载.通常有以下三种加载情况: (1) 访问了子类的静态变量或静态方法:仅对类的静态变量,静态块执行初始化操作,并仅初始化 ...
- Java类的加载、链接和初始化
一.Java的类加载机制回顾与总结: 我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象.这样 ...
- java类从加载、连接到初始化过程
类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JV ...
- JAVA类的加载、连接与初始化
JAVA类的加载.连接与初始化 类的声明周期总共分为5个步骤1.加载2.连接3.初始化4.使用5.卸载 当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载.连接和初始化,而连接这个类 ...
- java 类的加载、连接和初始化
JVM和类 调用Java命令运行Java程序时,该命令将会启动一条Java虚拟机进程,不管该Java程序启动了多少条线程,创建了多少个变量,它们都处于该Java虚拟机进程里,共享该JVM进程的内存区. ...
随机推荐
- MFC/VC++ 响应回车键的实现
在VC++中,要实现对回车键的响应实现,一般通过截获消息来响应,即通过处理BOOL PreTranslateMessage(MSG* pMsg)这个函数来处理 实现如下: BOOL PreTransl ...
- Java基础知识强化之集合框架笔记49:键盘录入5个学生信息(姓名,语文成绩,数学成绩,英语成绩)按照总分从高到低输出到控制台
1. 键盘录入5个学生信息(姓名,语文成绩,数学成绩,英语成绩)按照总分从高到低输出到控制台: 分析: A: 定义学生类 B: 创建一个TreeSet集合 C: 总分从高到底如何实现 ...
- Tomcat 的 SSL 配置
本教程使用 JDK 6 和 Tomcat 7,其他版本类似. 基本步骤: 使用 java 创建一个 keystore 文件 配置 Tomcat 以使用该 keystore 文件 测试 配置应用以便使用 ...
- 程序员带你十天快速入门Python,玩转电脑软件开发(三)
声明:本次教程主要适用于已经习得一门编程语言的程序员.想要学习第二门语言.有梦想,立志做全栈攻城狮的你 . 如果是小白,也可以学习本教程.不过可能有些困难.如有问题在文章下方进行讨论.或者添加QQ群5 ...
- css制作导航栏的上下三角
1)先完成一个导航条 <style type="text/css"> .nav-ul{ list-style: none; } .nav-ul li{ width: 1 ...
- SVN服务器的搭建 分类: 网络 2014-11-27 01:18 204人阅读 评论(4) 收藏
一.首先来下载和安装SVN服务器 现在Subversion已经迁移到apache网站上了,下载地址: http://subversion.apache.org/packages.html 这是二进制文 ...
- [网络] C# NetHelper网络通信编程类教程与源码下载
点击下载 NetHelper.zip 主要功能如下所示 检查设置的IP地址是否正确,返回正确的IP地址 检查设置的端口号是否正确,返回正确的端口号 将字符串形式的IP地址转换成IPAddress对象 ...
- 面向报文(UDP)和面向字节流(TCP)的区别
转载:http://blog.csdn.net/wanghaobo920/article/details/11877589 面向报文(UDP)和面向字节流(TCP)的区别 面向报文的传输方式是应用层交 ...
- Lucene的Query类介绍
把Lucene的查询当成sql的查询,也许会笼统的明白些query的真相了. 查询分为大致两类,1:精准查询.2,模糊查询. 创建测试数据. private Directory directory; ...
- Sdut 2151 Phone Numbers (山东省ACM第一届省赛题 A)
题目描述 We know thatif a phone number A is another phone number B's prefix, B is not able to becalled. ...