4. JVM

4.1 GC

1. 垃圾收集

基础 : 可达性分析算法 GC ROOTS

  1. 复制算法
  2. 标记清除
  3. 标记整理
  4. 分代收集 – 1. 新生代 ; 2.3 老年代
    注: Oop Map – 安全点 – 安全区

以下部分内容来自

1. 3 种基本算法

标记清除法/标记压缩法、复制收集算法、引用计数法

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

在之前介绍三色标记法时,其实已经能看到标记-清除算法的影子了,正是因为如此,它是最简单也是最重要的一种算法。
标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段,有对象头标记和位图标记(bitmap marking)这两种方式,后者可以与写时复制技术(copy-on-write)相兼容。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段,回收时会把对象作为分块,连接到被称为「空闲链表(free-lis)」的链表中去。
清除操作并不总是在标记阶段结束后就全部完成的,一种「延迟清除(Lazy Sweep)」的算法可以缩减因清除操作导致的应用 STW 时间。延迟清除算法不是一下遍历整个堆(清除所花费的时间与堆大小成正比),它只在分配对象时执行必要的堆遍历,同时其算法复杂度只与活动对象集的大小成正比。
下图是标记-清除算法执行前后,堆空间的变化情况:

 从上图可以看到,标记-清除算法执行完成后,会让堆出现碎片化,这会带来两个问题:

  • 大量的内存碎片会导致大对象分配时可能失败,从而提前触发了另一次垃圾回收动作;
  • 具有引用关系的对象可能会被分配在堆中较远的位置,这会增加程序访问所需的时间,即「访问的局部性(Locality)」较差。

上述两个问题,将分别由下面介绍的标记-压缩算法和标记-复制算法来解决。

标记-压缩算法(Mark-Compact)

标记-压缩算法是在标记-清除算法的基础上,用「压缩」取代了「清除」这个回收过程,如下图所示,GC 将已标记并处于活动状态的对象移动到了内存区域的起始端,然后清理掉了端边界之外的内存空间。

 压缩阶段需要重新安排可达对象的空间位置(reloacate)以及对移动后的对象引用重定向(remap),这两个过程都需要搜索数次堆来实现,因此会增加了 GC 暂停的时间。标记-压缩算法的好处是显而易见的:在进行这种压缩操作之后,新对象的分配会变得非常方便——通过指针碰撞即可实现。与此同时,因为 GC 总是知道可用空间的位置,因此也不会带来碎片的问题。
标记-压缩算法算法还有很多变种,如 Robert A. Saunders 研究出来的名为 Two-Finger 的压缩算法(论文:The LISP system for the Q-32 computer. In The Programming Language LISP: Its Operation and Applications[4]),可以把堆搜索的次数缩短到2次, Stephen M. Blackburn 等研究出来的 ImmixGC 算法(论文:Cyclic reference counting with lazy mark-scan[5])结合了标记-清除和标记-压缩两种算法,可以有效地解决碎片化问题。

标记-复制算法(Mark-Copy)

标记-复制算法与标记-压缩算法非常相似,因为它们会对活动对象重新分配(reloacate)空间位置。两个算法区别是:在标记-复制算法中,reloacate 目标是一个不同的内存区域。
标记清除算法的优点很多,譬如:

  1. 不会发生碎片化
  2. 优秀的吞吐率
  3. 可实现高速分配
  4. 良好的 locality

对比算法执行前后堆空间的变化,可以看到,不难发现标记-复制算法最大缺点在于所需空间翻倍了,即堆空间的利用率很低。

 标记-复制在复制阶段,需要递归复制对象和它的子对象,递归调用带来的开销是不容忽视的。

引用计数法

引用计数法,它的基本原理是,在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。引用计数的增减,一般发生在变量赋值、对象内容更新、函数结束(局部变量不再被引用)等时间点。当一个对象的引用计数变为 0 时,则说明它将来不会再被引用,因此可以释放相应的内存空间。
缺点:

  1. 无法释放循环引用的对象。
  2. 必须在引用发生增减时对引用计数做出正确的增减,而如果漏掉了某个增减的话,就会引发很难找到原因的内存错误。引用数忘了增加的话,会对不恰当的对象进行释放;而引用数忘了减少的话,对象会一直残留在内存中,从而导致内存泄漏。
  3. 引用计数管理并不适合并行处理: 就如同 中的算法一样,无法在并行情况下对数量进行准确的计算。
