1. 内存的分配

  1. 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值可以通过参数`-XX:MaxTenuringThreshold`来设置,HotSpot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。<br /> 经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From""To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为ToSurvivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

image.png

1.0.查看堆内存分配

image.png
image.png
可以看到默认的内存空间如下:

区域 内存大小
新生代(YoungGen) eden 65024k(63.5M)
from(servivor) 10752k(10.5M)
to(servivor) 10752k(10.5M)
老年代(ParOldGen) 173568(169.5M)
元空间(Metaspace) 16G(我内存大小)

1.1. 堆内存常见分配策略

GC - 图4image.png

1.1.1. 对象优先在eden区分配

  • 目前主流垃圾收集器都会采用分代回收算法,需要将内存分为新生代和老年代;
  • 多数情况下对象在新生代eden区分配,当eden区没有足够空间进行分配,会发起一次Minor GC;

Minor GC 和 Full GC

  • Minor GC ——新生代GC :指发生在新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
  • Major GC/Full GC——老年代GC :指发生在老年代的GC,出现Major GC经常会伴随至少一次Minor GC,Major GC 的速度一般比Minor GC 的慢10倍以上。

1.1.2. 大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
参数 -XX:PretenureSize Threshold 表示超过这个值的时候,对象直接在old区分配内存。
目的:避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

但是
从上图中可以看出,eden空间被占据了96%,大对象并没有直接进入老年代,这和垃圾收集器的选择有关:
image.png
image.png
UseParallelGC 是虚拟机运行在Server模式下的默认值,使用Parallel Scavenge+Serial Old(PS MarkSweep)的收集器组合进行内存回收。
int[] 对象没有直接进入老年代的原因是 pretenureSizeThreshold参数只对SerialParNew两个收集器有效,Parallel Scavenge收集器不认识这个参数。

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

采用了分代收集的思想来管理内存,虚拟机会给每个对象一个对象年龄计数器。

  1. 对象在eden出生并经历了一次Minor GC 后存活,移动到Survivor,年龄设置为1;
  2. 对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定的程度(默认15岁,不同垃圾收集器不同),就会晋升到老年代;(遍历所有对象,如果某个年龄大小超过了survivor区的一半,则选取该值和设定阈值中更下的一个作为晋升年龄阈值)

2. 对象的死亡

垃圾回收前第一步就是先判断哪些对象已经死亡。
GC - 图8

2.1. 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题,两个对象互相引用导致计数器不为0。

2.2. 可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

2.3. 引用的类型

通过可达性分析判断对象的引用链是否可达,判断对象的存活都与引用有关。

  1. 强引用(大部分引用都是强引用),JVM不会随意回收具有强引用的对象。
  2. 软引用,内存空间够就不回收,内存空间不足就回收,用来实现内存敏感的高速缓存。可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。
  3. 弱引用,弱引用对象比软引用对象具有更短暂的生命周期,内存空间够不够都会回收它。
  4. 虚引用,形同虚设,用于跟踪对象被垃圾回收的活动。 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

2.4. 不可达对象的死亡

宣告一个对象的死亡至少需要经过两次标记过程;
标记过程:

  1. 可达性分析法中不可达对象被第一次标记并根据对象是否有必要执行finalize方法进行一次筛选:
  2. 当对象没有覆盖finalize方法或finalize方法已经虚拟机调用过,不会执行finalize方法;
  3. 被判定需要执行finalize的对象会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

2.5. 废弃常量

废弃常量:

  1. 常量池中存在字符串”abc”但是没有任何引用指向”abc”,那么”abc”就是废弃常量;
  2. 运行时常量池中基本数据类型何时回收?

运行时常量池主要回收的是废弃常量。

2.6. 如何判断一个类是无用类

方法区主要回收的是无用类。
同时满足下面三个条件的类会被判定为”无用的类”,可以进行回收:

  1. 该类的所有实例对象都已经被回收,Java堆中不存在该类的任何实例对象;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用。这里不是很懂

3. 垃圾收集算法

GC - 图9

3.1. 标记-清除算法

