当一个类加载到内存后,JVM运行时数据区域都需要存储什么东西呢?

1)类的格式信息、主次版本号、 常量池、字段、方法、属性信息需要存储,这部分数据需要每个线程共享;
当类加载器将静态的二进制字节码从类文件加载到内存后,对内容进行验证、解析转换为符合JVM存储的数据格式。

2)生成的Class类对象和代码运行时动态创建的对象和数组需要存储;
通过loading、linking、initializing后生成的Class对象,做为程序访问这个类的入口,
以及程序运行时产生的对象和数组,都有可能会被不同的线程引用和访问,因此这部分区域的数据也需要线程共享。
3)每个线程的线程数据需要存储和每个线程下方法的方法数据需要存储;
当线程创建的时候需要有一个线程栈来存放线程信息,以及PC计数器记录当前线程正在执行的代码行号。
还有JVM本地线程执行时需要的本地线程栈存放本地线程的执行情况。

《Java虚拟机规范》规定了如下内存区域来存储数据

  • 共享区域
  • 堆区
  • 方法区
    • 运行时常量池
    • 线程私有区域
  • JVM线程栈
  • 本地方法栈
  • PC计数器

Runtime DataArea

堆区

几乎所有的对象以及数组都存放在堆区。

方法区

用于存储已经被虚拟机加载的类型信息、常量、静态变量、及时编译器变异后的代码缓存等数据。

运行时常量池

运行时常量池是方法区的一部分,用于存放编译期或者运行期生成的各种字面量和符号引用。

Java虚拟机栈

每个线程会生成一个线程栈,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口信息。

局部变量表

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、long 、float、double)、引用类型、returnAddress类型。
这些数据类型在局部变量表中的存储空间以局部变量槽来表示(64位的long和double占用2个变量槽,其余数据类型占用1个变量槽)。局部变量表所需的内存空间在编译期完成分配,因此当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是确定的。在运行期间是不会改变局部变量表的大小(槽的多少)。

本地方法栈

与虚拟机栈作用类似,为虚拟机使用到的本地方法服务。

PC计数器

当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

对于JDK1.8以后HotSpot虚拟机改用与Jrokit、J9一样与本地内存中实现的元空间(Meta-space)来实现方法区。无论是原空间还是永久代,他们只是方法区的实现,字符串常量池和静态变量的存储逻辑上仍然属于方法区。元空间使用的是物理内存、永久代使用的是JVM内存。因此元空间的大小只受限于物理内存。

HotSpot 虚拟机实现

HotSpot 虚拟机对象的创建与引用

对象的创建

1) 在new一个对象的时候,先需要通过类加载检查,然后虚拟机为新生对象分配内存。

对象所需要的内存大小在类加载完成后便可以完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

  • 指针碰撞
    假设内存规整,将使用过的在一边,空闲的在一边,中间放一个指针作为分界点的指示器,所分配内存就是把指针向空闲空间方向挪动一段与对象相等的距离。
    使用Serial、ParNew带压缩整理过程的GC收集器时,采用指针碰撞,简单高效;
  • 空闲列表
    假设内存不规整,是一块块不连续的,那么就需要维护一个列表记录哪块内存可用,哪块内存不可用,在分配的时候在列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。
    使用CMS基于清除算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。
Thread Local Allocation Buffer

在堆中创建对象是特别频繁的事情,并且涉及到多线程的访问。如何保证高效且安全的申请空间?

  • 同步或者CAS+失败重试的方式保证更新操作的原子性;
  • 为每个线程在堆中预先分配一小块内存,通过-XX:+/-UseTLAB 设定;

2)分配完内存后,为内存空间初始化零值以及必要的设置。
初始化零值可以保证对象实例字段在Java对象中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
之后设置这个对象的类型、关联类的元数据信息、GC分代年龄信息等。

3)执行 <init>() 方法,将对象进行初始化。

对象的内存布局

在Hotspot虚拟机中,对象在内存中的存储布局划分为三个部分

  • 对象头(Header)
    • 一部分是对象自身运行时数据,HashCode, GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳(数组长度)。
    • 一部分是类型指针,指向它的类型元数据的指针,用来确定是哪个类的实例。
  • 实例数据(Instance Data)
    字段的存储顺序受到虚拟机分配策略(-XX:FieldsAllocationStyle)和字段在源码中的顺序影响。
    默认相同宽度的字段被分配到一起存放,父类中定义的变量会出现在子类之前。
    使用+XX:CompactFields=true字类中较窄的变量也会插入到父类变量的空隙之中。
  • 填充(Padding)
    为了满足硬件的性能需求和提高垃圾回收时指针扫描的方便性。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8个字节的整数倍(任何对象的大小必须是8个字节的整数倍)。

对象的访问定位

对象在堆中创建好了,Java程序会通过栈上局部变量表中的reference字段来引用堆上的对象。

  • 句柄访问
    Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。
    好处:reference中存储的事稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针。
    句柄访问
  • 直接指针
    reference中存储的是对象的地址。好处是速度快,节约了指针定位的时间开销。
    直接指针

计算机为了提升效率,增加了多内核,并且为每个内核增加了自己的高速缓存,而他们又共享同一个主内存。

计算机处理器、高速缓存、主内存之间的交互关系

除了增加高速缓存外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,但保证结果与顺序执行的结果一致。

与处理器的乱序执行优化类似,Java虚拟机的即时编译器也有指令重排序优化。

Java内存模型

《Java虚拟机规范》中曾试图定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问差异。

主内存与工作内存

Java内存模型的主要目的是定义程序中各种变量的访问规则(关注虚拟机中把变量存储到内存和从内存中取出变量的底层细节。)
Java内存模型规定了所有的变量都存储在主内存(除线程私有的以外)。
线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的数据。不同的线程之间也无法直接访问对方工作内存的变量,线程之间变量值的传递均需要通过主内存来完成。

内存间交互操作

关于主内存与工作内存之间的交互协议,Java内存模型中定义了8种的操作。

Java虚拟机实现时必须保证每种操作都是原子的,不可再分的(对于double和long来讲,load、store、、read、write允许有例外)。

  • lock : 锁定,作用于主内存的变量,把一个变量标识为一条线程独占的状态。

  • unlock: 解锁,作用于主内存的变量,把一个处于锁定状体的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read: 读取,作用于主内存的变量,把一个变量的值从主内存传输到工作内存,以便随后的load动作使用。

  • write: 写入,作用于主内存的变量,把store操作从工作内存中得到的变量值存放到主内存变量中。

  • load: 载入,作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use:使用,作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign: 赋值,作用于工作变量,把一个从执行引擎接收的值赋给工作内存变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store: 存储,把工作内存中一个变量的值传送到内存中,以便随后的write操作使用。

