admin管理员组

文章数量:1530892

java 编译执行流程

Java 源文件—>编译器—>字节码文件—>JVM—>机器码

Java 内存区域与内存溢出异常

执行引擎: 即时编译器(JIT)/垃圾收集

程序计数器

当前线程所执行的字节码的行号指示器,唯一一个没有 oom 的区域

虚拟机栈

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

局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用和 returnAddress 类型

如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出 StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常

本地方法栈

本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常

此内存区域的唯一目的就是存放对象实例,是垃圾收集器管理的内存区域
Java 虚拟机通过参数-Xmx 和-Xms 设定扩展。
如果在 Java 堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常

方法区

它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
java8 中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,常量池表用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,具备动态性,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern()方法
当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

直接内存

直接内存的分配不会受到 Java 堆大小的限制,但是需要合理设置

虚拟机对象

对象的创建

当使用 Serial、ParNew 等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用 CMS 这种基于清除 (Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存

一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用 CAS 配上失败 重试的方式保证更新操作的原子性;
另外一种是把内存分配的动作按照线程划分在不同的空间之中进 行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定。虚拟机是否使用 TLAB,可以通过-XX:+/-UseTLAB 参数来 设定

虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值

执行构造函数,即 Class 文件中的<init>()方法

对象的布局

对象头(Header, 实例数据(Instance Data)和对齐填充(Padding)

对象头:
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,使用着动态定义的数据结构;

另外一部分是类型指针,即对象指向它的类型元数据的指针,Java 虚拟机通过这个指针 来确定该对象是哪个类的实例

实例数据:
是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

对齐填充:

对象的访问定位

主流的访问方式主要有使用句柄和直接指针两种


使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机 HotSpot 而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了 Shenandoah 收集器的话也会有一次额外的转发,具体可参见第 3 章),但从整个软件开发的范围来看,在各种语言、框架中 使用句柄来访问的情况也十分常见

OutOfMemoryError 异常

要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链,找到泄漏对象是通过怎 样的引用路径、与哪些 GC Roots 相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息 以及它到 GC Roots 引用链的信息

检查 Java 虚拟机的堆参数(-Xmx 与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运 行期的内存消耗

虚拟机栈和本地方法栈溢出

无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候, HotSpot 虚拟机抛出的都是 StackOverflowError 异常

方法区和运行时常量池溢出

java7 之前: 常量池分配在永久代,常量池内存溢出异常抛出 permgen space
java8:
-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存 大小
-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整
-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率
-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比

直接内存溢出

如果发现内存溢出之后产生的 Dump 文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是 NIO),那就可以考虑重点检查一下直接内存方面的原因了

垃圾收集与内存分配策略

哪些内存需要回收

中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭不需要过多考虑如何回收的问题

而堆和方法区这两个区域则有着很显著的不确定性需要考虑如何回收的问题

引用计数算法

单纯的引用计数 就很难解决对象之间相互循环引用的问题

可达性分析算法

通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连, 或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的

固定可作为 GC Roots 的对象
在虚拟机栈(栈帧中的本地变量表)中引用的对象: 各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量
方法区中类静态属性引用的对象: 类的引用类型静态变量
方法区中常量引用的对象
本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象
所有被同步锁(synchronized 关键字)持有的对象
反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存

四种引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值

软引用是用来描述一些还有用,但非必须的对象

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系,唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是 否有必要执行 finalize()方法。假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记,如果对 象要在 finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合

任何一个对象的 finalize()方法都只会被系统自动调用一次,如果对象面临 下一次回收,它的 finalize()方法不会被再次执行

尽量避免使用 finalize()方法

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。

加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。

该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

标记-清除算法

主要缺点有两个:

第一个是执行效率不稳定,如果 Java 堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低

第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

标记-复制算法

为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点

Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾搜集时,将 Eden 和 Survivor 中仍 然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间

标记-整理

其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行

运行时内存

新生代(1/3)

分为 eden 区/servivorFrom/sivivorTo

eden(8/10)

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收

servivorFrom(1/10)

上一次 GC 的幸存者,作为这一次 GC 的被扫描者

servivorTo(1/10)

保留了一次 MinorGC 过程中的幸存者

minorGC 过程(复制算法)
Eden/servivorFrom -> servivorTo/old -> 清空 eden/servivorFrom -> servivorFrom 和 servivorTo 互换

老年代(2/3)

主要存放应用程序中生命周期长的内存对象
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC(标记清楚算法) 进行垃圾回收腾出空间

首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。
MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常

永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常

java8 元数据

元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中。
这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制

HotSpot 的算法细节

根节点枚举

根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统 看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化 的情况,若这点不能满足的话,分析结果准确性也就无法保证。这是导致垃圾收集过程必须停顿所有用户线程的其中一个重要原因,即使是号称停顿时间可控,或者(几乎)不会发生停顿的 CMS、G1、ZGC 等收集器,枚举根节点时也是必须要停顿的

安全点

只是在“特定的位置”记录了这些信息(oopmap(记录引用对象)),这些位置被称为安全点(Safepoint)
例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点

如何在垃圾收集发生时让所有线程(这里其实不包括 执行 JNI 调用的线程)都跑到最近的安全点,然后停顿下来。
这里有两种方案可供选择:抢先式中断 (Preemptive Suspension)(几乎没有采用) 和主动式中断(Voluntary Suspension)
主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志
HotSpot 使用内存保护陷阱的方式, 把轮询操作精简至只有一条汇编指令的程度

安全区域

所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于 Sleep 状态或者 Blocked 状态,
这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。
对于这种情况,就必须引入安全区域(Safe Region)来解决
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化

记忆集与卡表

记忆集(数据结构)用以避免把整个老年代加进 GC Roots 扫描范围
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等
字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为 1,称为这个元素变脏(Dirty),没有则标识为 0。
在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描

写屏障

在 HotSpot 虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的 AOP 切面
应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作
除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题
为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元
素未被标记过时才将其标记为变脏

在 JDK 7 之后,HotSpot 虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。
开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡

并发的可达性分析

可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析, 这意味着必须全程冻结用户线程的运行
当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:
赋值器插入了一条或多条从黑色对象到白色对象的新引用;
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用;

要解决并发扫描时的对象消失问题,产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)
增量更新: 黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象
原始快照: 无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
CMS 是基于增量更新 来做并发标记的,G1、Shenandoah 则是用原始快照来实现

