前言
大家好,在该专栏的上一篇文章中我们介绍了一下关于 Java 中类的相关知识点。那么这篇文章我们来看一下一个 Java 类是怎么被虚拟机加载并使用的,本文内容参考了《深入理解Java机》一书。
试想一下,如果没有 Eclipse,IDEA 等 Java 编程工具,我们在编写好一个 Java 类源文件(.java)后如何将其编译成一个 .class 文件呢?没错,通过 javac
命令,实际上也就是 javac
程序,它一般在你 Java 安装目录的 bin
子目录下:
在这里我们不仅看到了 javac
命令,还看到了我们非常熟悉的 java
,javadoc
,javah
, javap
等程序。其中 javadoc
是为类生成 html
格式的文档的程序,javah
是为某个存在 native
方法的类生成 jni
头文件的程序,javap
是用来生成某个类的字节码的程序。好了,让我们回到最开始接触 Java 的时候, 来手动编译并运行一个类吧,我们先新建一个类文件,暂且叫 Main.java
:
public class Main {
public static void main(String[] args) {
System.out.println("This is a simple class!");
}
}
我们来通过命令行进行操作(确保你已经将 java 安装目录下的 bin 子目录成功的添加到环境变量中,即成功配置好 Java 环境):
好了,我们已经成功得到了编译出来的类文件了。当我们调用 java Main
命令时,会执行这个类中的 main(Stirng[] args)
方法,在这个过程中首先会创建一个虚拟机进程,然后虚拟机会寻找并加载 Main
类,在加载完成后执行其 main
方法。那么接下来我们来看看虚拟机是如何寻找并加载类的。
类加载过程
要加载一个类,先得找到这个类,因此在上面的命令行中我们先进入了 Main.class
文件所在的目录,然后调用 java Main
命令,这样虚拟机就会在当前目录下寻找名为 Main
的 class 文件。那么如果当前命令调用时所在目录不在 Main.class
文件所在目录该怎么办呢?我们需要使用 -classpath
参数来指定要加载的类所在的目录:
这里我在 D盘根目录下调用 java
命令,通过 -classpath
命令指定 Main
所在的目录,虚拟机成功加载该类并运行,所以在上面调用 java Main
命令等同于 java -classpath "C:\Users\MAIBENBEN\Desktop\blog\Java常用技术\Java 类机制(2)----类加载过程" Main
命令。那么如果我有多个类文件目录(当然本例中没有)怎么办呢?我们只需要在 -classpath
参数值中以分号分隔多个路径即可:-classpath "C:\Users\MAIBENBEN\Desktop\blog\Java常用技术;C:\Users\MAIBENBEN\Desktop\blog\Java常用技术\Java 类机制(2)----类加载过程"
。好了,现在虚拟机已经可以找到我们自定义的类了,可以进行加载这个类的动作了。一般来说,虚拟机要加载一个类,需要经过以下步骤:加载
->验证
->准备
->解析
->初始化
。其中,验证
、解析
和 准备
这三个过程也被称为 链接
。当这些动作完成并且中途没有出错后,类就已经被成功加载到虚拟机中了(生成一个 Class
类型的对象,储存在方法区 / 堆中,一般是方法区)。之后这个类就可以被使用了。
加载
步骤主要是将要进行加载的类的 .class
文件的二进制流加载进来,但是如何得到类的二进制数据,虚拟机并未进行明确规定,可以是通过命令编译出来的类文件,也可以是通过网络传输的数据,甚至可以是人为编辑的二进制文件(如果你对 .class
文件格式足够熟悉的话)。即加载哪个类数据和如何加载类数据是开发者可自定义的。之后会对加载进来的类数据进行格式解析,如果解析成功(二进制数据符合规定的类数据格式),则会在内存中生对应类的一个 Class
对象,否则抛出 ClassFormatError
异常。
验证
步骤用来判断在上一步加载的类是否符合当前虚拟机要求,并且不会危害当前虚拟机安全。从语言特性上来说,Java 类在编译时就会判断语法的正确性,那为什么虚拟机还要花大功夫来进行验证呢?正如 加载
部分所述,我们得到的类数据的方法有很多种,可以通过编译后的 .class
文件,也可以通过网络等方式。也就是说类数据的来源是未知的,如果这个时候虚拟机不进行验证的话,很可能加载对虚拟机有害的类数据,其次,即使上一步生成的类对象是一个正常的类,但是由于 Java 语言是在不断更新的,因此类和虚拟机之间可能会产生版本差,即类本身用到了一些 Java 语言新版本的特性,而当前虚拟机是旧版本的,即其不能处理新版本的语言特性,这种情况也会导致出错。因此 验证
这一部分是非常重要的。
准备
阶段,虚拟机会将类对象中的 静态变量 (即被 static
关键字修饰的非常量)赋零值,例:
private static int value1 = 3;
对于 short
,int
,long
,float
,double
数值描述的类型赋值为 0,boolean
类型赋值为 false
,char
类型赋值为 \u0000
,引用类型赋值为 null
。具体所指定的值(这里为 3)则会在 初始化
阶段进行赋值。而对于常量,例:
private static final int value1 = 3;
在该阶段会直接赋值为所指定的值(这里即为 3)。
解析
阶段虚拟机会针对类和接口、字段、类方法、接口方法、方法类型等进行解析,这个过程会加载该类定义的字段的类型中还未被加载进入虚拟机中的类。
初始化
阶段是类加载的最后一个阶段,这个阶段中虚拟机会调用<cinit>
方法,当然,这个方法不是开发者写的,而是在编译器编译类的过程中编译器加上的,编译器会收集静态变量赋值代码和 static{}
代码块,将这些代码放入 <cinit>
方法中,收集的顺序即为代码块在类中声明的顺序。上面 准备
步骤中静态变量的最终值会在该方法中赋值给该静态变量。同时,虚拟机会保证在子类的 <cinit>
方法执行时,其父类的 <cinit>
已经被执行完毕,其次,每个 <cinit>
方法只会被调用一次。这里说一下编译器为类生成的 <init>
方法和 <cinit>
方法区别:<init>
在创建某个类的实例对象时被调用,而 <cinit>
方法在类加载的初始化阶段被调用,对于同一个类对象来说,<init>
方法可能被调用多次(每次实例化该类的一个对象时被调用),而 <cinit>
方法只会被调用一次(类加载时)。这里给出一个关于类的相关方法调用顺序的实践代码:
public class InvokeOrder {
static int v = 1;
static {
System.out.println(Thread.currentThread() + ": InvokeOrder cinit invoke: v = " + v);
}
int vv = 2;
{
System.out.println(Thread.currentThread() + ": InvokeOrder init invoke: vv = " + vv);
}
InvokeOrder() {
System.out.println(Thread.currentThread() + ": InvokeOrder constructor invoke");
}
static class SubClass extends InvokeOrder {
static int x = 3;
static {
System.out.println(Thread.currentThread() + ": SubClass: cinit invoke: x = " + x);
}
int xx = 4;
{
System.out.println(Thread.currentThread() + ": SubClass init invoke: xx = " + xx);
}
SubClass() {
System.out.println(Thread.currentThread() + ": SubClass constructor invoke");
}
}
public static void main(String[] args) {
SubClass subClass = new SubClass();
}
}
结果如下所示:
由此,我们可以得出创建一个还未被加载的类的实例对象时相关代码的执行顺序:父类静态代码块->子类静态代码块->父类非静态代码块->父类构造方法->子类非静态代码块->子类构造方法。并且我们还可以发现:虚拟机不会另辟线程去进行类加载,进行类加载的线程即为执行了导致该类被加载的代码的线程。
我们在上文中已经知道了在类加载过程中虚拟机允许开发者来自定义要加载的类数据,那么开发者该通过什么方式来自定义这个要加载的类数据呢?答案自然是大名鼎鼎的类加载器(ClassLoader
)。
我们从上文中已经知道了,整个类加载过程的 准备
阶段中获取类数据的操作是开发者可控的,即具体怎么加载类数据和加载什么类数据是可以由开发者决定的。那么开发者通过什么控制这个操作呢?自然是大名鼎鼎的类加载器(ClassLoader
)。
ClassLoader
我们还是从官方对这个类的说明开始:
/**
* A class loader is an object that is responsible for loading classes. The
* class <tt>ClassLoader</tt> is an abstract class. Given the <a
* href="#name">binary name</a> of a class, a class loader should attempt to
* locate or generate data that constitutes a definition for the class. A
* typical strategy is to transform the name into a file name and then read a
* "class file" of that name from a file system.
*
* <p> Every {@link Class <tt>Class</tt>} object contains a {@link
* Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
* it.
*/
大概意思为类加载负责进行类的加载,同时 ClassLoader
是一个抽象类,其功能是通过给定的类名生成描述这个类的二进制数据。一个典型的场景是将类名转换为对应的类文件名并读取该文件,得到对应的类数据,再生成对应的 Class
对象。每一个 Class
对象都包含一个指向加载它的 ClassLoader
对象的引用字段,可以通过 Class
类的实例方法 getClassLoader()
来的到这个 ClassLoader
对象。
从上面的说明我们可以知道:ClassLoader
具有的能力是解析类的二进制数据来生成对应的 Class
对象,ClassLoader
类提供了 defineClass(byte[] b, int off, int len)
方法来完成这个功能,ClassLoader
类部分源码如下: