JVM类加载

Stella981
• 阅读 284

运行时数据区

java虚拟机定义了若干种程序运行时使用到的运行时数据区

1.有一些是 随虚拟机的启动而创建,随虚拟机的退出而销毁

2.第二种则是与线程一一对应,随线程的开始和结束而创建和销毁。

java虚拟机所管理的内存将会包括以下几个运行时数据区域

JVM类加载  

PC****寄存器

也叫程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。

每一条JVM线程都有自己的PC寄存器

在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)

如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址

如果该方法是native,那PC寄存器的值是undefined。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java****虚拟机栈

与PC寄存器一样,java虚拟机栈(Java Virtual Machine Stack)也是线程私有的。每一个JVM线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

JVM stack 可以被实现成固定大小,也可以根据计算动态扩展。

如果采用固定大小的JVM stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVM Stack初始容量的手段。

如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。

JVM Stack 异常情况:

StackOverflowError:当线程请求分配的栈容量超过JVM允许的最大容量时抛出

OutOfMemoryError:如果JVM Stack可以动态扩展,但是在尝试扩展时无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈时抛出。

Java****堆

在JVM中,堆(heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

Java堆载虚拟机启动的时候就被创建,堆中储存了各种对象,这些对象被自动管理内存系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾回收器)”)所管理。这些对象无需、也无法显示地被销毁。

Java堆的容量可以是固定大小,也可以随着需求动态扩展,并在不需要过多空间时自动收缩。

Java堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。

JVM实现应当提供给程序员调节Java 堆初始容量的手段,对于可动态扩展和收缩的堆来说,则应当提供调节其最大和最小容量的手段。

Java 堆异常:

OutOfMemoryError:如果实际所需的堆超过了自动内存管理系统能提供的最大容量时抛出。