经典垃圾收集器(serial->parallel->cms->g1->shenandoah->zgc)

Serial 收集器

是一个单线程工作的收集器,对于运行在客户端模式下的虚拟机来说是一个很好的选择

parnew 收集器

是 Serial 收集器的多线程并行版本
ParNew 可以说是 HotSpot 虚拟机中第一款退出历史舞台的垃圾收集器

g1

是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作

parallel scavenge

是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器
Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量,也经常被称作"吞吐量优先收集器",主要适合在后台运算而不需要太多交互的分析任务

serial old

是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法

parallel old

是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现
在注重 吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合

cms

真正意义上支持并发的垃圾收集器
是一种以获取最短回收停顿时间为目标的收集器。目前很 大一部分的 Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS 收集器就非常符合这类应用的需求,基于标记-清除算法实现
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)

由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的
CMS 默认启动的回收线程数是(处理器核心数量 +3)/4, 当处理器核心数量不足四个时, CMS 对用户程序的影响就可能变得很大
由于 CMS 收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生
可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction 的值 来提高 CMS 的触发百分比,降低内存回收频率,获取更好的性能
但参数-XX:CMSInitiatingOccupancyFraction 设置得太高将会很容易导致 大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置

空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况
为了解决这个问题, CMS 收集器提供了一个-XX:+UseCMS-CompactAtFullCollection 开关参数(默认是开启的,此参数从 JDK 9 开始废弃),用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在 Shenandoah 和 ZGC 出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从 JDK 9 开始废弃),这个参数的作用是要求 CMS 收集器在执行过若干次(数量 由参数值决定)不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为 0,表 示每次进入 Full GC 时都进行碎片整理)

garbage first

器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式
到了 JDK 8 Update 40 的时候,G1 提供并发的类卸载的支持
G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以
根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果

处理思路是让 G1 收集器去跟踪各个 Region 里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis 指定,默 认值是 200 毫秒),优先处理回收价值收益最大的那些 Region,这也就是“Garbage First”名字的由来

可以由用户指定期望的停顿时间是 G1 收集器很强大的一个功能,设置不同的期望停顿 时间,可使得 G1 在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡
通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的

