本文整理自网络和书籍。推荐个很赞的资源:Java虚拟机
当调用java命令运行某个程序的时候,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里面,它们都使用该JVM虚拟机进程的内存区。当系统出现以下集中情况的时候,JVM进程将被终止:
System.exit()
或者Runtime.getRuntime().exit()
代码处结束程序。比如,定义了一个如下的类:
public class TestJVMStatic {
public static int a = 6;
}
public class ATest {
public static void main(String[] args) {
TestJVMStatic a = new TestJVMStatic();
a.a++;
System.out.println(a.a);
}
}
public class BTest {
public static void main(String[] args) {
TestJVMStatic b = new TestJVMStatic();
System.out.println(b.a);
}
}
当以此执行ATest和BTest后,a的值分别为7和6。虽然同一个类的所有实例的静态变量共享同一块内存区,但是上述代码两次运行的Java程序分别位于不同的JVM虚拟机进程中,两个JVM之间并不会共享数据。
最好的人,像孩子一样,真诚。像夕阳一样,温暖。像天空一样,宁静。
当程序主动使用某个类的时候,如果该类未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所有有时也把这三个步骤统称为类加载或者类初始化。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型(创建了一个java.lang.Class
对象),这就是虚拟机的类加载机制。
注意:其中验证、准备、解析三个部分统称链接。
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定(不固定);它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
这里简要说明下Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:
加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
java.lang.reflect.Proxy
中,就是用ProxyGenerator.generateProxyClass
来为特定接口生成*$Prxoy
的代理类的二进制字节流。SAP Netweaver
)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。java.lang.Class
对象,作为方法区这些数据的访问入口。 相对于类加载过程的其他阶段,加载阶段(准备地说,是在加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader
)来完成,也可以由用户自定义的类加载器完成。开发人员可以通过继承ClassLoader
基类来创建自己的类加载器去控制字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class
类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可以分为如下三个阶段:
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
验证Class文件格式规范,并且能被当前版本的虚拟机处理。
序号 | 内容 |
---|---|
1 | Class文件是否以魔数 0xCAFEBABE 0 x C A F E B A B E 开头。 |
2 | 主、次版本号是否在当前虚拟机处理范围之内等。 |
3 | 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。 |
4 | 指向常量的各种索引值是否有指向不存在的变量或者不符合类型的常量。 |
5 | CONSTANT_Utf8_info 型的变量中是否有不符合UTF8编码的数据。 |
6 | Class文件中的各个部分及文件本身是否有被删除的或者附加的其他信息。 |
7 | …… |
这个阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。
这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合Java语言规范要求。
验证点可能包括如下:
序号 | 内容 |
---|---|
1 | 这个类是否有父类(除了java.lang.Object 之外,所有的类都应当有父类)。 |
2 | 这个类是否继承了不允许被继承的类(被final修饰的)。 |
3 | 如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。 |
4 | 类中的字段、方法是否与父类产生了矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。 |
5 | …… |
第二个阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
该阶段的主要工作就是进行数据流和控制流分析,这个阶段对类的方法体进行校验分析。
这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:
由于数据流验证的高复杂性,虚拟机设计团队为了避免将过多的时间消耗在字节码验证阶段,在JDK1.6之后的Javac编译器中进行了一项优化,给方法体的Code属性的属性表中增加了一项名为“StackMapTable
”的属性,这项属性描述了方法体中所有的基本块(Basic Block
,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,这可以将字节码验证的类型推导转变为类型检查从而节省一些时间。
当然,理论上StackMapTable
也存在错误或者被篡改的可能,所以是否有可能在恶意篡改了Code属性的同时,也生成相应的StackMapTable
属性来骗过虚拟机的类型校验则是虚拟机实现时值得思考的问题。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static
修饰的变量),而不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
public static int value = 12;
那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何Java方法,而把value
赋值为123的putstatic
指令是程序被编译后,存放于类构造器
方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue
属性,那在准备阶段变量value
就会被初始化为ConstantValue
属性所指定的值,建设上面类变量value
定义为:
public static final int value = 123;
编译时javac
将会为value
生成ConstantValue
属性,在准备阶段虚拟机就会根据ConstantValue
的设置将value设置为123。
基本数据类型的零值:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
直接引用(Direct References):直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry
、checkcast
、getfield
、instanceof
、invokeinterface
、invokespecial
、invokestatic
、invokevirtual
、multianewarray
、new
、putfield
和putstatic
这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info
、CONSTANT_Fieldref_Info
、CONSTANT_Methodef_Info
、CONSTANT_InterfaceMethoder_Info
四种常量类型。
类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器
方法的过程。
在类的初始化阶段,虚拟机负责对类进行初始化。在Java类中对类变量指定初始值有两种方式:
方式 |
---|
声明类变量时指定初始值。 |
使用静态初始块为类变量指定初始化值。 |
public class Test {
static {
// 使用静态初始化为变量b指定初始值
b = 6;
System.out.println("--------------");
}
// 声明变量a时指定初始化值
static int a = 5;
static int b = 9; // 一号
static int c;
public static void main(String[] args) {
System.out.println(Test.b);
}
}
输出结果:
--------------
9
上面代码先在静态初始化块中为b变量赋值,此时类变量b的之为6;接着程序向下执行,执行到一号
代码处的时候,这行代码也属于该类的初始化语句,所以程序再次为类b变量赋值。也就是说,当Test类初始化结束后,该类的类变量b的值为9。
为了验证上述结果,我们修改下b变量的声明位置:
public class Test {
static int b = 9; // 一号
static {
// 使用静态初始化为变量b指定初始值
b = 6;
System.out.println("--------------");
}
// 声明变量a时指定初始化值
static int a = 5;
static int c;
public static void main(String[] args) {
System.out.println(Test.b);
}
}
输出结果:
--------------
6
序号 | 内容 |
---|---|
1 | 假如这个类还没有被加载或者连接,则程序先加载并连接该类。 |
2 | 假如该类的直接父类还没有被初始化,则先初始化其直接父类。 |
3 | 假如类中有初始化语句,则系统依次执行这些初始化语句。 |
触发场景 | 内容 |
---|---|
1 | 遇到new 、getstatic 、putstatic 或invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的Java代码场景是:1. 使用new关键字实例化的对象。 2. 读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候。 3. 调用类的静态方法的时候。 |
2 | 使用java.lang.reflect 包的方法对类进行反射调用的时候。 |
3 | 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则必须先初始化其父类。 |
4 | JVM启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。 |
在上面准备阶段public static int value = 12;
在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器
方法,这个阶段完成后value的值为12。
类构造器
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。
类构造器
方法与类的构造函数(实例构造函数
方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类
方法执行之前,父类的
方法已经执行完毕。因此在虚拟机中的第一个执行的
方法的类肯定是java.lang.Object
。
由于父类的
方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。
方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成
方法。
接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的
方法不需要先执行父接口的
方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的
方法。
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的
方法,其他线程都需要阻塞等待,直到活动线程执行
方法完毕。如果一个类的
方法中有耗时很长的操作,那就可能造成多个进程阻塞。
类加载器(ClassLoader)负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class
对象。
一般来说,Java虚拟机使用Java类的方式如下:Java源文件在经过Javac之后就被转换成Java字节码文件(.class 文件)。
类加载器负责读取Java字节代码,当需要使用一个类的时候,Java ClassLoader就会载入class进入内存中,并转换成java.lang.Class
类的一个实例。
每一个这样的实例用来表示一个Java类。实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载。
类加载器负责加载所有的类,系统为所有载入内存中的类生成一个java.lang.Class
实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。那么,怎么样才算是“同一个类”呢?
正如一个对象有一个唯一的标识一样,一个载入JVM中的类也有一个唯一的标志。在Java中,一个类用用其全限定类名(包括包名和类名)作为标识;但在JVM虚拟机中,一个类用其全限类名(包括包名和类名)和其类加载器作为唯一标识。
例如,如果在pg的包中i有一个Person的类,被类加载器ClassLoader的实例负责加载,则该Person类对应的Class对象在JVM中的表示为(Person、pg、k1)。这意味着两个类加载器加载的同名类:(Person、pg、k1)和(Person、pg、k2)是不同的,它们所加载的类也是完全不同、互不兼容的。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟中的唯一性。说通俗一些,比较两个类是否“相等”,只有在两个类是由同一个类加载器的前提之下才有意义,否则,即使这两个类来源于同一个class文件,只要加载它的类加载器不同,那这两个类必定不相等。这里所指的“相等”包括代表类的Class对象的equal
方法、isAssignableFrom()
、isInstance()
方法及instance
关键字返回的结果。
主要分为Bootstrap ClassLoader
、Extension ClassLoader
、Application ClassLoader
和User Defined ClassLoader
。
加载器 | 内容 |
---|---|
启动类加载器(Bootstrap ClassLoader) | 这个类加载器使用C++语言实现,并非ClassLoader的子类。主要负责加载存放在JAVA_HOME/jre/lib/rt.jar 里面所有的class文件,或者被-Xbootclasspath参数所指定路径中以rt.jar命名的文件。 |
扩展类加载器(Extension ClassLoader) | 这个加载器由sun.misc.Launcher$ExtClassLoader 实现,它负责加载JAVA_HOME/lib/ext 目录中的,或者被java.ext.dirs 系统变量所指定的路径中的所有类库。 |
应用程序类加载器(Application ClassLoader) | 这个加载器由sun.misc.Launcher$AppClassLoader 实现,它负责加载classpath 对应的jar及目录。一般情况下这个就是程序中默认的类加载器。 |
自定义类加载器(User Defined ClassLoader) | 开发人员继承ClassLoader 抽象类自行实现的类加载器,基于自行开发的ClassLoader 可用于并非加载classpath 中(例如从网络上下载的jar或二进制字节码)、还可以在加载class文件之前做些小动作 如:加密等。 |
JVM的类加载机制主要有以下三种:
加载机制 | 内容 |
---|---|
全盘负责 | 所谓全盘负责,就是当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。 |
双亲委托模型 | 所谓双亲委托,就是想让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类的时候才尝试从自己的类路径中加载该类。 |
缓存机制 | 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class的时候,类加载器先从缓存区搜寻该Class,只有当缓存区中不存在该Class对象的时候,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。 |
步骤 | 内容 |
---|---|
1 | 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则执行第2步。 |
2 | 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。 |
3 | 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。 |
4 | 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。 |
5 | 当前类加载器尝试寻找Class文件(从与此ClassLoader 相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。 |
6 | 从文件中载入Class,成功载入后跳到第8步。 |
7 | 抛出ClassNotFoundException 异常。 |
8 | 返回对应的java.lang.Class 对象。 |
上图中所展示的类加载器之间的这种层次关系,就称为类加载器的双亲委托模型。双亲委托模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
private ClassLoader parent;
// Hashtable that maps packages to certs
private Hashtable package2certs = new Hashtable(11);
}
双亲委托的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载。
使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object
存放在rt.jar
之中,无论那个类加载器要加载这个类,最终都是委托给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,相反,如果没有双亲委托模型,由各个类加载器去完成的话,如果用户自己写一个名为java.lang.Object
的类,并放在classpath
中,应用程序中可能会出现多个不同的Object类,Java类型体系中最基本安全行为也就无法保证。
java.lang.ClassLoader
类提供的几个关键方法;
loadClass
:此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到;从parent ClassLoader[ExtClassLoader]
中加载;如果没有加载到,则从Bootstrap ClassLoader
中尝试加载(findBootstrapClassOrNull
方法),如果还是加载失败,则抛出异常ClassNotFoundException
,在调用自己的findClass
方法进行加载。如果要改变类的加载顺序可以覆盖此方法;如果加载顺序相同,则可以通过覆盖findClass
方法来做特殊处理,例如:解密,固定路径寻找等。当通过整个寻找类的过程仍然未获取Class对象,则抛出ClassNotFoundException
异常。
如果类需要resolve
,在调用resolveClass
进行链接。
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
findLoadedClass
此方法负责从当前ClassLoader
实例对象的缓存中寻找已加载的类,调用的为native
方法。
protected final Class> findLoadedClass(String name) {
(!checkName(name))
return null;
urn findLoadedClass0(name);
}
private native final Class findLoadedClass0(String name);
findClass
此方法直接抛出ClassNotFoundException
异常,因此要通过覆盖loadClass
或此方法来以自定义的方式加载相应的类。
protected Class> findClass(String name)
throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
findSystemClass
此方法是从sun.misc.Launcher$AppClassLoader
中寻找类,如果未找到,则继续从BootstrapClassLoader
中寻找,如果仍然未找到,返回null。
protected final Class> findSystemClass(String name)
throws ClassNotFoundException {
ClassLoader system = getSystemClassLoader();
if (system == null) {
if (!checkName(name))
throw new ClassNotFoundException(name);
Class cls = findBootstrapClass(name);
if (cls == null) {
throw new ClassNotFoundException(name);
}
return cls;
}
return system.loadClass(name);
}
defineClass
此方法负责将二进制字节流转换为Class对象,这个方法对于自定义类加载器而言非常重要。如果二进制的字节码的格式不符合jvm class
文件格式规范,则抛出ClassFormatError
异常;如果生成的类名和二进制字节码不同,则抛出NoClassDefFoundError
;如果加载的class
是受保护的、采用不同签名的,或者类名是以java.开头的,则抛出SecurityException
异常。
protected final Class> defineClass(String name, byte[] b,
int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError {
return defineClassCond(name, b, off, len, protectionDomain, true);
}
// Private method w/ an extra argument for skipping class verification
private final Class> defineClassCond(String name,
byte[] b, int off, int len,
ProtectionDomain protectionDomain,
boolean verify)
throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain);
Class c = null;
String source = defineClassSourceLocation(protectionDomain);
try {
c = defineClass1(name, b, off, len, protectionDomain, source,
verify);
} catch (ClassFormatError cfe) {
c = defineTransformedClass(name, b, off, len, protectionDomain, cfe,
source, verify);
}
postDefineClass(c, protectionDomain);
return c;
}
resolveClass
此方法负责完成Class对象的链接,如果链接过,则直接返回。
ClassNotFoundException
这是最常见的异常,产生这个异常的原因为在当前的ClassLoader
中加载类时,未找到类文件。
NoClassDefFoundError
这个异常是因为 加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。
LinkageError
该异常在自定义ClassLoader
的情况下更容易出现,主要原因是此类已经在ClassLoader
加载过了,重复的加载会造成该异常。
深入理解Java虚拟机-JVM高级特性与最佳实践
》