Java内存模型规定必须满足如下规则:

1) 不允许read 和 load、store和write操作单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写单主内存不接受
2)不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3)不允许一个线程在没有发生assign操作就把数据从线程的工作内存同步回主内存中。
4) 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load\assign)的变量,即对一个变量进行use之前必须使用load, 使用store之前必须使用assign。。
5)一个变量在同一个时刻内只允许一个线程对其进行lock操作, 但lock操作可以被同一个线程重复执行多次, 多次执行lock后, 只有执行相同次数的unlock操作,变量才会被解锁。
6)对一个变量执行unlock之前, 必须先把此变量同步回主内存中(执行store、write操作)

volatile型变量的特殊规则

volatile是Java虚拟机最轻量级的同步机制。它有两个特性:
1)保证此变量对于所有线程的可见性(一个线程修改了这个变量的值,其他线程立即可见,也就是每个线程在使用被volatile修饰的变量时,都会强制从主内存中同步该变量的值到工作内存中)
在不符合以下两条规则的场景中,仍然需要通过加锁来保证原子性(synchronized、java.util.concurrent中的锁或者原子类):

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
  • 变量不需要与其他状态变量共同参与不变约束;

2) 禁止指令重排序优化

volatile变量读操作的性能与普通变量几乎没有差别,但是写操作会慢些, 因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

Singleton DoubleCheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private Singleton() {}

private volatile static Singleton instance = null;

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); //warning
}
}
}
return instance;
}
}

在上面的代码中,instance = new Singleton()最终会被编译成多条汇编指令。
(1)为Singleton的实例分配内存。
(2)调用Singleton的构造函数,初始化成员变量
(3)将instance对象指向分配的内存空间。

或者可能被重排序为(1)、(3)、(2), 也就是当分配内存后,如果没有对instance 加volatile,将instance对象指向分配的内存空间后,另外一个线程可以读到未被初始化的对象。

针对long和double类型变量的特殊规则

Java内存模型要求lock、unlock、read、write、 load、assign、use、store这8种操作都具有原子性,但是对于64位数据类型的long 和double, 在模型中定义了一条宽松的规定, 允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32为的操作来执行,也就是虚拟机自己选择是否要保证64位数据类型的load、store、read、write这四个操作的原子性。

经过实际的测试,在目前的主流平台下商用的64位Java虚拟机中并不会出现非原子性访问行为,但是对于32位的Java虚拟机(如32位的X86Hotspot)存在非原子性访问的风险。

因此,在实际开发中,除非该数据有明确可知的线程竞争,否则我们在编写代码的时候一般不需要因为这个原因刻意把用到的long和double变量专门声明位volatile。

原子性、可见性与有序性

原子性(Atomicity)

有Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write。因此基本数据类型的访问都是具备原子性的。

如果应用场景需要一个更大范围的保证,需要使用锁、或者synchronized关键字来保证原子性。
(lock、unlock并没有直接开放给用户使用)。

可见性(visibility)

可见性就是当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

volatile

volatile的特殊规则保证了新值立即能同步到主内存,以及每次使用前都立即从主内存刷新。而普通变量则不能保证“立即”。

synchronized

同步块的可见性时由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得。

final

被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值。

this引用逃逸,其他线程有可能通过这个引用访问到“初始化了一般”的对象。

有序性

如果在本线程中观察,所有的操作都是有序的(线程内似表现为串行的语义);
如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序和工作内存与主内存同步延迟现象);

Java语言提供了volatile和sychronized两个关键字来保证线程之间的有序性。
volatile:本身包含了禁用指令重排序的语义;
synchronized:一个变量在同一时刻只允许一个线程对其进行lock操作;

先行发生原则

先行发生原则:判断数据是否存在竞争,线程是否安全的重要手段。

程序次序规则:在一个线程内,按照控制流顺序(分支、循环等),书写在前面的操作先行发生在书写在后面的操作;

管程锁定规则:一个unlock的操作先行发生在后面对同一个锁的操作。

volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;

线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;

线程的中止规则:线程中所有的操作都先行发生于此线程的终止检测,可以通过thread.join()方法是否结束、thread.isAlive()返回值来检测线程是否已经停止;

线程的终端规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过thread.interrupted()检测是否有中断发生;

对象终结规则:一个对象初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始;

传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出,操作A先行发生于操作C。

Java代码要在JVM中正常运行,首先要先编译成.class文件,由类加载器将其加载到内存中。之后由类编译器来编译执行。
在加载的过程中需要注意什么呢?
1)什么时候需要加载一个类呢?
2)随便任何一个文件都是否可以被加载呢,它的加载过程是什么样的?
3)类加载器需要对内存中的内容做什么工作才可以被JVM直接使用?

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

1
2
类型的加载、链接和初始化过程都是在程序运行期间完成的。
Java可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类的加载时机

Java虚拟机规范并没有规定什么时候加载类,但是规定了什么时候初始化一个类。

虚拟机规范严格规定了有且只有5中情况必须立即对类进行“初始化”。
1)遇到new、getstatic、putstatic、invokestatic;
2)初始化类的时候,对父类进行初始化;
3)JVM执行的时候,对主类进行初始化;
4)使用java.lang.reflect包的方法对类进行反射调用时;
5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后解析结果是REF_getstatic, REF_putstatic, REF_invokestatic时,对该类进行初始化;
6) jdk1.8中子类实现的接口中定义了default方法时;

类的加载过程

类的加载过程总共分为:Loading(加载)、Linking(连接)、Initializing(初始化)、Using(使用)、Unloading(卸载)。
其中 Linking阶段 分为 Verification(验证)、Preparation(准备)、Resolution(解析)。

ClassLoader

加载

加载阶段的成果是生成Class类对象,并将该对象存储在堆中。Class对象作为访问该对象数据的访问入口。
加载的步骤:
1) 根据类的全限定名读取类的二进制字节流;
2) 对二进制字节流进行解析转换,并将类信息存储在方法区中;
3) 堆中生成Class类对象,作为程序访问方法区中的类型数据的外部接口;