方法区(Method Area

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

方法区在虚拟机启动的时候创建。

方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。

方法区在实际内存空间中可以是不连续的。

Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。

Java 方法区异常:

OutOfMemoryError: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool

运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表现形式,它包括了若干种常量:编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。

运行时常量池是方法区的一部分。每一个运行时常量池都分配在JVM的方法区中,在类和接口被加载到JVM后,对应的运行时常量池就被创建。

在创建类和接口的运行时常量池时,可能会遇到的异常:

OutOfMemoryError:当创建类和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内存空间后就会抛出OutOfMemoryError

本地方法栈

Java虚拟机可能会使用到传统的栈来支持native方法(使用Java语言以外的其它语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)

如果JVM不支持native方法,也不依赖与传统方法栈的话,可以无需支持本地方法栈。

如果支持本地方法栈,则这个栈一般会在线程创建的时候按线程分配。

异常情况:

StackOverflowError:如果线程请求分配的栈容量超过本地方法栈允许的最大容量时抛出

OutOfMemoryError:如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

JVM中对象访问的两种方式

由于Reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针:

如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息如下图所示:

JVM类加载

如果使用直接指针访问方式,java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。如下图所示:

JVM类加载

这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的的时间开销。

JVM的垃圾回收

注:本文根据《深入理解Java虚拟机》第3章部分内容整理而成。

一.如何判断对象是否需要回收?

堆中几乎放着java世界中的所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。而如何判断对象是否应该回收,存在两个算法:引用计数算法(Reference Counting)和根搜索算法(GC Roots Tracing) 。但是Java语言中没有选用引用计数算法来管理内存,主要的原因是它很难解决对象之间的相互循环引用的问题。所以下面我们主要介绍根搜索算法。

在Java和C#中都是使用根搜索算法判定对象是否存活。算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当从GC到某一个对象不可达的时候,也就是说一个对象到GC Roots没有任何引用链相连的时候,这个对象就会被判定为可回收的。

在java中,可作为GC Roots的对象包括下面几种:

1.虚拟机栈(栈帧中的本地变量表)中的引用的对象。

2.方法区中的类静态属性引用的对象。

3.方法区中的常量引用的对象。

4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

根搜索算法的图示如下:

JVM类加载

图:根搜索算法判定对象是否可回收

二.垃圾收集算法

1.标记-清除算法(Mark-Sweep)

这是最基础的垃圾收集算法,算法分为“标记”和“清除”两 个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的主要缺点有两个:一个是效率问题,标记和清除效率都不高;另一个是 空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致分配大对象时没有足够的大的连续空间,而不得不提前触发另一次垃圾收集动作。

JVM类加载

图:标记-清除算法示意图

2.复制算法(Copying)

为了解决效率问题,有了“复制”的 算法,他将可用内存分为大小相同两块。每次只用一块,当一块空间用完了,就将还存活的对象复制到另一块上,然后将刚使用过的内存空间一次清理掉。这样使得 每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。实现简单,运行高效。只是这种算法的代价是将内存缩小到原来的一半,代价 太贵了点。实际上,新生代中的对象98%都是朝生夕死,所以不需要按1:1的比例来分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivior空间。当回收时,将Eden和Survivor中还存活的对象一次性的拷贝到另一块Suivivior中,最后清理掉Eden和刚用过的Survivor空间。

JVM类加载

图:复制算法示意图

3.标记-整理(Mark-Compact)

复制收集算法在对象存活率高的时候就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

因此人们提出另外一种“标记-整理”(Mark-Compact)算法由于老年代中的对象生存周期都较长,有人提出“标记-整理”算法,标记过程和“标记-清理”一样,但在清除已死对象的同时会对存活对象进行整理,这样可以减少碎片空间。

JVM类加载

图:标记-整理算法示意图

4.分代收集

当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,这种算法并没有什么新的思想出现,只是根据对象不同的存活周期将内存划分为几块。一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就用复制算法,只要少量复制成本就可以完成收集。而老年代中因为对象的存活率较高、周期长,就用“标记-整理”或“标记-清除”算法来回收。

Class文件结构

一、Java的语言无关性

到今天为止,或许大部分的程序员都还认为Java虚拟机执行Java程序是一件理所当然和天经地义的事情。但在Java发展之初,设计者们就考虑过了在 Java虚拟机上运行其它语言的可能性。时至今日商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机上运行的语言,如 Clojure,Groovy,JRuby,Jython,Scala,等等。

实现语言无关性的基础仍然是虚拟机和字节码存储格式,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其它语言 的编译器一样可以把程序编译成Class文件,虚拟机不需要关心Class来源于什么语言,只要它符合Class文件应用的结构就可以在Java虚拟机中 运行。如下图所示:

JVM类加载

图1:Java虚拟机提供的语言无关性

二、Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。

表是由多个无符号数或其他表作为数据项组成的符合数据类型。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,由下图所示的数据项构成:

JVM类加载

图2:Class文件格式

下面首先给出一段简单的Java程序,以这段代码编译成的Class文件为基础进行Class文件结构的介绍,代码如下:

public class TestClass{ private int num; public int inc(){ for(int i=0; i<10; i++){ num = i; } return num; }

public static void main(String\[\] args){
    new TestClass().inc();

} }

把代码编译成Class文件后,使用16进制编辑器WinHex打开这个Class文件,可以看到Class的内部结构如下:

JVM类加载

图3:Class文件的结构

2.1 魔数与Class文件的版本

从上面的图2可以看出,Class文件格式表中,第一项为一个u4类型的名为magic的数据项。这是因为每个Class文件的头4个字节称为魔数 (Magic Number),它的唯一作用就是用来确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别。例如图片格 式。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以很随便地被人为改动。

Class文件的魔数的值为:0xCAFFBABE,这个名称的由来据说还有个很有趣的典故,在这里就不详细表述了,有兴趣的可以百度或者google!

class文件魔数值如下图所示:

JVM类加载

图4:Class文件的魔数

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,Jdk1.1之后的每个JDK版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的 Class文件,但不能运行以后版本的Class文件。

根据上面的图4所示,代表版本号的第5个字节和第6个字节值为0x0000,而主版本号的值为0x0032,即十进制的50,该版本号说明这个是可以被JDK1.6或以上版本的虚拟机执行的Class文件。

下图列出了从jdk1.1到jdk1.7之间,主流的JDK版本编译器输出的默认的和可支持的Class文件版本号:

JVM类加载

图5:Class文件版本号

2.2 常量池

紧接着主次版本号之后的是常量池入口,常量池是Class文件结构中与其他项目关联最多的数据类型。常量池之中主要存放两大类常量:字面量 (Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概 念,包括了下面三类常量:

1.类和接口的全限定名(Fully Qualified Name)

2.字段的名称和描述符(Descriptor)

3.方法的名称和描述符。

使用Javap命令输出常量表

D:\JVM>javap -verbose TestClass Compiled from "TestClass.java" public class TestClass extends java.lang.Object
SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#20; // java/lang/Object."":()V
const #2 = Field #3.#21; // TestClass.num:I
const #3 = class #22; // TestClass
const #4 = Method #3.#20; // TestClass."":()V
const #5 = Method #3.#23; // TestClass.inc:()I
const #6 = class #24; // java/lang/Object
const #7 = Asciz num; const #8 = Asciz I; const #9 = Asciz <init>; const #10 = Asciz ()V; const #11 = Asciz Code; const #12 = Asciz LineNumberTable; const #13 = Asciz inc; const #14 = Asciz ()I; const #15 = Asciz StackMapTable; const #16 = Asciz main; const #17 = Asciz ([Ljava/lang/String;)V; const #18 = Asciz SourceFile; const #19 = Asciz TestClass.java; const #20 = NameAndType #9:#10;// "":()V
const #21 = NameAndType #7:#8;// num:I
const #22 = Asciz TestClass; const #23 = NameAndType #13:#14;// inc:()I
const #24 = Asciz java/lang/Object; { public TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V
4: return LineNumberTable: line 1: 0

public int inc(); Code: Stack=2, Locals=2, Args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 19 8: aload_0 9: iload_1 10: putfield #2; //Field num:I
13: iinc 1, 1 16: goto 2 19: aload_0 20: getfield #2; //Field num:I
23: ireturn LineNumberTable: line 5: 0 line 6: 8 line 5: 13 line 8: 19 StackMapTable: number_of_entries = 2 frame_type = 252 /* append */ offset_delta = 2 locals = [ int ] frame_type = 250 /* chop */ offset_delta = 16

public static void main(java.lang.String[]); Code: Stack=2, Locals=1, Args_size=1 0: new #3; //class TestClass
3: dup 4: invokespecial #4; //Method "":()V
7: invokevirtual #5; //Method inc:()I
10: pop 11: return LineNumberTable: line 12: 0 line 13: 11

}

2.3 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是 接口还是类;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否声明为final,等等。

2.4 类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型 的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。 Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这 个类实现了哪些接口,所有被实现的接口按类定义中的implements(如果类是一个接口则是extends)后的接口顺序从左到右排列在接口的索引集 合中。

2.5 字段表集合

字段表(field_info)用于描述接口或类中声明的变量。字段(field)包括了类级变量和实例级变量,但不包括方法内部声明的变量。一个字段 的信息包括:作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、 并发可见性(volatile修饰符,是否强制从主内存读写)、可否序列化(transient修饰符)、字段数据类型(基本数据类型、对象、数组)、字 段名称。这些信息中,各个修饰符都是布尔值,要么有,要么没有。

全限定名称:如果TestClass类是定义在com.chenzhou.jvm包中,那么这个类的全限定名为com/chenzhou/jvm/TestClass。

简单名称:简单名称指没有类型和参数修饰的方法或字段名称,在上面的例子中,TestClass类中的inc()方法和num字段的简单名称分别为“inc”和“num”。

描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型 (byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都用一个大写字符来表 示,而对象则用字符L加对象的全限定名来表示,如下图所示:

JVM类加载

图6:描述符标识字符含义

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如定义一个“java.lang.String[][]”类型的二维数组,将会被记录为:"[[Ljava/lang/String;",一个整型数组"int[]"将被记录为"[I"。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照严格的顺序放在一组小括号"()"内,如方法void inc()的描述符为"()V",方法java.lang.String.toString()的描述符为"()Ljava/lang /String;",方法int indexOf(char[] source,int offset,int count,char[] target,int tOffset,int tCount,int fromIndex)的描述符为"([CII[CIII)I"。

2.6 方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎完全一致。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。

2.7 属性表集合

Java虚拟机规范第二版中预定义了9项虚拟机应当能识别的属性,包括:Code、ConstantValue、Deprecated、 Exceptions、InnerClasses、LineNumberTable、LocalVariableTable、SourceFile、 Synthetic。

根据字节码指令介绍方法执行流程

在上一篇博客中介绍了《Class文件结构》,其中就提到了一个例子,下面我们依然根据该例子的字节码来对方法的执行流程进行讲解。

java类源码如下:

public class TestClass{ private int num; public int inc(){ for(int i=0; i<10; i++){ num = i; } return num; }

public static void main(String\[\] args){
    new TestClass().inc();

} }

使用javap -verbose命令反编译后,输出常量表和字节码如下:

D:\JVM>javap -verbose TestClass Compiled from "TestClass.java" public class TestClass extends java.lang.Object
SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#20; // java/lang/Object."":()V
const #2 = Field #3.#21; // TestClass.num:I
const #3 = class #22; // TestClass
const #4 = Method #3.#20; // TestClass."":()V
const #5 = Method #3.#23; // TestClass.inc:()I
const #6 = class #24; // java/lang/Object
const #7 = Asciz num; const #8 = Asciz I; const #9 = Asciz <init>; const #10 = Asciz ()V; const #11 = Asciz Code; const #12 = Asciz LineNumberTable; const #13 = Asciz inc; const #14 = Asciz ()I; const #15 = Asciz StackMapTable; const #16 = Asciz main; const #17 = Asciz ([Ljava/lang/String;)V; const #18 = Asciz SourceFile; const #19 = Asciz TestClass.java; const #20 = NameAndType #9:#10;// "":()V
const #21 = NameAndType #7:#8;// num:I
const #22 = Asciz TestClass; const #23 = NameAndType #13:#14;// inc:()I
const #24 = Asciz java/lang/Object; { public TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V
4: return LineNumberTable: line 1: 0 public int inc(); Code: Stack=2, Locals=2, Args_size=1 0: iconst_0 //定义一个常量0,放入操作数栈
1: istore_1 //把该常量弹出栈顶存入到局部变量表
2: iload_1 //把该局部变量放入操作数栈
3: bipush 10 //把常量10放入操作数栈
5: if_icmpge 19 //把i和10进行比较
8: aload_0 //加载局部变量表index为0的变量放入操作数栈
9: iload_1 //加载局部变量表index为1的变量放入操作数栈
10: putfield #2; //Field num:I //把i的值赋给num字段
13: iinc 1, 1 //局部变量i自增1
16: goto 2 //跳转到第2行
19: aload_0 //加载局部变量表index为0的变量放入操作数栈
20: getfield #2; //Field num:I //获取字段num的值
23: ireturn //返回
LineNumberTable: line 5: 0 line 6: 8 line 5: 13 line 8: 19 StackMapTable: number_of_entries = 2 frame_type = 252 /* append */ offset_delta = 2 locals = [ int ] frame_type = 250 /* chop */ offset_delta = 16 public static void main(java.lang.String[]); Code: Stack=2, Locals=1, Args_size=1 0: new #3; //class TestClass
3: dup 4: invokespecial #4; //Method "":()V //调用实例初始化方法
7: invokevirtual #5; //Method inc:()I //调用普通方法inc()
10: pop 11: return LineNumberTable: line 12: 0 line 13: 11 }

方法的调用指令分为以下几种:

  1. invokevirtual指令用于调用所有的虚方法。
  2. invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  3. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例构造器化方法、私有方法和父类方法。
  4. invokestatic指令用于调用静态方法(static方法)。

其它具体的指令可以参考:字节码指令集

类加载的时机

本文根据《深入理解java虚拟机》第7章部分内容整理

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备 (Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载 (Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图所示:

JVM类加载

如上图所示,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序来按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段后再开始。

类的生命周期的每一个阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

Java虚拟机规范没有强制性约束在什么时候开始类加载过程,但是对于初始化阶段,虚拟机规范则严格规定了有且只有四种情况必需立即对类进行“初始化”(而加载、验证、准备阶段则必需在此之前开始),这四种情况归类如下:

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始 化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入 常量池的静态字段除外)时、以及调用一个类的静态方法的时候。

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。

4.当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

对于这四种触发类进行初始化的场景,在java虚拟机规范中限定了“有且只有”这四种场景会触发。这四种场景的行为称为对类的主动引用,除此以外的所有引用类的方式都不会触发类的初始化,称为被动引用。

下面通过三个实例来说明被动引用:

示例一

父类SuperClass.java

package com.chenzhou.classloading; /** _ * ClassName:SuperClass
_ * Function: 被动使用类:通过子类引用父类的静态字段,不会导致子类初始化
.
_
_ * Date: 2012-7-18 上午__09:37:06 _
_ * @author _chenzhou _ * _@version _ * @since _JDK 1.6 _ * _@see _ */ public class SuperClass { static{ System.out.println("SuperClass init!"); } public static int value = 123; }

子类SubClass.java

package com.chenzhou.classloading; public class SubClass extends SuperClass { static{ System.out.println("SubClass init!"); } }

主类NotInitialization.java

package com.chenzhou.classloading; /** _ * ClassName:NotInitialization
_ * Function: 非主动使用类字段演示
.
_
_ * Date: 2012-7-18 上午__09:41:14 _
_ * @author _chenzhou _ * _@version _ * @since _JDK 1.6 _ * _@see _ */ public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); } }