2. 3 种进阶算法
分代回收

分代回收的目的,正是为了在程序运行期间,将 GC 所消耗的时间尽量缩短。
分代回收的基本思路,是利用了一般性程序所具备的性质,即大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。
HotSpot 虚拟机中,在新生代用复制算法,老年代使用标记清除/整理算法。

问题 :如果存在老生代对象对新生代对象的引用。如果只扫描新生代区域的话,那么从老生代对新生代的引用就不会被检测到。
这样一来,如果一个年轻的对象只有来自老生代对象的引用,就会被 误认为 已经“死亡”了。
因此,在分代回收中,会对对象的更新进行监视,将从老生代对新生代的引用,
记录在一个叫做 记录集 Rset(remembered set)的表 中。在执行 小回收(Minor Gc) 的过程中,这个记录集也作为一个根来对待。

解决方案 :在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操作的子程序,需要被嵌入到所有涉及对象更新操作的地方。
这个负责记录引用的子程序是这样工作的。设有两个对象:A 和 B,当对 A 的内容进行改写,并加入对 B 的引用时,
如果①A 属于老生代对象,②B 属于新生代对象,则将该引用添加到记录集中。
这种检查程序需要对所有涉及修改对象内容的地方进行保护,因此被称为 写屏障(Write barrier)

增量回收

为了维持程序的实时性,不等到 GC 全部完成,而是将 GC 操作细分成多个部分逐一执行。这种方式被称为增量回收

并行回收

并行回收的基本原理是,是在原有的程序运行的同时进行 GC 操作,这一点和增量回收是相似的。
不过,相对于在一个 CPU 上进行 GC 任务分割的增量回收来说,并行回收可以利用多 CPU 的性能,尽可能让这些 GC 任务并行(同时)进行。

3. Card Table 数据结构

为了支持高频率的新生代的回收,虚拟机使用一种叫做卡表(Card Table)的数据结构.
卡表作为一个 比特位 的集合,每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用.

一、作用
卡表中每一个位表示年老代 4K 的空间,
卡表记录为 0 的年老代区域没有任何对象指向新生代,
卡表记录为 1 的区域才有对象包含新生代引用,
因此在新生代 GC 时,只需要扫描卡表位为 1 所在的年老代空间。使用这种方式,可以大大加快新生代的回收速度。

二、结构
卡表是个单字节数组,每个数组元素对应堆中的一张卡。

每次年老代对象中某个引用新生代的字段发生变化时,Hotspot VM 就必须将该卡所对应的卡表元素设置为适当的值,从而将该引用字段所在的卡 标记为脏
如下图:

在 Minor GC 过程中,垃圾收集器只会在脏卡中扫描查找年老代-新生代引用。

Hotspot VM 的字节码解释器和 JIT 编译器使用 写屏障 维护卡表。
写屏障 (Write barrier) 是一小段将卡状态设置为脏的代码。解释器每次执行更新引用的字节码时,都会执行一段写屏障,JIT 编译器在生成更新引用的代码后,也会生成一段写屏障。
虽然写屏障使得应用线程 增加了 – 性能开销 ,但 Minor GC 变快了许多,整体的垃圾收集效率也提高了许多,通常应用的吞吐量也会有所改善。

4. 评价指标

1、吞吐量
应用系统的生命周期内,应用程序所花费的时间和系统总运行时间的比值
系统总运行时间=应用程序耗时+GC 耗时
2、垃圾回收器负载
垃圾回收器负载=GC 耗时/系统总运行时间
3、停顿时间
垃圾回收器运行时,应用程序的暂停时间
4、垃圾回收频率
垃圾回收器多长时间运行一次。一般而言,频率越低越好,通常增大堆空间可以有效降低垃圾回收发生的频率,但是会增加回收时产生的停顿时间。
5、反应时间
当一个对象成为垃圾后,多长时间内,它所占用的内存空间会被释放掉。

2. 内存分配

1. 基础知识

-Xms 堆大小
-Xmx 可扩展大小
-Xmn 老年代大小
-XX:SurvivorRatio Eden 区与 Survivor 区大小比例

注: surivor 区分为 from 区与 to 区