注意:
数组类不通过类加载器创建,由java虚拟机直接在内存中动态构造出来的。
数组的元素类型(去掉所有维度)要靠类加载器加载完成。
数组的组件类型(去掉一个维度)是引用类型,数组将被标识在加载该组建类型的类加载器的类名称空间上。
如果数组的组件类型不是引用类型,Java虚拟机会把数组标记为与引导类加载器关联。

验证

验证加载的类信息是否符合Java虚拟机规范,类的实现和继承、方法的执行、对象和方法的可访问性等等,主要分为四个阶段:

1)文件格式的验证
主要对Class文件中内容进行验证,是否符合Java虚拟机规范。
比如魔数、常量池的类型、指向常量的索引值是否存在或者类型是否正确等等;

这个阶段的验证是基于二进制字节流进行的,只有通过了验证才会被允许进入Java虚拟机的内存方法区存储。

2)元数据验证
对字节码描述信息进行语义分析,保证描述信息符合《Java语言规范》比如:

  • 这个类是否有父类、父类是否允许继承(被final修饰);
  • 这个类是否实现了父类所要求实现的所有方法;
  • 类中的字段、方法是否与父类产生矛盾;

3)字节码验证
对类的方法体进行验证,保证任何跳转指令都不会跳转到方法体以外的字节码指令上;
保证类型转化都是有效的.

4)符号引用验证

虚拟机在解析阶段将符号引用转化为直接引用的时候,对符号引用进行验证;

  • 符号引用中通过字符串描述的全限定名能否找到对应的类;
  • 在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;
  • 符号引用中的类、字段、方法的可访问性是否可以被当前类访问;

符号引用阶段抛出的异常有:IllegalAccessError、NoSuchFieldError、NoSuchMethodError;

准备

准备阶段是正式为类中定义的变量分配内存并设置类变量初始值的阶段;

对于类变量(静态属性),设置的是“零值”,真正赋值是在初始化阶段;
对于常量(final static),会被设置为ConstantValue属性所指定的初始值。

对于常量,Javac时会为value生成ConstantValue属性。

解析

解析阶段是Java虚拟机将常量池的符号引用替换为直接引用的过程。《Java虚拟机规范》并没有规定何时进行解析。

  • 符号引用:可以无歧义的定位到目标即可;与虚拟机的内存布局无关;引用的目标不一定已经加载到虚拟机中;必须遵循《Java虚拟机规范》;
  • 直接引用:直接可以指向目标的指针、偏移量,或者可以间接定位到目标的句柄;与虚拟机的内存布局直接相关;直接引用的目标一定已经加载到虚拟机内存中;

解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符号引用进行。

类、接口解析:

虚拟机的完整解析过程:
1)如果不是一个数组类型,虚拟机将会把全限定名传给当前类的加载器去加载,在加载的过程中,由于元数据验证、字节码验证的需要,又可能出发其他相关类的加载动作。
2)如果是一个数组类型,并且数组的元素类型为对象,那么会按照第一点的规则加载数组元素类型。如果是基础数据类型包装类,那么由虚拟机生成一个代表该数组唯独和元素的数组对象;
3)前面两部解析OK后进行符号引用验证,确定当前类是否有加载类的访问权限。

如果一个类拥有另外一个类的访问权限,那么至少有一条是成立的:
1)被访问类是public的,并且与访问类处于同一个模块;
2)被访问类是public的,不与访问类处于同一个模块,但是被访问类的模块允许访问类访问;
3)被访问类不是public的,但是与访问类处于同一个包中

字段解析

对字段表内class_index项中索引的CONSTANT_Class_Info符号引用进行解析(即字段所属类)

1)先查找当前类是否包含了简单名称与描述符与目标相匹配
2)从下往上的顺序查找实现的接口类
3)从下往上的顺序查找父类
4)都无法查找到,抛出java.lang.NoSuchFieldError.

如果查找成果返回了引用,将会对这个字段进行权限验证,对于不具备权限的访问会抛出IllegalAccessError;

方法解析

解析出方法表class_index项中索引的方法所属的类或者接口的符号引用。
具体的解析过程与字段解析类似。

接口方法解析

与方法解析类似。

初始化

这个阶段Java虚拟机才真正开始执行类中编写的程序代码。程序在这个阶段初始化类变量和其他资源。

初始化阶段也是执行<clinit>()的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的;
编译器收集顺序是由语句在源文件中出现的顺序决定的;
静态语句块中智能访问到定义在静态语句块之前的变量;
定义在静态语句块后的变量,静态语句块可以赋值,但是不能访问;
Java虚拟机保证父类的<clinit>() 总是优先于子类的<clinit>()执行;

如果一个类中没有静态语句块,也没有对变量的赋值操作,编译器可以不为这个类生成clinit()方法。

类加载器

类加载器主要通过一个类的全限定名来获取描述该类的二进制字节流,它处于Java虚拟机的外部,方便让应用程序自己决定如何获取所需的类。

对于任意一个类,必须由加载它的类加载器与类本身来确定在Java虚拟机中的唯一性。
每个类加载器都拥有一个独立的类名称空间。

类加载器的双亲委派模型

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应有自己的父“类加载器”。
当类加载器需要加载一个类的时候,首先看该类是否已经被加载,如果没有被加载则尝试着让父“类加载器”去加载,如果没有父“类加载器”则让bootstrapClassLoader作为父“类加载器”,如果父“类加载器”加载失败,则使用自己的find_class方法去加载。

  • 启动类加载器(Bootstrap Class Loader)
    启动类加载器负责加载<JAVA_HOME>\lib 目录下的jar包(按名字识别,名字不符合的类库存放在lib目录下也不会被加载),也可以指定 -Xbootclasspath参数指定加载目录。启动类加载器无法被java程序直接引用。
  • 扩展类加载器(Extension Class Loader)
    扩展类加载器负责加载<JAVA_HOME>\lib\ext目录下的jar包,或者被java.ext.dirs指定的路径这种的所有类库。这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码实现的。
  • 应用程序类加载器(Application Class Loader)
    负责加载用户类路径上的所有类库。这个类加载器是在sun.misc.Launcher$AppClassLoader实现。