输出结果:

SuperClass init!

123

由结果可以看出只输出了“SuperClass init!”,没有输出“SubClass init!”。这是因为对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。

示例二

父类SuperClass.java如上一个示例一样

主类NotInitialization.java

package com.chenzhou.classloading; /** _ * ClassName:NotInitialization
_ * Function: 通过数组定义来引用类,不会触发此类的初始化
.
_
_ * Date: 2012-7-18 上午__09:41:14 _
_ * @author _chenzhou _ * _@version _ * @since _JDK 1.6 _ * _@see _ */ public class NotInitialization { public static void main(String[] args) { SuperClass[] scs = new SuperClass[10]; } }

输出结果为空

没有输出“SuperClass init!”说明没有触发类com.chenzhou.classloading.SuperClass的初始化阶段,但是这段代码会触发 “[Lcom.chenzhou.classloading.SuperClass”类的初始化阶段。这个类是由虚拟机自动生成的,该创建动作由 newarray触发。

示例三

常量类ConstClass.java

package com.chenzhou.classloading; /** _ * ClassName:ConstClass
_ * Function: 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
.
_
_ * Reason: _TODO ADD REASON.
_ * Date: 2012-7-18 上午__09:46:56 _
_ * @author _chenzhou _ * _@version _ * @since _JDK 1.6 _ * _@see _ */ public class ConstClass { static{ System.out.println("ConstClass init!"); }

public static final String _HELLOWORLD_ \= "hello world"; }

主类NotInitialization.java

package com.chenzhou.classloading; /** _ * ClassName:NotInitialization
_ * Function: 非主动实用类字段演示
.
_
_ * Date: 2012-7-18 上午__09:41:14 _
_ * @author _chenzhou _ * _@version _ * @since _JDK 1.6 _ * _@see _ */ public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }

输出:hello world

上面的示例代码运行后也没有输出“SuperClass init!”,这是因为虽然在Java源码中引用了ConstClass类中的常量HELLOWORLD,但是在编译阶段将此常量的值“hello world”存储到了NotInitialization类的常量池中,对于常量ConstClass.HELLOWORLD的引用实际上都被转化为 NotInitialization类对自身常量池的引用了。实际上NotInitialization的Class文件之中已经不存在 ConstClass类的符号引用入口了。

接口的加载过程与类加载的区别在于上面提到的四种场景中的第三种,当类在初始化时要求其父类都已经初始化过了,但是一个接口在初始化时,并不要求其父类都完成了初始化,只有在真正用到父类接口的时候(如引用父接口的常量)才会初始化。

类加载的过程

在我的上一篇文章《JVM学习笔记(六):类加载的时机》中提到了java类从加载到卸载过程包括了加 载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(using)、和卸载(Unloading)七个阶段。下面我们来详细讲解一下类加载的全过程,也就是加载、验 证、准备、解析和初始化这五个阶段的过程。

一.加载

首先要说明的是“加载”(Loading)阶段只是“类加载”(Class Loading)过程的一个阶段。不要混淆了这两个概念。在加载阶段,虚拟机需要完成以下三件事情:

1.通过一个类的权限定名称来获取定义此类的二进制字节流。