- 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
- 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
- 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域
- 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”
- 新的“From”就是上次GC前的“To”。
- 不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

  1. 大对象直接进入老年代 :很长的字符串以及数组
  2. 长期存活的对象进入老年代 -XX:MaxTenuringThreshold
  3. 动态对象年龄判定 :如果在 Survivor 中,相同年龄所有对象的大小总和大于 Survivor 空间的一半,大于或等于此年龄的对象就可以直接进入老年代。
  4. 分配担保机制
    1. 检查老年代最大可用连续空间与新生代所有对象的总空间 –. yes –> MinorGc
    2. HandlePromotionFailure 是否允许担保失败 –> yes –> 检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小 –> MinorGC
2. Minor GC ,Major GC , Full GC 触发条件

堆内存空间: Eden、Survivor 和 Tenured/Old 空间

Minor GC 触发条件:当 Eden 区满时,触发 Minor GC。
Major GC 触发条件:
Full GC 触发条件:
(1)调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
(5)由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

3. 垃圾收集器

1. CMS (Concurrent Mark Sweep)

4 个步骤:

  • 初始标记:标记 GC ROOTS 可以直接关联的对象
  • 并发标记:GC TRACING
  • 重新标记:修正并发标记期间,用户程序继续动作而导致的标记产生变动的那一部分对象的标记记录
  • 并发清除

3 个缺点:

  • 对 CPU 资源非常敏感
  • 无法处理浮动垃圾(并发清理阶段,用户线程仍旧在运行,因此一直在产生垃圾,而无法在当次收集中处理掉它们)
  • 产生大量的空间碎片
2. G1 (Garbage-First)

4 个特点:

  • 并行与并发: 使用多个 CPU 或 CPU 核心来缩短 Stop-The-World 停顿的时间
  • 分代收集
  • 空间整合: 基于标记-整理算法
  • 可预测的停顿: 可以建立 可预测的停顿时间模型 ,让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不超过 N 毫秒。

4 个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收 : 首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。

G1 的 GC 模式

Young GC:选定所有年轻代里的 Region。通过控制年轻代的 region 个数,即年轻代内存大小,来控制 young GC 的时间开销。
Mixed GC:选定所有年轻代里的 Region,外加根据 global concurrent marking 统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收益高的老年代 Region。
注意: Mixed GC 不是 full GC,它只能回收部分老年代的 Region,如果 mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会使用 serial old GC(full GC)来收集整个 GC heap。

global concurrent marking :类似 CMS,为 Mixed GC 提供标记服务。
四个过程:

  • 初始标记(initial mark,STW)。它标记了从 GC Root 开始直接可达的对象。
  • 并发标记(Concurrent Marking)。这个阶段从 GC Root 开始对 heap 中的对象标记,标记线程与应用程序线程并行执行,并且收集各个 Region 的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup)。清除空 Region(没有存活对象的),加入到 free list。

G1 中的几个重要概念 –

一、Region

传统的 GC 收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8 去除了永久代,引入了元空间 Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。

如下图所示:
!

而 G1 的各代存储地址是不连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个 Region 占有一块连续的虚拟内存地址。如下图所示:
!

在上图中,我们注意到还有一些 Region 标明了 H,它代表 Humongous,这表示这些 Region 存储的是巨大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。H-obj 有如下几个特征:

  • H-obj 直接分配到了 old gen (老年代),防止了反复拷贝移动。
  • H-obj 在 global concurrent marking 阶段的 cleanupfull GC 阶段回收。
  • 在分配 H-obj 之前先检查是否超过 initiating heap occupancy percent 和 the marking threshold, 如果超过的话,就启动 global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
    为了减少连续 H-objs 分配对 GC 的影响,需要把大对象变为普通的对象,建议增大 Region size。

二、SATB

全称是 Snapshot-At-The-Beginning,由字面理解,是 GC 开始时活着的对象的一个快照。它是通过 Root Tracing 得到的, 作用是 维持并发 GC 的正确性。

那么它是怎么维持并发 GC 的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它的 field 还没有被标记或标记完。
  • 黑:对象被标记了,且它的所有 field 也被标记完了。

由于并发阶段的存在, Mutator(更改器和) Garbage Collector 线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:

  • Mutator 赋予一个黑对象该白对象的引用。
  • Mutator 删除了所有从灰对象到该白对象的直接或者间接引用。

对于第一个条件,在并发标记阶段,如果该白对象是 new 出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region 中有两个 top-at-mark-start(TAMS)指针,分别为 prevTAMS 和 nextTAMS。在 TAMS 以上的对象是新分配的,这是一种隐式的标记。
对于在 GC 时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以 SATB 破坏了第二个条件。
也就是说,一个对象的引用被替换时,可以通过 write barrier 将旧引用记录下来。 (并没有看懂在说什么)

