一些名词:
Minor GC (新生代GC)
Major GC (老年代GC)
Full GC(总体GC)
Mixed GC(G1垃圾收集器特有gc)
常见问题
如何判断对象是否死亡(两种方法)。
- 引用计数
- 可达性分析
简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了
SoftReference
类来实现软引用。 - 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
WeakReference
类 - 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了
PhantomReference
类来实现虚引用。
方法区垃圾回收介绍
方法区的 GC 主要回收两部分内容:常量池中废弃的常量和不再使用的类型
如何判断一个常量是废弃常量
- 运行时常量池主要回收的是废弃的常量,只要常量池中的常量没有任何一个地方引用,这个常量就可以别回收
- 例如,假设在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
如何判断一个类是无用的类
- 方法区主要回收的是无用的类
- 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
垃圾收集有哪些算法,各自的特点?
- 标记清除(Mark Sweep)
- 执行效率不稳定
- 容易产生内存碎片
- 标记复制(Mark-Copy)
更加适合新生代- 不会有内存碎片
- 但是空间浪费严重,可用空间只有原来的一半
- 标记整理(Mark Compact)
- 移动对象是一个比较重的操作,而且移动对象必须全程暂停用户应用程序才能进行
HotSpot 为什么要分为新生代和老年代?
- 因为根据对象的生命周期采用不同分代,我们可以根据每个代的特点选择合适的垃圾收集算法。
- 比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
记忆集与卡表是什么?
想象这样一个场景:现在要进行一次只局限于新生代区域的收集(Minor GC),但是新生代内的对象是很有可能被老年代的某个对象所引用着的。
那么 Minor GC 时需要同时扫描老年代?那样代价太大。
此时就需要对分代收集理论加一条假说:
- 跨代引用相对于同代引用来说仅占极少数。
记忆集
有了这个假说,我们可以设计新的解决方案:不扫描整个老年代,只需要在新生代建立一个全局数据结构,他把老年代内存划分为小块,标识出老年代的哪一块内存会存在跨代引用,这个数据结构就是记忆集(Remember Set)。此后如果有 Minor GC ,会将记忆集对应的内存区域加入 GC Roots 一起扫描。
卡表
卡表(Card Table)实际上就是记忆集的一种实现方式。
记忆集有很多精度,例如:
- 字长精度
- 对象精度
- 卡精度
卡表就是第三种“卡精度”的实现。
对于HotSpot虚拟机来说,卡表的实现方式就是一个字节数组。
CARD_TABLE [this address >> 9] = 0;
该字节数组(Card Table)的每一个元素都对应着其标识内存区域中一块特定大小的内存块,这个内存块叫做“卡页”(Card Page)。可以看出 HotSpot 中使用的卡页默认是 2 9 = 512 2 ^ 9 = 512 29=512 字节(地址右移9位,相当于用地址除以512)。
只要一个卡页内的对象存在一个或者多个跨代对象指针,就将该位置的卡表数组元素修改为1,表示这个位置为脏,没有则为0。
在GC的时候,就直接把值为1对应的卡页对象指针加入GC Roots一起扫描即可。
写屏障
写屏障可以看做是虚拟机层面对“引用类型字段赋值”这个动作的AOP切面。实际就是在其他分代引用当前对象时,更新卡表脏的状态。
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里完成卡表状态更新
post_write_barrier(field, new_value);
}
这意味着只要更新了对象引用,就会产生写屏障开销,但是相比于扫描整个老年代的代价还是很低的。
伪共享问题
假设CPU缓存行大小是64字节,那么一个卡表元素占1字节,64个卡表元素就共享了同一个缓存行,而对应的卡页内存大小就是 64 * 512 = 32KB
的大小。
那么多线程更新时,如果多个更新同时处于这32KB内存内,就会导致更新卡表时写入同一个缓存行,进而影响性能。
如何解决呢?
JDK7之后新增了一个参数 -XX:+UseCondCardMark
,代表是否开启卡表更新的判断,没有被标记过才标记为脏。更新逻辑类似于:
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
也就是说,只有当卡表元素未被标记过时才会将其标记为变脏。
三色标记是什么?
根节点枚举经过了各种优化下(包含但不限于上述的卡表),带来的 STW 停顿已经属于可控范围了。
那么还存在的问题就是从 GC Roots 开始遍历,怎么才能高效地标记对象?
在三色标记法中,把从 GC Roots 开始遍历的对象按照“是否访问过”这个条件标记为以下三种颜色:
白色,在刚开始遍历的时候,所有的对象都是白色的。如果在分析结束的阶段,仍然是白色,则代表不可达
灰色,被垃圾回收器扫描过,但是至少还有一个引用没有被扫描
黑色,被垃圾回收器扫描过,并且这个对象的引用也全部都被扫描过,是安全存活的对象。黑色对象不可能不经过灰色对象而直接指向某个白色对象
可以想象成是一个“传染过程”,黑色代表被传染,蔓延到白色,灰色就是传染边界。
三色标记的问题
CMS 和 G1 的 GC 流程都包含标记:
- 初始标记:标记GC ROOT能关联到的对象,这一步需要STW,但是停顿的时间很短。
- 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个时间会比较长,但是现在是可以和用户线程并发执行的,这个效率的问题就是三色标记关注的问题。
可以看到当并发标记时,一边是收集器在标记颜色,一边是用户线程在修改引用关系,这样就可能出现问题:
- 将原本消亡的对象误标记为存活:只是产生了了一些浮动垃圾,下次再收集即可,可以接受
- 将原本存活的对象误标记为已消亡:这个问题就比较严重了
经过 Wilson 研究表明,当且仅当同时满足两个条件才会发生这种对象消失的问题:
- 插入了一条或者多条黑色到白色对象的引用
- 删除了全部从灰色到白色对象的直接或间接引用
那么要解决这个问题,破坏这2个条件之一即可。方案有:
- 增量更新:破坏第一个条件。也就是说,在标记过程中如果发生了“插入黑色到白色引用”这种情况,就将新插入引用记录,在并发扫描结束后再以黑色对象为根重新扫描。
- 原始快照:破坏第二个条件。把要删除的“灰色到白色”引用记录下来,在并发扫描结束后,以灰色对象为根重新扫描一次。无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照来进行搜索。
如果对应到垃圾回收器的话,CMS使用的是增量更新,而像G1则是使用原始快照。
常见的垃圾回收器有哪些?
- Serial / Serial Old
- ParNew
- Parallel / Parallel Old
- CMS
- G1
- ZGC
介绍一下 CMS,G1,ZGC 收集器
-
CMS(Concurrent Mark Sweep)
是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 优点:
- 并发收集、低停顿
- 缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;所谓浮动垃圾,就是在并发标记、并发清理期间产生的垃圾
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
- 优点:
-
G1 (Garbage-First)
是一款面向服务器的垃圾收集器,目标是低停顿以及停顿可预测。被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。
在JDK 9发布之时,G1取代了之前Parallel Scavange + Parallel Old
的组合,成为服务端模式下的默认垃圾收集器。
将连续的Java堆分成了大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或是老年代空间。Region是G1内存单次回收的最小单元,即每次收集到的内存空间都是Region的整数倍。
优势:- 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段
-
ZGC(Z Garbage Collector)
ZGC是一款基于Region布局的、(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为主要目标的一款垃圾收集器
Minor Gc 和 Full GC 有什么不同呢?
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
- 部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。(G1)
- 整堆收集 (Full GC):收集整个 Java 堆和方法区