定制自己的类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CustomClassLoader extends ClassLoader {

public CustomClassLoader(ClassLoader parent) {
super(parent);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

String path = "/classes/";
try {
InputStream ins = new FileInputStream(path + name.replace(".", File.separator) + ".class");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
byte[] classData = baos.toByteArray();
return defineClass(name, classData, 0, classData.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) throws Exception {
CustomClassLoader classLoader = new CustomClassLoader(null);
Class clazz = classLoader.loadClass("com.*.ClassLoader");
System.out.println(clazz.getClassLoader());
System.out.println(clazz.getClass());
}

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

无符号数:属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码结构构成的字符串值。

表:由多个无符号数或者其他表作为数据项构成的符合数据类型,所以表都习惯性地以_info结尾。
表用于描述有层次关系的符合结构的数据,整个Class文件就是一张表,由下表中数据项构成。

类型 名称 个数
u4 Magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flag 1
u2 this_class 1
u2 super_class 1
u2 interface_count 1
u2 interfaces interface_count
u2 fields_count 1
field_info fields fields_count
u2 method_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

魔数

每个Class文件的头四个字节称为魔数,其值的16进制表示为0xCAFEBABE(换算为二进制为1100 1010 1111 1110 1011 1010 1011 1110),虚拟机在加载类时分析该文件是否为Class文件。

版本号

第5、6个字节表示次版本号。第7、8个字节表示主版本号。
高版本的JDK能够向下兼容低版本的Class文件,虚拟机会拒绝执行超过其版本的Class文件。

常量池

常量池中存放两大类常量:

  • 字面量常量:
    比较接近Java语言层面的常量概念,比如字符串和被声明为final的常量值。对于范围在-127~128之间的包装类型也是常量。
  • 符号引用常量:属于编译原理方面的概念,包含
    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

常量池的大小不固定,用两个字节表示,第零项空出来,为了满足某些指向常量池的索引值的数据“不引用任何一个常量池项目”。

常量池的每一项常量都是一个表,这个表的第一位都是一个u1类型的标识位。

下面是常量池中的14种项目类型:
常量池中的14种项目类型

下面是常量池中的14种项目类型的结构表:

常量池中的表类型

常量池中的表类型

常量池中数据项类型 类型标志 类型描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型字面值
CONSTANT_Float 4 float类型字面值
CONSTANT_Long 5 long类型字面值
CONSTANT_Double 6 double类型字面值
CONSTANT_Class 7 对一个类或接口的符号引用
CONSTANT_String 8 String类型字面值
CONSTANT_Fieldref 9 对一个字段的符号引用
CONSTANT_Methodref 10 对一个类中声明的方法的符号引用
CONSTANT_InterfaceMethodref 11 对一个接口中声明的方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用

访问标志

访问标志标示了当前类的修饰符,比如 public / final / super / interface / enum / abstract / annotation / synthetic

标志名称 标志值 含义
ACC_PUBLIC 0x00 01 是否为Public类型
ACC_FINAL 0x00 10 是否被声明为final,只有类可以设置
ACC_SUPER 0x00 20 是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE 0x02 00 标志这是一个接口
ACC_ABSTRACT 0x04 00 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x10 00 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x20 00 标志这是一个注解
ACC_ENUM 0x40 00 标志这是一个枚举

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

  • 类索引:this_class, 用于确定这个类的全限定名
  • 父类索引:super_class, 父类索引用于确定这个类的父类的全限定名
  • 接口索引集合:interfaces, 用于描述这个类实现了哪些接口

字段表集合

字段表集合(field_info)用于描述接口或者类中声明的变量,包含类变量和实例变量,但不包含方法内部声明的局部变量。

|类型|名称|数量|
|—-|—-|—-|—-|
|u2 |access_flag|1|访问修饰符|
|u2 |name_index | 1| 字段的简单名称,如String str = “123”, str就是简单名称|
|u2 |descritpor_index|1| 字段或者方法的描述符|
|u2 |attributes_count |1 | |
|attribute_info | attributes | attributes_count| |

访问修饰符

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为public
ACC_PRIVATE 0x0002 字段是否为private
ACC_PROTECTED 0x0004 字段是否为protected
ACC_STATIC 0x0008 字段是否为static
ACC_FINAL 0x0010 字段是否为final
ACC_VOLATILE 0x0040 字段是否为volatile
ACC_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

描述符:
描述符的作用是用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。
根据描述符的规则:基本数据类型以及代表无返回值的void类型都用一个大写的字符来表示;
而对象类型则用字符L加对象的全限定名来描述;

标志符号 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void 按照先参数列表后返回值的顺序描述,参数列表按照顺序放在”()”内部,String toString()描述为V()java.lang.String
L 对象类型
[ 数组类型 [I: 代表int[], [[I:代表int[][]

方法表集合

Class 文件中对方法的描述和对字段的描述完全是一致的, 方法表中的结构和字段表中的接口一样。

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有ACC_VOLATILE和ACC_TRANSIENT。 但是增加了synchronized,native,abstract,strictfp(strict float point, 精确的浮点数)关键字的修饰符。
对于方法里的代码,经过编译器变异常字节码指令后,存放在方法属性表中一个名为code的属性里。

标志名称 标志值 含义
ACC_PUBLIC 0x00 01 方法是否为public
ACC_PRIVATE 0x00 02 方法是否为private
ACC_PROTECTED 0x00 04 方法是否为protected
ACC_STATIC 0x00 08 方法是否为static
ACC_FINAL 0x00 10 方法是否为final
ACC_SYHCHRONRIZED 0x00 20 方法是否为synchronized
ACC_BRIDGE 0x00 40 方法是否是有编译器产生的方法
ACC_VARARGS 0x00 80 方法是否接受参数
ACC_NATIVE 0x01 00 方法是否为native
ACC_ABSTRACT 0x04 00 方法是否为abstract
ACC_STRICTFP 0x08 00 方法是否为strictfp (strict-float-point,精准的浮点数)
ACC_SYNTHETIC 0x10 00 方法是否是有编译器自动产生的

属性表集合

在Class文件、字段表、方法表中都可以携带自己的属性表集合(attribute_info),用于描述某些场景的专有信息。

不强制要求各属性表的顺序,只要不与已有属性表重名即可。
任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机在运行时会忽略不认识的属性。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
LocalVariableTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault 方法表 用于记录注解类元素的默认值
BootstrapMethods 类文件 用于保存invokeddynamic指令引用的引导方式限定符

对于看了文章还有点懵的同学,看这里的视频讲解,忽略广告。《全网最牛JVM字节码结构分析、Class类文件核心结构》。

注:本系列为《深入理解Java虚拟机》的读书笔记。
少量来源于网络,视频教材。

如何设计一个虚拟机

假设自己要写一个虚拟机,应该考虑什么?

1
2
3
4
5
虚拟机要运行什么?
如何把规范的文件内容读取到内存中?
读取到内存中的数据如何存储?
如何执行这些规范文件中的数据翻译成操作系统认识的指令?
执行完成后如何清理?

虚拟机的主要功能以及组成

虚拟机要运行代码,这些代码要定制一个规范,这样虚拟机才可以根据制定好的规范去解析和执行。对于Java虚拟机来讲,它要读取的文件就是Class类文件。
我们需要一段代码把Class类文件内容读取到内存中,同时要注意判断,读取的文件内容是否符合Class类文件的规范,同时文件内容是否会危害到虚拟机。
把文件内容读取到内存中后,如何存放、方便管理和使用,同时要保证性能、安全性。
把这些文件内容“翻译”为操作系统认识的指令,并且操控这些指令按照预定的“规则”运行,达到可预期的结果。为了提升效率需要考虑多线程和并发。
执行完成后,清理不需要的内存空间,以便其他程序使用(JVM规范中并没有规定垃圾回收部分)。

JVM虚拟机不包含ClassLoader部分

jvm

虚拟机相关知识传送门

扩展阅读:深入理解Java虚拟机到底是什么

Java虚拟机是一种抽象的计算机,它模拟了一套自己的硬件体系结构(处理器、堆栈、指令集和寄存器)。
不同的平台有着不同的实现版本,屏蔽了平台的差异性。使得不同平台上的虚拟机可以运行被javac编译的同一份.class文件。

关于JVM的面试题:
24个Jvm面试题总结及答案
常见JVM面试题及答案整理
2020年JVM面试题吐血整理【过年必看】
总结的JVM面试题
面试必问的JVM应该怎么学(面试题含答案)
大厂面试经:高频率JVM面试问题整理!
2019年JVM面试都问了什么?快看看这22道面试题!(附答案解析)
JVM 面试题 68 问,面试又可以多扯一个小时了!
【搞定Jvm面试】 Java 内存区域揭秘附常见面试题解析

HotSpot 虚拟机常用配置

配置 说明
-Xms1g 最小堆内存
-Xmx4g 最大堆内存
-XX:+UseG1GC 使用G1垃圾回收器
-Xss256k 每个线程栈大小256k
-XX:MetaspaceSize=256m 元数据区大小256M
-XX:MaxMetaspaceSize=256m 元数据区最大256M
-XX:MaxGCPauseMillis=500 GC最大停顿毫秒数
-XX:+DisableExplicitGC 禁用代码中显式调用GC,System.gc()将会失效
-XX:+UnlockExperimentalVMOptions 解锁实验参数
-XX:+UseStringDeduplication GC的同时做重复字符串消除,只用于G1
-XX:InitiatingHeapOccupancyPercent=60 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45.
-XX:ParallelGCThreads=8 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:G1MixedGCCountTarget=4 设置在标记周期完成之后混合收集的数量,以维持old region(也就是老年代)中,最多有G1MixedGCLiveThresholdPercent的存活对象。默认值为8,混合收集的数量将维持在这个值之内。(JVM build > 23)
-XX:MaxTenuringThreshold=7 年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:+UseGCLogFileRotation 滚动记录GC日志文件
-XX:NumberOfGCLogFiles=5 GC日志文件数量为5个
-XX:GCLogFileSize=64M 单个GC日志文件大小64M
-Xloggc:/app/data/logs/gc-date +%Y%m%d-%H-%M.log GC日志文件存储目录和名称
-XX:+PrintGCDetails 打印GC详情
-XX:+PrintGCDateStamps 打印GC时时间戳
-XX:+PrintHeapAtGC GC时打印堆信息
-XX:+PrintAdaptiveSizePolicy 自适应大小策略,每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。
-XX:+UseFastAccessorMethods
-XX:SoftRefLRUPolicyMSPerMB=0 softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap

命令行工具

jps

列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟唯一ID(与操作系统进程ID一致)。

jps [option] [hostid]

选项 作用
-q 只输出lvmid,省略主类的名称
-m 输出虚拟机启动时传递给主类main()的参数
-l 输出主类的全名,如果是jar包,则输出jar路径
-v 输出虚拟机启动时的jvm参数

jstat

用于监视虚拟机各种运行状态信息的命令行工具。
显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。

jstat [option vimid [interval[s|ms] [count]]]
[protocal:][//]lvmid[@hostname[:port]/servername]

选项 作用
-class 监视类加载、卸载数量、总空间以及类装载耗费的时间
-gc 监视Java堆状况,包括Eden、Survivor、老年代、永久代的容量,已用空间,垃圾收集时间
-gccapacity 监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到最大最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,会额外输出导致上一次垃圾收集产生的原因
-gcnew 监视新生代垃圾收集情况
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
-gcold 监视老年代垃圾收集状况
-gcoldcapacity 与-gcold相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出即时编译器编译过的方法、耗时等信息
-printcompilation 输出已经被即时编译的方法

每隔1s查询一次2105的gc情况,查询10次。
jstat -gc 2105 1000 10

jstat -gcutil 2105

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

jinfo

实时查看和调整虚拟机各项参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usage:
jinfo [option] <pid>
(to connect to running process)
jinfo [option] <executable <core>
(to connect to a core file)
jinfo [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)

where <option> is one of:
-flag <name> to print the value of the named VM flag
-flag [+|-]<name> to enable or disable the named VM flag
-flag <name>=<value> to set the named VM flag to the given value
-flags to print VM flags
-sysprops to print Java system properties
<no option> to print both of the above
-h | -help to print this help message

jmap

用于生成堆转储快照,查询finalize执行队列、Java堆和方法区的详细信息(空间使用率、当前使用收集器类型)

生成堆转储文件的方式:

1
2
3
4
-XX:+HeapDumpOnOutOfMemoryError 在虚拟机内存溢出异常出现后自动生成堆转储快照文件。
-XX:HeapDumpOnCtrlBreak 使用ctrl+Break 生成堆转储快照文件
kill -3 pid 生成堆转储快照文件
jmap -dump:format=b,file=heap.dump 3120
选项 作用
-dump 生成java堆转储快照,格式为-dump:[live,]format=b,file=,live子参数说明是否只dump出存活对象
-finalizerinfo 显示再F-Queue中等待Finalizer线程执行finalize方法的对象。只有在Linux平台有效
-heap 显示Java堆详细信息,使用回收器类型、参数配置、分代状况,只在Linux平台下有效
-histo 显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat 在以ClassLoader为统计口径显示永久代内存状态。只在Linux下有效
-F 当虚拟机进程对-dump选项没有响应时,可以使用-F强制生成dump快照,只在Linux下有效

jhat

用于分析jmap生成的堆转储快照。

jhat heap.dump

jhat功能相对简陋,分析工作耗时且耗费硬件资源,一般不在本机使用。
替代方案:VisualVM、Eclipse Memory Analyzer、IBM HeapAnalyzer。

jstack

生成虚拟机当前时刻的线程快照。目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)

Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

高级工具

JConsole

基于JMX的可视化监视、管理工具。

JHSDB

基于服务性代理实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言实现的API集合。
JCMD 和JHSDB工具对比

VisualVM

多合一故障处理工具,不需要被监视的程序基于特殊Agent执行,通用性强,对应用程序的实际性能影响较小,可以直接在生产环境使用。

Java Mission Control

可持续在线监控工具。

arthas

阿里巴巴提供的性能分析工具
https://alibaba.github.io/arthas/

编译

  • 前端编译器: JDK的Javac 、EclipseJDT中的增量式编译器(ECJ)
  • 即时编译器:HotSpot虚拟机的C1\c2编译器、Graal编译器
  • 提前编译器:JDK的Jaotc、GNU Compiler for Java, Excelsisor JET.

前端编译器

编译过程分为1个准备过程和3个处理过程:
1)初始化插入式注解处理器
2)解析与填充符号表

  • 语法、词法分析
    词法分析是将源代码的字符流转变为标记集合的过程,单个字符是程序编写的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。

    语法分析时根据标记序列构造抽象语法树的过程,抽象语法树是用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表程序代码中的一个语法结构。例如包、类型、修饰符、运算符、接口、返回值、代码注释等。

    经过词法和语法分析生成语法树以后,编译器后续的操作都建立在抽象语法树之上。

  • 填充符号表
    符号表是一组符号地址和符号信息构成的数据结构,符号表中所登记的信息在编译的不同阶段都要被用到。如,在语义分析过程中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,对符号名进行地址分配时,符号表是地址解析分配的直接依据。

3)注解处理器
插入式注解处理器可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器(javac)的工作过程。

如果在处理注解期间对语法树进行过修改,编译器将回到解析以及填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改位置,每次循环称为一个轮次。

4)语义分析与字节码生成

语义分析过程分为标注检查和数据以及控制流分析两个步骤。

在经过语法分析后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查等等。

  • 标注检查
    检查的内容包括变量是用钱是否已经被声明、变量与赋值之间的数据类型是否能够匹配等等。
    常量折叠:int a = 1 + 1 ; ==> int a = 2;

  • 数据流以及控制流分析
    数据流分析以及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了等问题。
    编译器的数据以及控制流分析与类加载时的数据以及控制流分析的目的基本哈桑一致,但是校验范围有所区别。

  • 解语法糖
    Java语言的语法糖包含泛型、变长参数、自动装箱拆箱等,Java虚拟机在运行时并不知节支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程称为解语法糖。

  • 字节码生成

    字节码生成阶段需要把前面哥哥步骤生成的信息转化成字节码指令写到磁盘中外,还要进行少量的代码添加和转换工作。
    如实例构造器<init>()方法和<cinit>()方法的添加。
    如果用户代码中没有提供任何构造函数,那么编译器将会添加一个没有参数的、可访问性与当前类型一致的默认构造函数,这个工作在填充符号表阶段已经完成。

    编译器会把语句块、变量变量初始化、调用父类的实例构造器方等操作收敛到<init>()和<cinit>()方法中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序执行,

语法糖

泛型、自动装箱、拆箱、遍历循环、变长参数和条件编译、内部类、枚举类、断言、数值直面量、枚举和字符串的switch、try with resource, lambda等。

泛型

类型擦除

自动装箱、拆箱和循环遍历

1)大于127的值不会自动拆箱
2)没有遇到运算符的情况下不会自动拆箱
3)equals方法不处理数据转型