2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

相对于类加载过程的其他阶段,加载阶段是开发期相对来说可控性比较强,该阶段既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

二.验证

验证是连接(linking)阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

1.文件格式验证:该阶段主要是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

2.元数据验证:这一阶段主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

3.字节码验证:这一阶段是整个验证阶段最复杂的一个阶段,主要工作是进行数据流和控制流分析。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

4.符号引用验证:主要是在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。

验证阶段对于虚拟机的类加载机制来说,是一个非常重要但不一定是必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,从而缩短虚拟机类加载的时间。

三.准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起被分配在Java堆中。

四.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个 符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

对于同一个符号引用可能会出现多次解析,虚拟机可能会对第一次解析的结果进行缓存。

解析动作分为四类:包括类或接口的解析、字段解析、类方法解析、接口方法解析。

五.初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化阶段是执行类构造器()方法的过程。对于()方法具体介绍如下:

1)()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。

2)()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方 法执行之前,父类的()方法已经执行完毕,因此在虚拟机中第一个执行的()方法的类一定是 java.lang.Object。

3)由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下面的例子所示,输出结果为2而不是1。

public class Parent { public static int A = 1; static{ A = 2; } }

public class Sub extends Parent{ public static int B = A; }

public class Test { public static void main(String[] args) { System.out.println(Sub.B); } }