目前在小内存应用上 CMS 的表现大概率仍然要会优于 G1,而在大内存应用上 G1 则大多能发挥其 优势,这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间

低延时垃圾收集器(shenandoah/ZGC)

内存的扩大,对延迟反而会带来负面的效果

Shenandoah 是一款只有 OpenJDK 才会包含,而 OracleJDK 里反而不存在的收集器
Shenandoah 反而更像是 G1 的下一代继承者,它们两者有着相似的堆内存布局,在初始标记、并发标记等许多阶段的处理思路上 都高度一致,甚至还直接共享了一部分实现代码,这使得部分对 G1 的打磨改进和 Bug 修改会同时反映 在 Shenandoah 之上,而由于 Shenandoah 加入所带来的一些新特性,也有部分会出现在 G1 收集器中,譬 如在并发失败后作为“逃生门”的 Full GC,G1 就是由于合并了 Shenandoah 的代码才获得多线程 Full GC 的支持

Shenandoah 摒弃了在 G1 中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨 Region 的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降 低了伪共享问题(见 3.4.4 节)的发生概率。

初始标记
并发标记
最终标记
并发清理
并发回收
初始化引用更新
并发引用更新
最终引用更新
并发清理

管通过对象头上的转发指针(Brooks Pointer)来保证并发时原对象与复制对象的访问一致性

计划在 JDK 13 中将 Shenandoah 的内存屏障模型改 进为基于引用访问屏障(Load Reference Barrier)[10]的实现,所谓“引用访问屏障”是指内存屏障只拦 截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写,这能够 省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗

ZGC 收集器

ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
ZGC 直接把标记信息记在引用对象的指针上,由于这些标志位进一步压缩了原本就只有 46 位的地址空间,也直接导致 ZGC 能够管理的内存不可以超过 4TB,不能支持 32 位平台,不能支持压缩指针(-XX: +UseCompressedOops)等

并发标记
并发预备重分配
并发重分配
并发重映射

收集器选择

如果你虽然没有足够预算去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟,那 ZGC 很值得尝试

如果你对还处于实验状态的收集器的稳定性有所顾虑,或者应用必须运行在 Win-dows 操作系统 下,那 ZGC 就无缘了,试试 Shenandoah 吧。

如果你接手的是遗留系统,软硬件基础设施和 JDK 版本都比较落后,那就根据内存规模衡量一 下,对于大概 4GB 到 6GB 以下的堆内存,CMS 一般能处理得比较好,而对于更大的堆内存,可重点考 察一下 G1

虚拟机及垃圾收集器日志

java -Xlog:gc* xxx

垃圾收集器参数总结

UseXxxGC

内存分配与回收策略

对象优先在 Eden 分配
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看- XX:HandlePromotionFailure 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者-XX: HandlePromotionFailure 设置不允许冒险,那这时就要改为进行一次 Full GC

虚拟机性能监控,故障处理

jps: 虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一 ID
jstat: 虚拟机统计信息监视工具
是用于监视虚拟机各种运行状态信息的命令行工具
jinfo: java 配置信息工具
是实时查看和调整虚拟机各项参数
jmap: java 内存映像工具
生成堆转储快照的-dump 选项和用于查看每个类的实例、空间占用统计
jstack: java 堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照,通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因

可视化故障处理工具

JHSDB(免费): 基于服务性代理的调试工具
JConsole(免费): java 监控与管理控制台
VisualVM(免费): 功能最强大的运行监视和故障处理程序之一,它可以直接应用在生产环境中生成
浏览堆转储快照
分析程序性能
BTrace 插件动态日志跟踪
JMC(付费)
启动飞行记录时,可以进行记录时间、垃圾收集器、编译器、方法采样、线程记录、异常记 录、网络和文件 I/O、事件记录等选项和频率设定

调优案例分析与实战

大内存硬件上的程序部署策略
单体应用在较大内存的硬件上主要部署方式有两种
通过一个单独的 java 虚拟机实力来管理大量的 java 堆内存
同时使用若干个 java 虚拟机,建立逻辑集群来利用硬件资源

集群见同步导致的内存溢出
集群的写操作会带来很大的网络同步的开销

堆外内存导致的溢出错误
这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制
直接内存
线程堆栈
socket 缓存区
JNI 代码
虚拟机和垃圾收集器