该算法分为“标记”和“清除”阶段:

  • 首先比较出所有需要回收的对象,进行标记;
  • 在标记完成后统一回收掉所有被标记的对象。

它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
产生的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

3.2. 复制算法

为了解决效率问题,“复制”收集算法出现了。

  • 它可以将内存分为大小相同的两块,每次使用其中的一块;
  • 当这一块的内存使用完后,就将还存活的对象复制到另一块去;
  • 然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

3.3. 标记-整理算法

根据老年代的特点提出的一种标记算法。

  • 标记过程仍然与“标记-清除”算法一样;
  • 标记后,让所有存活的对象向一端移动;
  • 然后直接清理掉端边界以外的内存。

3.4. 分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块;
一般将Java堆分为新生代和老年代,这样可以根据各个年代的特点选择合适的垃圾收集算法。

  • 新生代中:每次都有大量对象死去,所以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  • 老年代中:对象存活率较高,没有额外的空间进行分配担保,所以选择“标记-清除”或“标记-整理”算法进行垃圾收集

4. 垃圾收集器

GC - 图10收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。

4.1. Serial收集器

串行收集器,单线程进行垃圾收集,收集时“Stop The World”;
新生代采用复制算法,老年代采用标记整理算法;
优点:和其他收集器的单线程相比,有较高的单线程收集效率。适合Client模式。
GC - 图11

4.2. ParNew收集器

Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法
用户线程与垃圾收集线程同时执行(可能并行可能交替执行),用户程序运行,垃圾收集器运行在另一个CPU上。
GC - 图12

4.3. Parallel Scavenge 收集器

复制算法的多线程收集器。高效利用CPU,提高吞吐量(运行用户代码时间/cpu总消耗时间)
新生代采用复制算法,老年代采用标记-整理算法。
-XX:+UseParallelGC 使用Parallel收集器 + 老年代串行
-XX:+UseParallelOldGC 使用Parallel收集器 + 老年代并行
GC - 图13

4.4. CMS收集器

CMS(Current Mark Sweep,同步标记清除)收集器,是基于“标记-清除”算法实现的
以获取最短回收停顿时间为目标,注重用户体验;
是HotSpot虚拟机第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

运行过程

  1. 初始标记:暂停其他线程,并记录下直接与root相连的对象,速度很快
  2. 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  3. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  4. 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

GC - 图14

优点:并发收集、低停顿
缺点

  1. 对cpu敏感;
  2. 无法处理浮动垃圾;
  3. 使用“标记-清除算法”时会产生大量垃圾。

4.5. G1收集器

G1(Garbage-First),是面向服务器的垃圾收集器,用于配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还能具备高吞吐量性能特征。
不需要其他收集器配合就能独立管理整个GC堆。
采用 局部复制算法,整体 标记整理算法实现
G1收集器运行大致分为如下几个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

5. 面试题

5.0. 堆内存的常用分配策略有哪些?

有三种常用分配策略:

  1. 新生对象首先在Eden区分配内存;
  2. 大对象直接在老年代分配内存;
  3. 长期存活的对象进入老年代。

    5.1. 如何判断对象是否死亡(两种方法)。

    判断对象死亡有两种方法:引用计数法和可达性分析法;

  4. 当使用引用计数法时:

原理:在对象中添加一个引用计数器,引用则加一,引用失效则减一,引用为0,则该对象是不可用的;
缺点:当对象直接相互引用时就会失效。

  1. 当使用可达性分析法时:

原理:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

5.2. 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。

强引用:即使发生OOM也不会被垃圾回收的对象;
软引用:如果内存空间足够就不会回收,如果内存空间不足,就会回收软引用的对象;
弱引用:GC只要发现弱引用对象,不管内存是否足够都会进行回收;
虚引用:主要用来跟踪对象被垃圾回收的情况,不决定对象的生命周期,任何时候都能被垃圾回收;
虚引用与软引用和弱引用的区别:虚引用必须和引用队列(ReferenceQueue)一起使用,虚引用不决定对象的生命周期。
软引用的好处:加速JVM的回收速度,维护系统的运行安全,防止内存溢出(OOM, OutOfMemory)等问题的产生。