变长参数

条件编译

Java编译器将所有编译但愿的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间可以互相提供符号信息。

根据条件中布尔值常量值的真假,编译器将把分支中不成立的代码块消除掉。

后端编译

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存。
当程序启动以后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,减少解释器的中间损耗,获得更高的效率。

即时编译器

即时编译器将运行比较频繁的代码块或者方法编译成本地机器吗,并且对这段代码进行优化。

在Hotspot中内置了2个即时编译器 Client Compiler、 Server Compiler。 虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用-client 或者 -Server 来强制指定虚拟机运行在客户端模式或者服务端模式。

解释模式(Interperted Mode):-Xint
编译模式(Compiled Mode): -Xcomp
混合模式(Mixed Mode): -Xmixed (默认)

编译器编译本地代码需要占用程序运行时间,位了打到响应速度与运行效率之间打到最佳平衡,HotSpot在编译子系统中加入了分层编译的功能。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
第0层,程序春解释执行,并且解释器不开启性能监控。
第一层:使用客户端编译器将字节码编译为本地代码,进行简单可靠的稳定优化,不开启性能监控功能。
第二层:使用客户端编译器执行,仅开启方法以及回边次数统计等优先的性能监控功能。
第三层:使用客户端编译器执行,开启全部性能监控,除了第2层的痛就之外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
第四层:使用服务端编译器将字节码编译为本地代码,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