外部命令导致系统缓慢
执行这个 Shell 脚本是通过 Java 的 Runtime.getRuntime().exec()方法来调用的。 这种调用方式可以达到执行 Shell 脚本的目的,但是它在 Java 虚拟机中是非常消耗资源的操作,而且不仅是处理器消耗,内存负担也很重

服务器虚拟机进程崩溃
使用了异步的方式调用 Web 服务,但由于两边服务速度的完全不对等

不恰当数据结构导致内存占用过大

由 window 虚拟内存导致的长时间停顿
在 Java 的 GUI 程序中要避免这种现象,可以加入参数“- Dsun.awt.keepWorkingSetOnMinimize=true”来解决。这个参数在许多 AWT 的程序上都有应用

由安全点导致长时间停顿
是 HotSpot 虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用 int 类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop)
相对应地,使用 long 或者范围更大的数据类型作为索引值的循环就被称为不可数循环 (Uncounted Loop), 将会被放置安全点

开发工具速度调优

升级 JDK 版本的性能变化及兼容问题
在 eclipse.ini 中明确指定- XX:MaxPermSize=256M 这个参数

编译时间和类加载时间的优化
因此通过参数-Xverify:none 禁止掉字节码验证过程也可作为一项优 化措施

调整内存设置控制垃圾收集频率

把-Xms 和-XX: PermSize 参数值设置为-Xmx 和-XX:MaxPermSize 参数值一样,这样就强制虚拟机在启动的时候就把老年代和永久代的容量固定下来,避免运行时自动扩展,参数-XX:+DisableExplicitGC 屏蔽掉 System.gc()

可以通过以下几个参数要求虚拟机生成 GC 日志:
-XX:+PrintGCTimeStamps(打印 GC 停顿时 间)
-XX:+PrintGCDetails(打印 GC 详细信息)
-verbose:gc(打印 GC 信息,输出内容已被前一 个参数包括,可以不写)
-Xloggc:gc.log

选择收集器降低延迟
在 eclipse.ini 中再加入这两个参数,-XX:+UseConc-MarkSweepGC 和-XX:+UseParNewGC(ParNew 是 使用 CMS 收集器后的默认新生代收集器,写上仅是为了配置更加清晰),要求虚拟机在新生代和老年代分别使用 ParNew 和 CMS 收集器进行垃圾回收

修改收集器配置后的 Eclipse 配置
-vm D:/_DevSpace/jdk1.6.0_21/bin/javaw.exe -startup plugins/org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.0.200.v20090519 -product org.eclipse.epp.package.jee.product -showsplash org.eclipse.platform -vmargs -Dcom.sun.management.jmxremote -Dosgi.requiredJavaVersion=1.5 -Xverify:none -Xmx512m
-Xms512m -Xmn128m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+DisableExplicitGC -Xnoclassgc -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=85

虚拟机执行子系统

Class 类文件的结构
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以"_info"结尾

魔数

每个 Class 文件的头 4 个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件
Class 文件的魔数取得很有“浪漫气息”, 值为 0xCAFEBABE

常量池

它是 Class 文件结构中与其他项目关联最多的数据,通常也是占用 Class 文件空间最大的数据项目之一,还是在 Class 文件中第一个出现的表类型数据项目
Class 文件结构中只有 常量池的容量计数是从 1 开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从 0 开始
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
符号引用则属于编译原理方面的概念,主要包括下面几类常量
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量

截至 JDK 13,常量表中分别有 17 种不同类型的常量
CONSTANT_Utf8_info UTF-8 编码的字符串
CONSTANT_Integer_info 整型字面量
CONSTANT_Float_info 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 接口中方法的符号引用
CONSTANT_NameAndType_info 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 表示方法句柄
CONSTANT_MethodType_info 表示方法类型
CONSTANT_Dynamic_info 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 表示一个动态方法调用点
CONSTANT_Moudle_info 表示一个模块
CONSTANT_Package_info 表示一个模块中开放或者导出的包

由于 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名 称,所以 CONSTANT_Utf8_info 型常量的最大长度也就是 Java 中方法、字段名的最大长度
而这里的最大长度就是 length 的最大值,既 u2 类型能表达的最大值 65535。所以 Java 程序中如果定义了超过 64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译
在 JDK 的 bin 目录中专门用于分析 Class 文件字节码的工具:javap (javap -verbose Xxx.class)