SATB 也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过 GC,这就是 float garbage。因为 SATB 的做法精度比较低,所以造成的 float garbage 也会比较多。


三、RSet

全称是 Remembered Set,是辅助 GC 过程的一种结构,典型的空间换时间工具,和 Card Table 有些类似。
还有一种数据结构也是辅助 GC 的:Collection Set(CSet),它记录了 GC 要收集的 Region 集合 ,集合里的 Region 可以是任意年代的。
在 GC 的时候,对于 oldyoung 和 oldold 的跨代对象引用,只要扫描对应的 CSet 中的 RSet 即可。

Rset : 属于 points-into 结构(谁引用了我的对象)
Card Table : 则是一种 points-out(我引用了谁的对象)的结构
G1 的 RSet 是在 Card Table 的基础上实现的:每个 Region 会记录下别的 Region 有指向自己的指针,并标记这些指针分别在哪些 Card 的范围内。
这个 RSet 其实是一个 Hash Table, Key – 别的 Region 的 起始地址Value 是一个集合 – 里面的元素是 Card Table 的 Index
!

这里解释一下 :
上图有三个 Region 。红色代表 Rset ,灰色大方框代表 Card Table。
Region2 的 Rset2 中有两个 Region 的起始地址,分别指向 Region1 , Region3。 – 代表 Region1 与 Region3 引用了我的对象。
Region1 的 Card Table 位置上,存在一个对 Region2 的引用。 – 代表 Region1 引用了 Region2 的对象。
Region3 同理。

作用:
在做 YGC(Minor GC)的时候,只需要选定 young generation region 的 RSet 作为根集,这些 RSet 记录了 oldyoung 的跨代引用,避免了扫描整个 old generation。
而 mixed gc 的时候,old generation 中记录了 oldold 的 RSet,youngold 的引用由扫描全部 young generation region 得到,这样也不用扫描全部 old generation region。所以 RSet 的引入大大减少了 GC 的工作量。


四、Pause Prediction Model

G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
G1 GC 是一个响应时间优先的 GC 算法,它与 CMS 最大的不同是,用户可以设定整个 GC 过程的 期望停顿时间 ,参数 ‘-XX:MaxGCPauseMillis’ 指定一个 G1 收集过程目标停顿时间,默认值200ms。
G1 通过这个模型统计计算出来的历史数据来预测本次收集需要选择的 Region 数量,从而尽量满足用户设定的目标停顿时间。
停顿预测模型是 以衰减标准偏差为理论基础 实现的。
这里就不详细介绍了,有兴趣的,可以看

4.2 Java 内存

  1. 程序计数器
  2. 虚拟机栈 : 局部变量表、操作数栈、动态链接、方法出口
  3. 本地方法栈 : native 方法
  4. 堆 : 所有的对象实例以及数组
  5. 方法区 : 已被加载的类信息、常量、静态变量、即时编译器编译后的代码
  6. 运行时常量池 : 编译期生成的各种字面量和符号引用
  7. 直接内存 : NIO 类引入了一种基于通道(channel) 与缓冲区(buffer) 的 I/O 方式,使用 Native 函数库直接分配堆外内存,通过存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。

1. Java 对象的内存布局

  1. 对象头 : 哈希码(2bit)-分代年龄(4)、 轻量级锁定(标志位 00) 、重量级锁定、GC 标记、 可偏向(标志位 01)补充 : 类型指针、数组长度
  2. 实例数据 :
  3. 对齐填充

2. OOM 异常

  1. 堆溢出: 不断创建对象,并且存在可达路径,不被清除。那么对象在达到最大堆容量限制后就会产生内存溢出
    通过内存映像分析工具 (Eclipse Memory Analyzer) 对 Dump 出来的堆转存储快照进行分析。判断是内存泄漏还是内存溢出。
  2. 虚拟机栈与本地方法栈溢出:
    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverFlowError 异常。
    如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
  3. 方法区
    方法区存放 Class 的相关信息。如果存在大量的类填满方法区。则会产生溢出。
    通过动态代理或通过 CGLIB 动态生成大量的类,以及大量 JSP 与动态 JSP 文件的应用。

3. OOM 异常的解决