5.3. 如何判断一个常量是废弃常量

当没有任何对象引用该常量时,该常量为废弃常量。常量池中字符串”abc”的例子。

5.4. 如何判断一个类是无用的类

有三个判定条件需要同时满足:

  1. 该类的所有实例都已经被回收;
  2. 该类的ClassLoad已经被回收;
  3. 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法。

5.5. 垃圾收集有哪些算法,各自的特点?

垃圾收集有4个算法,分别是:

  1. 标记-清除算法:该算法分为”标记”和”清除”两个阶段,首先比较所有需要回收的对象,在标记完后统一回收被标记对象。它是最基础的垃圾收集算法,后续算法都是对其不足进行改进。该算法会带来两个明显的问题:效率问题和空间问题(产生大量的空间碎片)。
  2. 复制算法:将内存分为大小相同的两块,每次使用其中的一块。当这一块内存使用完后,将还存活的对象复制到另一块去,然后把使用的空间一次性清理掉。这样每次的内存回收都是对内存区域的一半进行回收。
  3. 标记-整理算法:根据老年代提出的一种算法,标记过程和标记-清除算法相同,后续会让所有存活对象向一段移动,然后清理掉端边界以外的内存。(我理解的是,这个算法效率同样不高)
  4. 分代回收算法:根据对象的存活周期不同,将内存分为几块,根据对象存活周期特点,选择合适的垃圾收集算法。Java虚拟机将内存空间分为新生代和老年代。

5.6. HotSpot 为什么要分为新生代和老年代?

将内存空间根据对象的存活周期分区,产生了新生代和老年代。新生代的对象存活率低,老年代的对象存活率高,可以根据其存活率特征对不同的内存区域选择适合的垃圾回收算法。

5.7. 常见的垃圾回收器有哪些?

Serial收集器:单线程串行的收集器;
PalNew收集器:在Serial收集器的基础上增加了多线程功能;
Parallel Scavenge收集器:类似于PalNew收集器,但吞吐量更高,即更高效的利用CPU;
CMS收集器(Concurrent Mark Sweep):是HotSpot第一款真正意义上的并发收集器,以获取最短回收停顿时间为目标,基本实现了垃圾收集线程与用户线程同时工作;
G1收集器:面向服务器的垃圾收集器,针对配置了多处理器大内存的机器,以及高概率满足GC停顿时间要求的同时还能具备高吞吐量性能特征。

5.8. 介绍一下 CMS,G1 收集器。

1.CMS收集器:Concurrent Mark Sweep

  • 是一种基于标记-清除算法的并发式收集器,以获取最短回收停顿时间为目标,适合在注重用户体验的应用上使用;
  • 运作过程分为4个步骤:初始标记->并发标记->重新标记->并发清除;
  • 优点:并发收集、低停顿;
  • 缺点:对CPU资源敏感、无法处理浮动垃圾、继承“标记-清除”算法的缺点(效率低,产生空间碎片);

2.G1收集器:Garbage-First
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征;
运作过程分为4个步骤:初始标记->并发标记->最终标记->筛选回收;
特点:

  1. 并行与并发(利用多核,并发执行)
  2. 分代收集(G1不需要其他收集器配合就能独立管理整个GC堆,并且保留了分代概念)
  3. 空间整合(局部复制算法,整体标记整理算法)
  4. 可预测的停顿(G1较于CMS的优势,能让使用者指定停顿在一个长度为M毫秒的时间片段内)

    5.9. Minor GC 和 Full GC 有什么不同呢?

    Minor GC :指发生在新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般较快;
    Full GC:指发生在老年代的垃圾收集动作,出现的Full GC经常会伴随至少一次Minor GC,Full GC的速度会比Minor GC 慢很多。

5.10 如何减少Full GC

  1. 老年代空间增大
  2. System.gc()方法通常会出发Full GC,通过-XX:+DisableExplicitGC来禁止调用System.gc()

参考文献:https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md#3-%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95