访问标志
这个标志用于识别一些类或 者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final

ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否被声明为 final,只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义(必须是真)
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为 abstract 类型,对于接口或者抽象类来说,此标志为真.其他类型为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

类索引, 父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合 (interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定该类型的继承关系
以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0
接口索引集合就用来描述这个类实现了哪些接口,接口顺序从左到右排列在接口索引集合中

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java 语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
字段可以包括的修饰符有字段的作用域(public、private、protected 修饰 符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称
各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
access_flags
接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标志
name_index 和 descriptor_index
它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符
简单名称则就是指没有类型和参数修饰 的方法或者字段名称
描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示,而对象类型则用字符 L 加对象的全限定名来表示
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型 的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”
用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
如方法 void inc()的描述符为“()V”,方法 java.lang.String toString()的描述符 为“()Ljava/lang/String;”,
方法 int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target, int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”

值为 0x0002,代表 private 修饰符的 ACC_PRIVATE 标志位为真
常量表中可查得第五项常量是一个 CONSTANT_Utf8_info 类型的字符 串,其值为“m”
代表字段描述符的 descriptor_index 的值为 0x0006,指向常量池的字符串“I”

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段

方法表集合

方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
仅在访问标 志和属性表集合的可选项中有所区别
因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志。
与之相对,synchronized、native、strictfp 和 abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了 ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志

ACC_PUBLIC 0x0001 是否为 public
ACC_PRIVATE 0x0002 方法是否为 private
ACC_PROTECTED 0x0004 方法是否为 protected
ACC_STATIC 0x0008 方法是否为 static
ACC_FINAL 0x0010 方法是否为 final
ACC_SYNCHRONIZED 0x0020 方法是否为 synchronized
ACC_BRIDGE 0x0040 方法是不是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为 native
ACC_ABSTRACT 0x0400 方法是否为 abstract
ACC_STRICT 0x0800 方法是否为 strictfp
ACC_SYNTHETIC 0x1000 方法是否由编译器自动产生

方法里的 Java 代码,经过 Javac 编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面

要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求 必须拥有一个与原方法不同的特征签名
特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以 Java 语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。
但是在 Class 文件格式之中,特征签名的范围明显要更大一些, 只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个 Class 文件中的

属性表集合

属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序
《Java 虚拟机规范》最初只预定义了 9 项所有 Java 虚拟机实现都应 当能识别的属性,而在最新的《Java 虚拟机规范》的 Java SE 12 版本中,预定义属性已经增加到 29 项
Code
Java 程序方法体里面的代码经过 Javac 编译器处理之后,最终变为字节码指令存储在 Code 属性内,但并非所有的方法表都必须存在这个属性,譬如接口或者抽 象类中的方法就不存在 Code 属性
max_stack 代表了操作数栈(Operand Stack)深度的最大值
max_locals 代表了局部变量表所需的存储空间,在这里,max_locals 的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位
code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令,《Java 虚拟机规范》中明确限制了一个方法不允许超过 65535 条字节码指令,即它实际只使用了 u2 的长度
在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象

ConstantValue
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值
只有被 static 关键字修饰的变量才可以使用这项属性
目前 Oracle 公司实现的 Javac 编译器的选择是,如 果同时使用 final 和 static 来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类 型是基本类型或者 java.lang.String 的话,就将会生成 ConstantValue 属性来进行初始化;如果这个变量没 有被 final 修饰,或者并非基本类型及字符串,则将会选择在<clinit>()方法中进行初始化
ConstantValue 属性是一个定长属性,它的 attribute_length 数据项值必须固定 为 2。constantvalue_index 数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、 CONSTANT_Integer_info 和 CONSTANT_String_info 常量中的一种

Deprecated
Deprecated 属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念
Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通 过代码中使用“@deprecated”注解进行设置

Exceptions
Exceptions 属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在 throws 关键字后面列举的异常

EnclosingMethod
InnerClasses
InnerClasses 属性用于记录内部类与宿主类之间的关联
数据项 number_of_classes 代表需要记录多少个内部类信息
inner_class_info_index

本文标签: 第三版虚拟机Java