4)()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

5)接口中可能会有变量赋值操作,因此接口也会生成()方法。但是接口与类不同,执行接口的 ()方法不需要先执行父接口的()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始 化时也不会执行接口的()方法。

6)虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线 程去执行这个类的()方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一 个类的()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。

类加载器以及双亲委派模型

1.什么是类加载器?

在类加载阶段,有一步是“通过类的全限定名来获取描述此类的二进制字节流”,而所谓的类加载器就是实现这个功能的一个代码模块,这个动作是在Java虚拟机外部实现的,这样做可以让应用程序自己决定如何去获取所需要的类。

类加载器的作用:首先类加载器可以实现最本质的功能即类的加载动作。同时,它还能够结合java类本身来确定该类在Java虚拟机中的唯一性。用通俗的 话来说就是:比较两个类是否相等,只有这两个类是由同一个类加载器加载才有意义。否则,即使这两个类是来源于同一个Class文件,只要加载它们的类加载 器不同,那么这两个类必定不相等。

2.双亲委派模型

从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实 现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:

1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。

2)扩展类加载器(Extension ClassLoader):按《深入理解java虚拟机》这本书上所说,该加载器主要是负责加载JAVA_HOME\lib\ext目录中的类库,但是貌似在JDK的安装目录下,没看到该指定的目录。该加载器可以被开发者直接使用。

3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器, 如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:

JVM类加载

如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承 (Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父 类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的 搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载

使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object 类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该 模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变 得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。

在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:

//有两个loadClass()方法,不会最终都是调用第二个方法 public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }

//最终调用的loadClass()方法 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //异步保护,防止重复加载一个相同的类
synchronized (getClassLoadingLock(name)) { // 检测这个类是否被加载了
Class c = findLoadedClass(name); //如果没有被加载,首先让父类去加载
if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) {//父类加载器不是null,也就是说不是BootStrap加载器
c = parent.loadClass(name, false); } else {//父类加载器是BootStrap加载器
c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found
// from the non-null parent class loader } //如果经过父类加载器之后,还是null,也就是说父类无法加载,那么再由自己完成加载
if (c == null) { // If still not found, then invoke findClass in order
// to find the class. long t1 = System.nanoTime(); c = findClass(name);//调用自己的findClass()方法 // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }

通过上面代码可以看出,双亲委派模型是通过loadClass()方法来实现的,根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检 查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失 败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
4个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
技术小男生 技术小男生
4个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue