Java 虚拟机为 Java 程序提供运行时环境,其中一项重要的任务就是管理类和对象的生命周期。类的生命周期从类被加载、连接和初始化开始,到类被卸载结束。当类处于生命周期中时,它的二进制数据位于方法区内,在堆区内还会有一个相应的描述这个类的 Class 对象。
10.1 Java 虚拟机及程序的生命周期
当通过 java 命令运行一个 Java 程序时,就启动了一个 Java 虚拟机进程。Java 虚拟机进程从启动到终止的过程,称为 Java 虚拟机的生命周期。 在以下情况,Java 虚拟机将结束生命周期:
- 程序正常执行结束。
- 程序在执行中因为出现异常或错误而异常终止。
- 执行了
System.exit()
方法。 - 由于操作系统出现错误而导致 Java 虚拟机进程终止。
当 Java 虚拟机处于生命周期中时,它的总任务就是运行 Java 程序, Java 程序从开始运行到终止的过程称为程序的生命周期,它和 Java 虚拟机的生命周期是一致的。
类的加载、连接和初始化
当 Java 程序需要使用某个类时,Java 虚拟机会确保这个类已经被加载、连接和初始化。其中连接过程又包括验证 、 准备和解析这 3 个子步骤,这些步骤必须严格地按以下顺序执行:
- 加载:查找并加载类的二进制数据。
- 连接:包括验证、准备和解析类的二进制数据。
- 验证:确保被加载类的正确性。
- 准备:为类的静态变量分配内存,并将其初始化为默认值。
- 解析 : 把类中的符号引用转换为直接引用。
- 初始化:给类的静态变量赋予正确的初始值。
________连接________
加载 -> |验证 -> 准备 -> 解析| -> 初始化
类的加载
类的加载是指把类的.class
文件中的二进制数据读入内存,把它存放在运行时数据区的 方法区 内,然后在 堆区 创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构,并且向 Java 程序提供了访问类在方法区内的数据结构的接口。
调用Class对象的方法 ________堆区__________ _______方法区______
Java 程序 ----------------------> |描述Worker类的Class对象| ---->| Worker类的数据结构 |
类的加载是由类加载器完成的。类加载器可分为两种。Java 虚拟机自带的加载器:包括启动类加载器、扩展类加载器和系统类加载器。用户自定义的类加载器:是 java.lang.ClassLoader 类的子类的实例,用户可以通过它来定制类的加载方式。
类加载器并不需要等到某个类被“首次主动使用”时再加载它, Java 虚拟机规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载过程中遇到class 文件缺失或者存在错误,类加载器必须等到程序首次主动使用该类时才报告错误(抛出一个 LinkageError
实例)。如果这个类一直没有被程序主动使用,那么类加载器将不会报告错误。
类的验证
当类被加载后,就进入连接阶段。连接就是把已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。连接的第一步是类的验证,保证被加载的类有正确的内部结构, 并且与其他类协调一致。如果 Java 虚拟机检查到错误,就会抛出相应的 Error
对象。
类的验证主要包括以下内容 :
- 类文件的结构检查:确保类文件遵从 Java 类文件的固定格式。
- 语义检查 :确保类本身符合 Java 语言的语法规定,比如验证 final 类型的类没有子类,以及 final 类型的方法没有被覆盖。
- 字节码验证:确保字节码流可以被 Java 虚拟机安全地执行。字节码流代表 Java 方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
- 二进制兼容的验证:确保相互引用的类之间协调一致。例如,在 A 类的 methodA() 方法中会调用 B 类的 methodB() 方法。 Java 虚拟机在验证 A 类时,会检查在方法区内是否存在 B 类的 methodB() 方法,假如不存在(当 A 类和 B 类的版本不兼容时,就会出现这种问题),就会抛出
NoSuchMethodError
错误。
类的准备
在准备阶段, Java 虚拟机为类的静态变量分配内存,并设置默认的初始值。
public class Sample {
private static int a = 1;
public static long b;
static {
b = 2;
}
...
}
//在准备阶段,将为 int 类型的静态变量 a 分配 4 个字节的内存空间,并且赋予默认值 0,
//为 long 类型的静态变量 b 分配 8 个字节的内存空间,并且赋予默认值 0
类的解析
在解析阶段,Java 虚拟机会把类的二进制数据中的符号引用替换为直接引用。
//Worker 类的 gotoWork() 方法会引用 Bike 类的 ride() 方法
public void gotoWork() {
bike.ride(); //这段代码在 Worker类的二进制数据中表示为符号引用
}
在 Worker 类的 二进制数据中,包含了一个对 Bike 类的 ride() 方法的符号引用,它由 ride() 方法的全名和相关描述符组成。在解析阶段, Java 虚拟机会把这个符号引用替换为一个指针,该指针指向 Bike 类的 ride()方法在方法区内的内存位置,这个指针就是直接引用。
类的初始化
在初始化阶段, Java 虚拟机执行类的初始化语句,为类的静态变量赋予初始值。静态变量的初始化有两种途径:1. 在静态变匿的声明处进行初始化。2. 在静态代码块中进行初始化。静态变量的声明语句,以及静态代码块都被看作类的初始化语句, Java 虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。
Java 虚拟机初始化一个类包含以下步骤:
- 假如这个类还没有被加载和连接,那就先进行加载和连接。
- 如果类存在直接的父类,并且这个父类还没有被初始化,那么就先初始化直接的父类。
- 如果类中存在初始化语句,那么就依次执行这些初始化语句。
当初始化一个类的直接父类时,也需要重复以上步骤,这确保当程序主动使用一个类时,这个类及所有父类(包括直接父类和间接父类)都已经被初始化, 程序中第一个被初始化的类是 Object
类。
package init;
class Base {
static int a = 1;
static { System.out.println("init base");}
}
class Sub extends Base {
static int b = 1;
static { System.out.println("init sub");}
}
public class InitTester {
static {System.out.println("init teseter");}
public void main(String[] args) {
System.out.println(Sub.b); //指定这行代码,先依次吃初始化Base和Sub
}
}
//Java 虚拟机首先初始化启动类 InitTester, 然后执行它的main()方法。
//打印结果:init tester, init base, init sub, 1
// 修改上述 main()
public void main(String[] args) {
Base base; //不会初始化 Base 类
base = new Base(); //初始化 Base 类
System.out.println(Sub.b); //初始化 Sub 类
}
当程序构造 Base 实例时,才会初始化 Base 类。
类的初始化时机
Java 虚拟机只有在程序首次 主动使用 一个类或接口时才会初始化它。只有 6 种活动被看作是程序对类或接口的主动使用:
- 创建类的实例。创建类的实例的途径包括:用 new 语句创建实例,或者通过反射、克隆及反序列化手段来创建实例。
- 调用类的静态方法。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用 JavaAPI 中某些反射方法。
- 初始化一个类的子类。
- Java 虚拟机启动时被标明为启动类的类。
除了上述 6 种情形,其他使用 Java 类的方式都被看作是 被动使用,都不会导致类的初始化。
- 对于
final
类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看作编译时常量。Java 程序中对类的编译时常量的使用,被看作是对类的被动使用,不会导致类的初始化。
当 Java 编译器生成 Sample 类的 class 文件时,它不会在 main() 方法的字节码流中保存一个表示class Tester{ public static final int a = 2 * 3; //变量 a 是编译时常量 public static final int b = (int)(Math.random() * 5); //变量 b 不是编译时常量 static {System.out.println("init tester");} } public class Sample { public static void main(String[] args) { System.out.println(Tester.a); } } //程序打印结果:6
Tester.a
的符号引用,而是直接在字节码流中嵌入常量值 6。因此当程序访问 Tester.a 时,客观上无须初始化 Tester 类 。 - 对于
final
类型的静态变量,如果在编译时不能计算出变量的取值,那么程序对类的这种变量的使用,被看作是对类的主动使用,会导致类的初始化。 - 只有当程序访问的静态变量或静态方法的确在当前类或接口中定义时,才可以看作是对类或接口的主动使用。
package initbase; class Base{ static int a= 1; static{ System.outprintln("init Base"); } static void method() { System.outprintln("metbod of Base"); } } class Sub extends Base{ static { System.outprintln("initSub"); } } public class Sample{ public static void main(String[] args) { System.out.println(Sub.a); //仅仅初始化 Base 类 Sub.method(); } } //静态变量a和静态方法method在Base父类中定义,因此Java虚拟机仅初始化Base而未初始化Sub //打印结果:init Base, 1, method of Base
- 调用
ClassLoader
类 的loadClass()
方法加载一个类 ,并不是对类的主动使用,不会导致类的初始化。class ClassA { static {System.out.println("init ClassA");} } public ClassB { public static void main(String[] args) { ClassLoader loader = ClassLoader.getSystemClassLoader(); //获得系统类加载器 Class objClass = loader.loadClass("ClassA"); //加载ClassA System.out.println("FLG"); objClass = Class.forName("ClassA"); //初始化ClassA } } //尽管系统类加载器加载了 ClassA, 但是 ClassA 没有被初始化。 //当程序调用 Class 类的静态方法 forName() 方法显式地初始化 ClassA,这是对 ClassA 的主动使用 //将导致 ClassA 被初始化,它的静态代码块被执行。 //打印结果: FLG, init ClassA
10.3 类加载器
类加载器用来把类加载到 Java 虚拟机中。从 JDKl.2 版本开始,类的加载过程采用父亲委托机制。在此委托机制中,除了 Java 虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当 Java 程序请求加载器 loader1 加载 Sample 类时,loader1 首先委托自己的父加载器去加载 Sample 类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器 loader1 本身加载 Sample 类。
Java 虚拟机自带以下几种加载器:
- 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如
java.lang.*
等。根类加载器从系统属性sun.boot.class.path
所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并没有继承Java.lang.ClassLoader
类。 - 扩展 (Extension) 类加载器:它的父加载器为根类加载器。它从
java.ext.dirs
系统属性所指定的目录中加载类库,或者从 JDK 的安装目录的jre\lib\ext
子目录(扩展目录)下加载类库, 如果把用户创建的 JAR 文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯 Java 类,是java.lang.ClassLoader
类的子类。 - 系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从
classpath
环境变量或者系统属性java.class.path
所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯 Java 类,是java.lang.ClassLoader
类的子类。
除了以上虚拟机自带的加载器,用户还可以定制自己的类加载器。Java 提供了抽象类 java.lang.ClassLoader
,所有用户自定义的类加载器应该继承 ClassLoader 类。
package loadtester;
public class Sample {
public static void main(String[] args) {
Class c;
ClassLoader cl, cl1;
cl = ClassLoader.getSystemClassLoader(); //获得系统加载器
System.out.println(cl); //打印系统加载器
while (cl != null) { //打印父加载器
cl1 = cl;
cl = cl.getParent();
System.out.println(cl1 + " 's parent is " + cl);
}
try {
c = Class.forName("java.lang.Object"); //获得代表Object类的Class实例
cl = c.getClassLoader(); //获得加载Object类的加载器
System.out.println("java.lang.Object's loader is " + cl);
c = Class.forName("loadtester.Sample"); //获得代表Sample类的Class实例
cl = c.getClassLoader(); //获得加载Sample类的加载器
System.out.println("Sample's loader is " + cl);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
以上程序打印结果
sun.misc.Launcher$AppClassloader@1d6096
sun.misc.Launcher$AppClassloader@1d6096's parent is sun.misc.Launcher$ExtClassLoader@3b6ab6
sun.misc.Launcher$ExtClassLoader@3b6ab6's parent is null
javalang.Object's loader is null
Sample's loader is sun.misc.Launcher$AppClassLoader@1d6096
第一行表明了系统类加载器 sun.misc.Launcher$AppClassLoader 类的实例。
第二行表明系统类加载器的父加载器为扩展类加载器,即 sun.misc.Launcher$ExtClassLoader 类的实例。
第三行表明扩展类加载器的父加载器为根类加载器。不过, Java 虚拟机并不会向 Java 程序提供根类加载器的引用,
而是用 “null" 来表示根类加载器,这样做是为了保护 Java 虚拟机的安全,
防止黑客利用根类加载器来加载非法的类,从而破坏 Java 虚拟机的核心代码 。
第四行表明,java.lang.Object 是由根类加载器加载的。
第五行表明,用户类 Sample 是由系统类加载器加载的。
*/
在父亲委托(Parent Delegation)机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器。
loader2 -> loader1 -> 系统加载器 -> 扩展加载器 -> 根加载器
假设 Java 程序要求 loader2 加载 Sample 类,代码如下:
Class sampleClass = loader2.loadClass("Sample");
loader2 首先从自己的命名空间中查找 Sample 类是否已经被加载,如果已经加载,就直接返回代表 Sample 类的 Class 对象的引用。如果 Sample 类还没有被加载,loader2 首先请求loader1代为加载, loader1再请求系统类加载器代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将 Sample 类所对应的 Class 对象的引用返回给loader1,loader1再将引用返回给loader2, 从而成功地将 Sample 类加载进虚拟机。若系统类加载器不能加载 Sample 类,则loader1尝试加载Sample类,若loader1不能成功加载,则loader2尝试加载。若所有的父加载器及loader2本身都不能加载,则抛出 ClassNotFoundException
异常。若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用类加载器(包括定义类加载器)被称为初始类加载器。
父亲委托机制的优点是能提供软件系统的安全性 。 因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如,java.lang.Object
类总是由根类加载器加载的,其他任何用户自定义的类加载器都不可能加载包含有恶意代码的 Java.lang.Object 类。
- 命名空间
每个类加载器有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。 - 运行时包
由同一类加载器加载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。
创建用户自定义的类加载器
要创建用户自定义的类加载器,只需拓展 java.lang.ClassLoader
类,然后覆盖 findClass(String name)
方法,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
//给类加载器指定一个名字,本例中为了便于区分不同的加载器对象
private String name;
private String path = "d:\\";
private final String fileType = ".class";
public MyClassLoader(String name) {
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
public String toString() { return name; }
public void setPath(String path) {this.path = path;}
public String getPath() {return path;}
protect Class findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
//把类的二进制数据读入内存
private byte[] loadClassData(String name) throw ClassNotFoundException {
FileInputStream fis = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
//把name字符串中的 . 替换为 \ 从而把类中的包名转换为路径名
name = name.repalceAll("\\.", "\\\\");
fis = new FileInputStream(new File(path + name + fileType));
baos = new ByteArrayOutputStream();
int ch = 0;
while ((ch = fis.read()) != -1) {
baos.write(ch);
}
data = bas.toByteArray();
fis.close();
baos.close();
} catch (IOException e) {
throw new ClassNotFoundException("Class is not found" + name, e); //异常转译
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("D:\\myapp\\serverlib\\");
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("D:\\myapp\\clientlib\\");
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("D:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
}
public static void test(ClassLoader loader) throws Exception {
Class objClass = loader.loadClass("Sample"); //加载Sample类
Object obj = objClass.newInstance(); //创建Sample类的实例
}
}
在 MyClassLoader
类的 main()方法中,loader1 加载器由 MyClassLoader 类的默认构造方法创建,它的父加载器为系统类加载器 。 在创建 loader2 加载器时,在构造方法中显式地指定父加载器为 loader1。在创建 loader3 加载器时,在构造方法中显式地指定父加载器为null, 即根类加载器。loader1、loader2 和 loader3 加载器分别从 D:\myapp\serverlib
D:\myapp\clientlib
和 D:\myapp\otherlib
目录下加载类。
根类加载器 <--- loader3 ---> D:\myapp\otherlib
|
扩展类加载器
|
系统类加载 ---> 环境变量 classpath
|
loader1 ---> D:\myapp\serverlib
|
loader2 ---> D:\myapp\clientlib
MyClassLoader 类的 test()
方法用来测试类加载器的用法,它调用 ClassLoader 类的 loadClass()
方法加载 Sample 类。
public class Sample {
public int vl = 1;
public Sample(){
System.out.println("Sample is loaded by " + this.getClass().getClassLoader());
new Dog(); //主动使用 Dog 类
}
}
public class Dog {
public Dog(){
System.out.println("Dog is loaded by " + this.getClass().getClassLoader());
}
}
|-D:\myapp
|-syslib
|-serverlib
|-clientlib
|-otherlib
把 MyClassLoader 类的 class 文件复制到 D:\myapp\syslib 目录下,以它为 classpath,使得 MyClassLoader 类由系统类加载器加载。
接下来通过改变 Sample 类和 Dog 类的存放路径,或者修改源程序,来演示类加载器的种种特性。
把 Sample.class 和 Dog.class 同时复制到 D:\myapp\serverlib 和 D:\myapp\otherlib目录下。 运行 MyClassLoader 类,打印结果为:
Sample is loaded by loaderl
Dog is loaded by loader1
Sample is loaded by loader3
Dog is loaded by loader3
当执行 loader2.loadClass("Sample")
时,先由它上层的所有父加载器尝试加载 Sample 类。 loader1 从 D:\myapp\serverlib 目录下成功地加载了 Sample 类,因此 loader1 是 Sample 类的定义类加载器, loader1 和 loader2 是 Sample 类的初始类加载器。
当执行 loader3.loadClass("Sample")
时,先由它上层的所有父加载器尝试加载 Sample 类。loader3 的父加载器为根类加载器,它无法加载 Sample 类,接着 loader3 从 D:\myapp\otherlib 目录下成功地加载了Sample 类,因此 loader3 是 Sample 类的定义类加载器及初始类加载器 。
在 loader1 和 loader3 各自的命名空间中 ,都存在 Sample 类和 Dog 类。也就是说,在Java虚拟机的方法区内,有两个 Sample 类和两个 Dog 类的二进制数据结构,分别来自 D:\myapp\serverlib
和 D:\myapp\otherlib
。
10.4 类的卸载
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期。Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java 虚拟机本身会始终引用这些自带的类加载器,而这些类加载器则会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的。由用户自定义的类加载器所加载的类是可以被卸载的。