一. 可通过命令定期抓取 heap dump 或者启动参数 OOM 时自动抓取 heap dump 文件。
二. 通过对比多个 heap dump,以及 heap dump 的内容,分析代码找出内存占用最多的地方。
三. 分析占用的内存对象,是否是因为错误导致的内存未及时释放,或者数据过多导致的内存溢出。

4.3 类加载器

1. 类加载过程

  • 加载 :可以通过自定义类加载器参与
  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  • 验证
  1. 文件格式验证
  2. 元数据验证 : 语义校验
  3. 字节码验证 :逻辑校验
  4. 符号引用验证 :发生在解析阶段中,将符号引用转化为直接引用
  • 准备 : 为类变量分配内存并设置类变量初始值的阶段
  • 解析 : 将符号引用替换为直接引用的过程。
  • 初始化 : () 类构造器 : 将类中的赋值语句与静态代码块合并而成 – () 实例构造器

2. 双亲委派模型

启动(Bootstrap)类加载器:采用 C++ 实现,它负责将 <Java.Runtime.Home>/lib 下面的核心类库或-Xbootclasspath 选项指定的 jar 包加载到内存中。由于启动类加载器到本地代码的实现,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替.

扩展(Extension)类加载器:扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java.Runtime.Home >/lib/ext 或者由系统变量-Djava.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径 java -classpath 或-Djava.class.path 变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

工作过程:
如果一个类加载器收到了类的加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。直到顶层的启动类加载器中,当父加载器反馈自己无法完成这个加载请求时,子加载器会尝试自己去加载。

3. 线程上下文类加载器

方便 JNDI 服务:SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
解决方法:Java 应用的线程的上下文类加载器 默认 就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
Java 默认的线程上下文类加载器是系统类加载器(AppClassLoader)。以下代码摘自 sun.misc.Launch 的无参构造函数 Launch()。
可以通过 java.lang.Thread 类的 setContextClassLoader() 设置。

4. OSGI(open service gataway initiative)