实施分层编译后, 解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无需额外承担手机性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时, 客户端编译器可先采用简单优化来为它争取更多的编译时间。

编译对象与触发条件

被编译的热点代码有:被多次调用的方法和被多次执行的循环体。这两种情况编译的目标对象都是整个方法体,而不是单独的循环体。

基于采样的热点探测

周期性的检查线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。
优点:简单高效,容易获取方法的调用关系
缺点:不能精确确认方法的忍睹,容易受到线程阻塞或者外界因素影响扰乱热点探测。

基于计数器的热点探测

为每个方法建立计数器,统计方法的执行次数,如果超过一定的阈值就认为它是热点方法。
优点:结果精确严谨
缺点:每个方法建立并维护计数器,不能直接获取到方法的调用关系。
HotSpot为每个方法准备了两类计数器:方法调用计数器和回边计数器(在循环边界往回跳),当调用次数超过设定的阈值,就会触发即时编译。
-XX:CompileThreshold 来设定触发即时编译的阈值。
默认设置下,方法调用计数器统计的是相对执行瓶绿,当一个时间段内调用次数不足以提交即时编时,这个方法的调用计数器就会被减半(衰减),可以使用-XX:-UseCounterDecay关闭热度衰减。可以设置-XX:CounterHalfLifeTime设置半衰周期的时间,单位是秒。