方便执部署的实现。可以在不重启服务器的情况下,对其中的逻辑代码进行更新。
由父类加载器与 Bundle 组成 , 每个 Bundle 的功能都是发布 export 与依赖 import。从而形成复杂的网状结构
原理:
OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。
当它需要加载 Java 核心库的类时(以 java 开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。
当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。

5. Tomcat

在双亲委派模型的基础上加入了 Common 类加载器,Catalina 类加载器,Shared 类加载器,WebApp 类加载器,Jsp 类加载器
Common 类加载器, /common 目录被 Tomcat 与所以 Web 应用程序共同使用
Catalina 类加载器, /server 目录中,被 Tomcat 使用
Shared 类加载器, /shared 目录中,被所有 Web 应用程序共同使用
WebApp 类加载器,Jsp 类加载器, /WebApp/WEB-INF 目录中,只能被此 Web 应用程序使用。

4.4 解释器与编译器

1. 编译模式

  1. Mixed Mode – 混合模式
    默认为混合模式,解释器与编译器搭配使用。
  2. Interpreted Mode – 解释模式
    使用 “-Xint” 参数。只使用解释。
  3. Compiled Mode – 编译模式
    使用 “-Xcomp” 参数。优先采用编译,当编译无法进行时,使用解释。

-version 命令,可以输出显示这三种模式

2. 分层编译(Tiered Compilation)

JDK1.7 中的 Server 模式虚拟机中被作为默认编译策略。

  1. 0 层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第一层编译
  2. 1 层,也叫 C1 编译(下文有解释),将字节码编译为本地代码,进行简单、可靠的优化
  3. 2 层,C2 编译。

3. OSR 编译

因为存在多次执行的循环体,所以触发 OSR 编译,以 整个方法 作为编译对象。
发生在方法执行过程中,所以叫( On Stack Replacement ) 方法栈帧还在栈上,方法就被替换了。

4. 编译对象以及触发条件

热点代码 的分类:

  1. 被多次调用的方法
  2. 被多次执行的方法体 – OSR 编译

热点探测(Hot Spot Detection)

  1. 基于采样 : 如果周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,则这个方法就是“热点方法”。
  2. 基于计数器 – HotSpot 虚拟机中采用。
    • 原理: 为每个方法建立计数器,统计方法的次数,如果执行次数超过一定的阈值,就认为它是“热点方法”
    • 计数器分类:
      • 方法调用计数器(Invocation Counter) :
        统计一段时间内 ,方法被调用的次数,如果超过时间限度,则将这个方法的调用计数器减少一半,称为 衰减
      • 回边计数器(Back Edge Counter) : 统计一个方法中循环体被执行的次数 – OSR 编译
        在字节码中遇到控制流向后跳转的指令,称为回边。

5. 优化措施

hotspot 中内嵌有 2 个 JIT 编译器,分别为 Client Compiler,Server Compiler,但大多数情况下我们称之为 C1 编译器和 C2编译器。

5.1 C1 编译器

client compiler,又称 C1 编译器,较为轻量,只做少量性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用。
在寄存器分配策略上,JDK6 以后采用的为 线性扫描寄存器分配算法 ,其他方面的优化,主要有方法内联、去虚拟化、冗余消除等。

A、方法内联

多个方法调用,执行时要经历多次参数传递,返回值传递及跳转等,C1 采用方法内联,把调用到的方法的指令直接植入当前方法中。-XX:+PringInlining 来查看方法内联信息,-XX:MaxInlineSize=35 控制编译后文件大小。
B、去虚拟化
是指在装载 class 文件后,进行类层次的分析,如果发现类中的方法只提供一个实现类,那么对于调用了此方法的代码,也可以进行方法内联,从而提升执行的性能。
C、冗余消除
在编译时根据运行时状况进行代码折叠或消除。

5.2 C2 编译器

Server compiler,称为 C2 编译器,较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用。和 C1 的不同主要在于寄存器分配策略及优化范围.
寄存器分配策略上 C2 采用的为传统的 图着色寄存器分配算法 ,由于 C2 会收集程序运行信息,因此其优化范围更多在于全局优化,不仅仅是一个方块的优化。
收集的信息主要有: 分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常等

逃逸分析(Escape Analysis) 是 C2 进行很多优化的基础,它根据运行状态来判断方法中的变量是否会被外部读取,如不会则认为此变量是不会逃逸的,那么在编译时会做标量替换、栈上分配和同步消除等优化。
如果证明一个对象不会逃逸到 方法或线程 之外,则:

- 栈上分配(Stack Allocation) :确定不会逃逸到 **方法外** ,让这个对象在栈上分配内存,对象占用的内存空间可以随栈帧的出栈而销毁。
- 同步消除(Synchronization Elimination) :确定不会逃逸到 **线程外** ,则无法被其他线程访问,所以可以取消同步措施。
- 标量替换(Scalar Repalcement) : 
    标量(Scalar)指一个数据无法再分解成更小的数据来表示 -- Java 中的原始数据类型
    聚合量(Aggregate)指一个数据可以继续分解 -- Java 中的对象
     **原理:** 直接创建若干个可以被方法使用的成员变量来替代。

5.3 其他措施(注: 不知是 C1 还是 C2)
  1. 语言无关的经典优化技术 – 公共子表达式消除(Common Subexpression Elimination)

    如果一个表达式 E 已经计算过,并且从先前的计算到现在值未曾改变,那么如果 E 再次出现,则可以直接使用之前的表达式结果,代替 E 。

  2. 语言相关的经典优化技术 – 数组边界检查消除(Array Bounds Checking Elimination)
    这个不是很了解,做一个重点。。。以后整理

4. 零散知识点

1. 静态多分派与动态单分派

静态分派 : 依靠静态类型定位方法。
编译阶段:Human man = new Man(); // 静态类型为 Human
运行阶段:man.sayHello() // 动态类型为 Man

重载的优先级
sayHello(char arg);
char int long float double // 不可转化为 byte short ,因为 char 转化是不安全的。
-. Character Serializable/Comparable Object char…(变长参数)

宗量:方法的接收者与方法的参数统称为宗量
单分派根据一个宗量对目标方法进行选择
多分派根据多个宗量对目标方法进行选择

public class QQ{};  
public class _360{};  
public static class Father {  
    public void hardChoice(QQ arg);  
    public void hardChoice(_360 arg);  
}  
public static class Son extends Father{  
    public void hardChoice(QQ arg);  
    public void hardChoice(_360 arg);  
}
 
Father father = new Father();  
Father son = new Son();
 
// 静态多分派 - 编译 : 方法的接收者 Father - Son, 参数 QQ - _360  
father.hardChoice(_360);  
// 动态多分派 - 运行 : 已经确定 参数为 QQ ,再判断 实际类型 , son的实际类型为 Son 。  
son.hardChoice(QQ);

参考

垃圾回收基础