当方法被调用时,首先检查该方法是否被即使编译器编译过,是则优先使用编译后的版本执行,否则将方法调用的计数器加一,然后判断方法调用计数器与回边计数器值之和是否超过阈值,超过则像即时编译器提交该方法的代码编译请求。
如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址会被系统自动改写成新值,下次调用该方法时就会使用已编译版本。

方法调用计数器 触发即时编译

回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”,回边计数器的目的是为了触发栈上的替换编译。回边计数器没有热度衰减过程。

编译过程

一般情况下,方法调用产生的标准编译请求和栈上替换编译请求,在虚拟机编译器未完成编译之前,都将按照解释方式执行代码,编译动作在后台的编译线程完成。
设置-XX:-BackgroundCompilation禁止后台编译,禁止后触发即时编译条件时,执行线程向虚拟机提交编译请求后一致阻塞等待,直到编译过程完成后再开始执行编译器输出的本地代码。

服务端编译器效率较低,但是编译速度远超传统的静态优化编译器,相对于客户端编译器编译输出的代码质量有很大的提交,可以大幅降低本地代码的执行时间,从而抵消掉额外的编译水间开销。

即时编译的缺点

1)占用程序运行时间
2)占用程序运算资源

提前编译器

提前编译器的分支:
与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作;
把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码时直接加载进来使用。

编译器优化技术

方法内联

最重要的优化技术之一。

逃逸分析

最前沿的优化技术之一。

  • 栈上分配
    支持方法逃逸,但不支持线程逃逸

  • 标量替换
    如果一个数据已经无法在分解为更小的数据表示时(原始数据类型),这些数据就可以称为标量。
    标量替换就是将用到的成员变量恢复为原始类型来访问的过程。
    加入逃逸分析可以证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不去创建这个对象,而改为直接创建它的若干个被这个的方法使用的成员变量来替代,成员变量存储在栈上。

    同步消除:如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定不会有竞争,对这个变量的同步措施也可以安全地消除掉。

公共子表达式消除

语言无关的经典优化技术。如果一个表达式E之前已经被计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。

数组边界检查消除

语言相关的经典优化技术。

Java技术体系的自动内存管理最根本目标是自动化地给对象分配内存以及自动回收分配给对象的内存。
《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪种垃圾收集器,以及虚拟机中与内存相关的参数设定。

HotSpot虚拟机中对象的分配原则

对象优先在Eden分配

在大多数情况下,对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时,虚拟机将发起一次minorGC。
当survivor区的空间无法存放Eden区的对象时,就会通过担保机制提前转移到老年代去。

-Xms:20M 堆内存初始值20M
-Xmx:20M 堆内存最大20M
-Xmn10M 新生代占用10M(剩下的10M留给老年代)
-XX:SurvivorRatio=8 决定了新生代Eden区与一个Survivor区的空间比例是8:1:1

大对象直接进入老年代

大对象是需要大量连续内存空间的Java对象,如很长的字符串或者元素数量很多的数组。

在分配空间时,如果对象比较大,就会提前触发垃圾收集,以获取足够的连续空间才能存放该对象, 当复制对象时大对象意味着高额的内存复制开销。

-XX:PretenureSizeThreshold=3145728 (大于3M的对象会直接分配到老年代中)
该参数只对Serial和ParNew两款收集器有效。

长期存活的对象将进入老年代

内存回收时必须决策那些存活对象应该存放在新生代,哪些存活对象存放在老年代。
虚拟机为每个对象定义了一个对象年龄(Age)存储在对象头重。

对象通常在Eden区诞生,如果经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间,并且将对象的年龄设置为1岁。对象在Survivor中每经过一次MinorGC,Age增加1, 当年龄增加到一定程度的时候,该对象会被移动到老年代中。
可以通过-XX:MaxTenuringThreshold设置(默认为15次)。

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

也就是当前Survivor空间大小为1M,有两个Age=1且大小为256k的对象时,这两个对象就会直接进入老年代。

空间分配担保

在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果条件成立,那么这次MinorGC时安全的;;
如果不成立,虚拟机会先查看-XX:HandlerPromotionFailure参数的设置值是否允许担保失败
如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次今生到老年代对象的平均大小
大于,尝试进行一次MinorGC;
小于,或者-XX:HandlePromotionFailure不允许冒险,则变更为FullGC。

在JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小,就会进行MinorGC,否则就进行FullGc。-XX:HandlePromotionFailure参数不在生效。

垃圾的回收

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

如何判断对象已死

  • 引用计数器
  • 可达性分析算法(固定可作为GCRoot的对象有)
    • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如每个线程背调用的方法堆栈中使用的参数、局部变量、临时变量
    • 在方法区中类静态属性引用的对象,如Java类的引用类型静态变量
    • 在方法区中常量引用的对象,如字符串常量池中的引用
    • 在本地方法栈中JNI引用的对象
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
    • 所有背同步锁持有的对象
    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

对象的引用

  • 强引用,如Object obj = new Object(); 无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉背引用的对象。
  • 软引用,描述有用但非必须的对象,被软引用关联的对象在系统将要发生内存溢出前会把这些对象例如回收范围内进行二次回收。(SoftReference)
  • 弱引用,描述非必须对象,被弱引用的对象只能生存到下一次垃圾收集发生位置。无论内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的时为了能在这个对象被收集器回收时受到一个系统通知(NIO用的RdirectMemory)

垃圾收集器算法

分代收集理论

建立在两个分代假说上:
1)弱分代假说,绝大数对象都是朝生夕灭的。
2)强分代假说,经过越多次收集过程的对象月难消亡。
3)(推理假说)跨代引用假说,跨代引用相对于同代来说仅占极少数。

分代收集区域
  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/ Yong GC)
    • 老年代收集(Major GC / Old GC)(只有CMS会单独回收老年代)
    • 混合收集(Mixed GC),目前只有GC存在混合回收行为。
  • 整堆收集(Full GC)

标记清除算法

标记需要被回收的对象后,回收掉所有被标记的对象;
或者标记不需要被回收的对象,同一回收未被标记的对象。

缺点:
不稳定,标记清除的性能随着对象的增多而降低;
空间碎片化问题,肯能会导致在分配较大对象时无法找到足够的连续空间内存不得不提前触发另一次垃圾收集动作。

mark-sweep

标记复制算法

为了解决标记清除算法面对大量可回收对象时执行效率低的问题。

优点:只需要复制少数存活对象,简单高效
缺点:浪费内存空间。

mark-copy

根据弱分代假说,以及实际情况,大部分的对象在第一次垃圾回收时就会被回收,因此并不需要按照1:1的比例来划分新生代的内存空间。
内存被分为较大的Eden区和较小的两个Survivor区域,HotSpot默认比例是8:1:1。
打给你Survivor空间不足容纳一次MinorGC后存活的对戏那个时,就需要依赖其他内存区域进行分配担保。

标记整理算法

根据老年代对象的存亡特征以及强分代假说提出的算法。
将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

mark-compact

吞吐量:赋值器和收集器的效率总和。
如果在清理时移动,必然会影响到对象回收的效率。
如果清理时不移动,必然会影响到对象分配的效率。

常见的垃圾收集器

Serial / SerialOld

Serial收集器是最古老的收集器,使用标记拷贝算法,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:在内存较小的桌面应用或者Client模式下,额外内存消耗最小,简单高效。

SerialOld同样是单线程收集起,使用标记整理算法。当CMS收集起发生失败时会转为SerialOld工作。

Serial GC

PS / PO

Paralle Scavenge/ Paralle Old 的目标是达到一个可控的吞吐量。

ParalleScavenge 同样是基于标记-复制算法,支持多线程并发收集,
吞吐量 = (运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio 控制垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy 就不需要人工指定新生代的大小,Eden于Survivor的比例,以及晋升老年代的大小,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

ParalleOld 收集老年代,基于标记整理算法。

Paralle Scavenge / Paralle Old

ParNew / CMS

ParNew是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集起完全一致。

ParNew / SerialOld

CMD是一种以获取最短回收停顿时间为目的的收集器。符合希望系统停顿时间尽可能短,以给用户带来良好交互体验的需求。他是一种标记-清除算法实现的。
包含四个步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清除

其中初始标记和重新标记这两个步骤仍需要STW。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象、速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长,但不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记为了修正并发标记期间因为用户程序继续运行导致标记产生变动的部分,这个阶段比初始标记耗时稍长;
并发清除,清理删除掉标记阶段判断已经死亡的对象,不需要移动哦存活对象,因此这个阶段也是与用户线程同时并发。

CMS

CMS缺点:
1) 耗CPU资源
2) 无法处理浮动垃圾,由于预留的内存无法分配新对象时,有可能出现回收失败而导致退化到Serial Old的模式收集。
3)分配占用大空间的对象时,会出现老年代还有很多剩余空间,但是无法找到足够大的连续空间分配对象,不得不提前触发FullGC。
提供了两个参数优化,但是JDK1.9之后已经废弃:
-XX:UseCMS-CompactAtFullCollection 不得不进行FullGC时开启内存碎片的合并整理。
-XX:CMSFullGCsBefore-Compaction,要求CMS收集器在执行过若干次不整理空间的FullGC之后,下一次进入FullGC前进行碎片整理。

Garbage First

G1时一款主要面向服务端应用的垃圾收集器。

在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

G1面向堆内存任何部分来组成回收集(CollectionSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的MixedGC模式。

G1把连续的Java堆内存划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演 新生代的Eden空间Survivor空间,或者老年代空间。收集器能够对不同角色的Region采用不同的=策略取处理,这样无论是新创建的对象还是存活了一段时间、经过多次收集的旧对象都能获得很好的收集再熬过。

Region中有一类Humongous区域专门存放大对象。 G1认为只要大小超过了一个Region容量的一般的对象就可以判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。那些超过了整个Region容量的超级大对象,会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把HumongousRegion作为老年代的一部分进行看待。

G因为将Region作为单词回收的最小单元,所以可以建立可预测的停顿时间模型。
每次根据用户设置的停顿时间-XX:MaxGCPauseMillis, 优先处理回收价值收益最大的那些Region。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在悠闲的时间内获取尽可能高的收集效率。

G1 Region

需要解决的问题

1)跨Region引用的对象: 每个Region都维护自己的记忆集避免全堆作为GCRoots扫描, 这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集是一种Hash表,Key时别的Region的起始地址,Value时一个集合,里面存储的元素时卡表的索引号。这种双向的卡表结构记录了我指向谁和谁指向了我,因此G1只少需要耗费大约10%~20%的额外内存来维持收集工作。

2)在并发阶段如何保证收集线程与用户线程互不干扰地运行?
用户线程在改变对象引用关系的时候,必须保证不能打破原本的对象图结构,导致比阿吉结果出现错误,
CMS使用增量更新算法,G1使用原始快照算法来实现。

垃圾收集对用户线程的影响还体现在回收过程中心窗卷对象的内存分配上,程序要继续运行就肯定会持续有新对象呗创建,G1位每个Region设计了两个名为TAMS(TOp at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时心分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象时被隐式标记过的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致FullGC而产生长时间的STW。

3)如何建立可靠的停顿模型
G1的停顿预测模型时以衰减均值为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可以测量的步骤话费的成本,并分析得出平均值,标准偏差、可行度等统计信息,衰减平均值时比普通的平均值更容易受到新数据的影响,平均值更容易受到新数据的影响,平均值代表整体平均状态,衰减均值代表最近的平均状态。 Region的统计状态越新越能决定器回收的价值。通过这些信息预测现在开始回收的话,哪些region组成回收集采可以在不超过期望停顿时间的约束下获得最高的收益。

G1的回收步骤

初始标记

只标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段的用户线程并发运行时,能正确滴在可用的Region中分配新对象。这个阶段需要停顿线程,但是耗时短,而且是借用进行MinorGC的时候同步完成,所以G1收集器在这个阶段实际并没有额外的停顿。

并发标记

从GCRoots开始对对子红对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但是可以和用户程序并发执行,当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

最终标记

对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。

筛选回收

负责更新Region的统计数据,对哥哥Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间,对存活对象移动时必须暂停用户线程,但是由多条线程完成的。

G1

G1从整体上来看是基于标记整理算法,但从局部来看时基于标记复制算法。无论如何,这两种算法意味着G1再与性期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发限一次收集。

G1位了垃圾收集产生的内存占用以及额外的执行副在都比CMS搞。
内存占用是由于双向卡表占用空间大。CMs只记录了老年代到新生代的卡表记录。

负载:CMS用写后屏障维护更新卡表;
G1除了使用写后屏障维护卡表外,位了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发前的指针变化。 原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点。

CMS的写屏障是同步操作,G1使用的是消息队列,把写前屏障和写后屏障中要做的事情都放在队列里,再异步处理。