GC
如何判断对象是否是垃圾?
引用计数法(已被淘汰):
原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。任何时刻计数器为0的对象就是不可能再被使用的。
问题:引用计数法很难解决对象之间相互循环引用的问题,因此主流的Java虚拟机都摒弃了这种算法。
可达性分析算法(主流算法):
原理:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(即不可达)时,则证明此对象是不可用的,即垃圾对象。
GC Roots的通常包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即Native方法)引用的对象。
符号表(symbol dictionary)
字符串表(string table)
对象监视器(object synchronizer)
元数据对象(universe)
被标记为不可达的对象是否一定被回收?
不是,因为要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法。
假如对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()
方法。
这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue
队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。
finalize()
方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
Java 的引用有哪些类型?
Java中的引用类型主要分为四种,每种类型在内存管理和对象回收方面都有不同的特性和用途。
强引用(Strong Reference)
定义:强引用是Java中最常见的引用类型,也是默认使用的引用类型。只要强引用存在,垃圾回收器就不会回收被引用的对象。
示例:
String strongRef = new String("StrongReference")
;特点:当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
软引用(Soft Reference)
定义:软引用是一种相对强引用较弱的引用类型。当系统内存充足时,软引用的对象不会被回收;只有在内存不足时,系统才会回收这些对象的内存。
示例:
SoftReference<String> softRef = new SoftReference<>(new String("SoftReference"));
特点:软引用通常用于实现内存敏感的高速缓存。
弱引用(Weak Reference)
定义:弱引用比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收只被弱引用关联的对象。
示例:
WeakReference<String> weakRef = new WeakReference<>(new String("WeakReference"));
特点:弱引用通常用于构建一些内存敏感的数据结构,如WeakHashMap,以避免内存泄漏问题。
虚引用(Phantom Reference)
定义:虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的,就是能在这个对象被收集器回收时收到一个系统通知。
示例:由于虚引用必须和引用队列(ReferenceQueue)联合使用,所以通常的示例会包含这两者的创建和使用。
特点:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
这四种引用类型在Java内存管理中扮演着不同的角色,合理使用它们有助于编写更高效、更健壮的Java程序。
对象的访问方式有哪些?
Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针,HotSpot 主要使用直接指针进行对象访问。
- 直接指针: reference 地址就是实例数据的地址。通过这个引用就能直接获取到实例数据的地址。除此之外,引用所指向的对内存中的对象数据有两部分组成,一部分就是这个对象实例本身,另一部分是对象类型在方法区中的地址。这种方式最大的好处就是访问对象的速度很快,比通过句柄访问对象节约了一半的寻址时间。
- 句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。
分代收集理论有哪些?
分代收集理论是Java虚拟机(JVM)中垃圾收集器设计的基础,它基于两个主要的分代假说:
弱分代假说(Weak Generational Hypothesis):
内容:绝大多数对象都是朝生夕灭的。
含义:在程序中,大多数对象的生命周期都很短暂,它们在创建后不久就变得不再需要,因此可以被垃圾收集器快速回收。
强分代假说(Strong Generational Hypothesis):
内容:熬过越多次垃圾收集过程的对象就越难以消亡。
含义:如果一个对象能够经历多次垃圾收集而存活下来,那么它更可能是长期存活的对象。这些对象往往是一些系统关键对象或缓存数据,它们不容易被回收。
根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。
基于这两个假说,分代收集理论将Java堆划分为不同的区域,主要是新生代(Young Generation)和老年代(Old Generation):
新生代(Young Generation):
存放存活时间短的对象。
进一步细分为Eden区、From Survivor区和To Survivor区,默认比例为8:1:1。
主要发生的垃圾收集是Minor GC或Young GC,即主要收集新生代的垃圾。
当新生代中的对象经过一定次数的垃圾收集后仍然存活,它们会被晋升到老年代。
老年代(Old Generation):
存放存活时间长的对象。
主要发生的垃圾收集是Major GC或Old GC,即主要收集老年代的垃圾。
由于老年代中的对象生命周期较长,垃圾收集的频率相对较低。
此外,分代收集理论还考虑到了跨代引用问题,即新生代对象可能引用老年代对象的情况。为了处理这种情况,引入了跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。并使用了记忆集(Remembered Set)这一数据结构来记录哪些老年代内存块存在跨代引用,从而在Minor GC时只扫描包含跨代引用的小块内存,提高了垃圾收集的效率。
总结来说,分代收集理论通过区分对象的存活时间和生命周期,将Java堆划分为不同的区域,并应用不同的垃圾收集策略,从而实现了垃圾收集的高效性和针对性。
什么是安全点和安全区?
在Java中,安全点和安全区是与Java虚拟机(JVM)的垃圾回收(GC)机制密切相关的两个重要概念。
安全点(Safepoint)
定义:安全点是程序执行过程中的一些特定位置,在这些位置上,垃圾回收器可以暂停程序的执行,以便进行垃圾回收,用户线程暂停的这行字节码指令是不会导致引用关系的变化。这些位置通常被JVM在编译时插入到字节码中,以确保线程在到达这些位置时处于一个稳定的状态,便于垃圾回收的执行。
特点:
- 位置选择:安全点通常位于方法调用、循环、同步块边界和异常跳转等位置。
- 主动式中断:JVM不会立即中断线程,而是设置一个中断标志。线程在运行过程中会轮询这个标志,一旦发现标志为True,就会在自己最近的安全点上主动中断挂起。
- 优化措施:为了减少应用程序暂停时间,JVM可以通过增量式垃圾收集、并发标记扫描和偏向锁等优化措施来降低单个安全点的暂停时间。
作用:
- 允许JVM在应用程序运行时进行垃圾回收。
- 确保垃圾收集期间应用程序的执行状态不会改变。
安全区(Safe Region)
定义:安全区是指在一段代码片段中,引用关系不会发生变化的区域。在这个区域内的任意地方开始垃圾回收都是安全的。
特点:
- 引用稳定性:在安全区内,线程可以确保引用关系不会发生变化,因此JVM可以在这个区域中的任意位置开始垃圾回收。
- 线程通知:当线程进入安全区时,它会通知JVM自己处于安全状态,允许JVM进行垃圾回收。当线程准备离开安全区时,它会检查JVM是否完成了必要的清理工作。
作用:
- 处理长时间运行且无法及时到达安全点的线程,确保垃圾回收的顺利进行。
- 减少垃圾回收对应用程序执行的影响,提高程序性能。
总结
安全点和安全区是JVM中用于确保垃圾回收顺利进行的两个重要机制。安全点通过在程序执行过程中预设的特定位置暂停线程,确保线程状态稳定;而安全区则允许线程在特定区域内继续执行,同时确保垃圾回收的安全性。这两个机制共同作用,使得JVM能够在不干扰应用程序正常执行的前提下,高效地进行垃圾回收。
什么是记忆集?
记忆集(Remembered Set)是一种在Java虚拟机(JVM)中用于解决对象跨代引用问题的数据结构。
定义
- 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。其主要目的是帮助垃圾收集器确定哪些对象可能仍然被引用,即使它们所在的内存区域已被标记为可回收。
作用
- 解决跨代引用问题:在JVM中,堆内存通常被划分为多个代(如新生代和老年代)。当新生代中的对象引用老年代中的对象时,就发生了跨代引用。记忆集可以帮助垃圾收集器跟踪这些跨代引用,从而避免误删仍被引用的对象。
- 优化垃圾收集:通过记忆集,垃圾收集器可以只扫描那些包含跨代引用的内存区域,而不是整个堆内存。这大大减少了垃圾收集所需的时间和资源。
实现方式
记忆集的实现可以有不同的精度,包括:
- 字长精度:每个记录精确到一个机器字长(如常见的32位或64位),该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:最常用的一种实现方式。它将整个堆划分为一个个指定大小的内存块(称为“卡页”),并维护一个卡表(Card Table)。一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或多个)对象的字段存在着跨代指针,就将对应卡表的数组元素的值标识为1(称为“变脏”),没有则标识为0。
在HotSpot JVM中,卡页的大小默认为512字节。当垃圾收集器进行Minor GC时,它会检查卡表以确定哪些卡页包含跨代引用,并只扫描这些卡页。
总结
- 记忆集是JVM中用于解决跨代引用问题的关键数据结构。通过精确记录哪些内存区域包含跨代引用,它帮助垃圾收集器更加高效地进行内存回收。卡表是实现记忆集的一种常用方式,它将堆内存划分为固定大小的卡页,并通过维护一个卡表来跟踪哪些卡页包含跨代引用。
有哪些 GC算法?
标记-清除算法(Mark-Sweep):
算法思想:分为两个阶段。标记阶段遍历所有可达的对象,并标记它们为存活;清除阶段则遍历堆中的所有对象,清除那些未被标记的对象。
优点:每个活着的对象的引用只需要找到一个即可判断其为存活,算法相对全面。
缺点:算法复杂度较高,执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。
标记-整理算法(Mark-Compact):
算法思想:标记阶段与标记-清除算法相同,但在清除阶段,它并不直接清除死亡对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。
标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。
优点:解决了内存碎片问题。
缺点:同样需要遍历整个内存空间,效率较低。尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。
复制算法(Copying):
算法思想:将可用内存空间分为两个区域,每次只使用其中一个。当这个区域被占满时,就将存活的对象复制到另一个区域,然后清除当前区域的所有内容。
优点:有效地解决了空间碎片化的问题。实现简单、运行高效,解决了内存碎片问题。
缺点:需要开辟两倍的内存空间。对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。
主要用于进行新生代: HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。 HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。
分代收集算法(Generational Collection):
- 算法思想:根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
Java 有哪些GC垃圾收集器?
Java中有多种GC(Garbage Collection)垃圾收集器,这些收集器根据不同的需求和场景进行设计。以下是Java中常见的垃圾收集器及其简要描述:
Serial GC:
串行垃圾收集器。
特点:单线程工作,收集时会暂停所有用户线程(Stop-The-World)。
优点:简单高效,对于单CPU或较小内存的应用,性能表现可能超过并行和并发收集器。
适用场景:客户端模式下的JVM。
Parallel GC(也称为Parallel Scavenge或Parallel Old GC):
并行垃圾收集器。
特点:多线程并行工作,减少垃圾收集时的停顿时间,提高应用程序的吞吐量。
适用场景:需要最大化应用程序吞吐量的场景。
CMS GC(Concurrent Mark-Sweep):
并发式垃圾收集器。
特点:与应用程序线程交替工作,尽可能减少应用程序的停顿时间。
缺点:可能会产生内存碎片,在垃圾对象多时效率较低。
适用场景:需要最小化GC中断或停顿时间的场景。
G1 GC(Garbage-First):
区域化分代式垃圾收集器。
特点:将堆内存划分为多个Region,并跟踪每个Region中的垃圾堆积价值大小,优先回收价值最大的Region。
优点:并行与并发、分代收集、低停顿时间、高吞吐量。
适用场景:配备多核CPU及大容量内存的机器,满足对GC停顿时间有严格要求的场景。
是JDK 9以后的默认垃圾回收器。
ZGC(JDK 11中引入):
适用于大内存和低延迟的场景。
特点:使用着色指针和读屏障实现并发标记,支持几乎无停顿时间的GC。
适用场景:需要极高响应速度的大内存应用。
总结:Java中的垃圾收集器各有特点,适用于不同的场景。在选择垃圾收集器时,需要根据应用程序的需求、硬件环境以及JVM的配置进行综合考虑。
Serial 垃圾收集器?
Serial垃圾收集器是Java虚拟机(JVM)中最基本、历史最悠久的收集器之一。
定义和概述
Serial收集器是JAVA虚拟机中针对新生代的单线程收集器,它使用复制算法进行垃圾收集。
Serial收集器进行垃圾收集时,会暂停所有其他的工作线程(Stop-The-World),直到收集结束。
特点
单线程:Serial收集器使用单个线程进行垃圾收集,这意味着在收集过程中,其他所有线程都必须暂停。
简单高效:由于其单线程的特性,Serial收集器实现简单,且对于限定单个CPU的环境来说,由于没有线程交互的开销,可以获得最高的单线程收集效率。
Stop-The-World:在Serial收集器进行垃圾收集时,所有用户线程都会暂停,直到收集结束。这可能会导致应用程序的短暂停顿。
应用场景
小型应用程序:Serial收集器适用于客户端模式(Client Mode)的小型应用程序,尤其是那些分配给虚拟机管理的内存不大的情况。
单核服务器:在单核服务器上,Serial收集器同样是一个很好的选择,因为它没有线程交互的开销,可以获得较高的收集效率。
参数和配置
通过JVM参数-XX:+UseSerialGC可以选择Serial作为新生代收集器。
-Xms30m -Xmx30m -Xmn10m:这些参数分别设置JVM的初始堆大小、最大堆大小和新生代大小。-Xmn10m指定新生代的空间为10M。
性能考虑
- 在用户的桌面应用场景中,由于分配给虚拟机管理的内存一般来说不会很大(几十M至一两百M),Serial收集器可以在较短时间(几十毫秒至一百多毫秒)内完成垃圾收集。只要这种情况不频繁发生,这种短暂的停顿是可以接受的。
总结
- Serial垃圾收集器以其简单、高效和适用于小型应用程序及单核服务器的特点,在Java虚拟机的发展历程中占据了重要的地位。尽管随着多核CPU的普及和更复杂应用程序的出现,其他更先进的垃圾收集器(如Parallel GC、CMS GC和G1 GC)得到了广泛的应用,但Serial收集器仍然在某些特定场景下发挥着重要作用。
ParNew 垃圾收集器?
ParNew垃圾收集器(ParNew Garbage Collector)是Java虚拟机(JVM)中针对新生代的一种垃圾收集器,它是Serial收集器的多线程版本。
特点
- 多线程并行收集:ParNew收集器采用多线程并行的方式进行垃圾收集,充分利用多核CPU的优势,提高垃圾收集的效率。
- 低延迟和高吞吐量:ParNew收集器的目标是在尽量保证系统响应性的前提下,获得最高的吞吐量(即垃圾收集时间占总时间的比例最小)。
- 新生代收集器:ParNew收集器主要在新生代(Young Generation)中进行垃圾收集,负责收集年轻代的Eden区和Survivor区。
- 复制算法:ParNew收集器采用的是复制算法。
工作流程
初始标记(Initial Mark):
停止应用程序的线程,仅仅标记出在新生代中直接引用的对象。
这个阶段是与应用程序并发执行的。
并发标记(Concurrent Mark):
在此阶段,垃圾收集器会与应用程序并发地执行,标记所有从根对象可达的对象。
这个阶段的并发执行能够减少垃圾收集的停顿时间。
重新标记(Remark):
- 停止应用程序的线程,重新标记在并发标记阶段有可能被修改的对象,以确保标记的准确性。
并发清除(Concurrent Sweep):
- 在此阶段,垃圾收集器会与应用程序并发地执行,清除被标记为垃圾的对象,并释放它们占用的内存空间。
并发重置(Concurrent Reset):
- 在清除完成后,垃圾收集器会与应用程序并发地执行,对垃圾收集器的数据结构进行重置,为下一次垃圾收集做准备。
参数与配置
使用ParNew收集器:通过JVM参数-XX:+UseParNewGC可以指定使用ParNew收集器。
线程数配置:ParNew收集器默认开启的收集线程数与CPU的数量相同。在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
适用场景
- 多核服务器环境:ParNew收集器主要用于多核服务器环境,适合应用程序对于系统吞吐量要求较高的场景。
- 与CMS搭配使用:ParNew收集器通常与CMS(Concurrent Mark-Sweep)收集器搭配使用。CMS收集器负责老年代的垃圾收集,而ParNew收集器负责新生代的垃圾收集。
优点:
- 可进行多线程收集,提高垃圾收集的效率。
- 是除了Serial收集器之外唯一能与CMS收集器配合工作的新生代收集器。
缺点:
- 在单线程或CPU数量较少的情况下,由于存在线程交互的开销,效率可能并不如Serial收集器。
总结
- ParNew垃圾收集器作为Java虚拟机中的一种多线程新生代收集器,充分利用了多核CPU的优势,提高了垃圾收集的效率。它通常与CMS收集器搭配使用,为Java应用程序提供了优秀的性能和响应性。然而,在特定情况下(如单线程或CPU数量较少),其效率可能并不如Serial收集器。
Parallel Scavenge 垃圾收集器?
Parallel Scavenge 新生代收集器,基于复制算法:
初始标记阶段(Initial Mark):
- 暂停所有应用线程,并标记所有的根对象。这个阶段会迅速地完成,一般只需暂停几毫秒。
并发标记阶段(Concurrent Mark):
- 在标记过程中,应用线程会继续运行。垃圾收集器会跟踪并标记可达对象,以及对象间的引用关系,直到所有的可达对象都被标记。
重新标记阶段(Remark):
- 为了处理在并发标记过程中发生的引用关系的变化,会再次暂停应用线程,并标记那些可能被回收的新对象。
并发清除阶段(Concurrent Sweep):
- 清除所有被标记的非活动对象,释放内存空间。
Parallel Scavenge垃圾收集器具有以下特性:
- 运行与应用线程并行:使用了多线程来并行处理垃圾收集,与应用线程一起工作。这样可以明显减少垃圾收集的时间,避免长时间的暂停。
- 追求吞吐量:Parallel Scavenge的目标是在尽可能短的时间内完成垃圾收集,以达到最高的吞吐量。吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值。
- 自适应的调整:具有自适应的调整机制,可以根据当前系统的负载情况、垃圾收集时间等动态地调整各个参数。这样可以使其能够适应不同的工作负载,并在不同的硬件平台上发挥最佳性能。
- 并发收集:并发收集是Parallel Scavenge的一个重要特性。通过并发标记和并发清除阶段,垃圾收集器可以同时运行和应用线程。这使得垃圾收集过程与应用程序的执行可以更好地交替进行,减少了垃圾收集对应用程序性能的影响。
- 低暂停时间:致力于尽可能减少垃圾收集时的应用程序暂停时间,以提供更好的用户体验。
Parallel Scavenge收集器参数:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间。虚拟机将尽力保证内存回收花费的时间不超过设定值。但需要注意的是,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。
- -XX:GCTimeRatio:直接设置吞吐量大小的参数。其值表示垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。比如,如果设置为19,则允许的最大GC时间就占总时间的5%。
- -XX:+UseAdaptiveSizePolicy,当启用时,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整SurvivorRatio、PretenureSizeThreshold等细节参数。
Parallel Scavenge垃圾收集器通过多线程和并发收集的方式,实现了高吞吐量和低暂停时间,适用于那些重视系统处理速度而可以接受一定垃圾收集时间的场景。
ParNew 和 Parallel Scavenge 收集器异同?
共同点
- 目标:两者都是为了提高垃圾收集的效率而设计的,通过并行处理来加速垃圾收集过程。
- 适用场景:都适用于多核处理器的环境,能够充分利用多核优势来提高垃圾收集的效率。
- 新生代收集器:两者都是新生代(Young Generation)的垃圾收集器,主要负责收集年轻代的Eden区和Survivor区。
- STW机制:两者都采用了“Stop-The-World”(STW)机制,在垃圾收集过程中会暂停其他线程的执行。
不同点
并行程度:
ParNew:ParNew是Serial收集器的多线程版本,采用多线程并行的方式进行垃圾收集。
Parallel Scavenge:与ParNew类似,但Parallel Scavenge更强调吞吐量优先,通过自适应调节策略来动态平衡吞吐量和低延迟。
目标优化:
ParNew:目标是尽量保证系统响应性的前提下,获得最高的吞吐量。
Parallel Scavenge:目标是达到一个可控制的吞吐量,被称为“吞吐量优先”的垃圾收集器。
自适应调节策略:
ParNew:不具备自适应调节策略,使用固定的参数配置。
Parallel Scavenge:具有自适应调节策略,能够自动调整年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数,以更好地平衡堆大小、吞吐量和停顿时间。
与其他收集器的配合:
ParNew:通常与CMS(Concurrent Mark-Sweep)收集器搭配使用,CMS负责老年代的垃圾收集。
Parallel Scavenge:在JDK 1.6时,提供了用于回收老年代的Parallel Old收集器,与之配合工作。
参数配置:
ParNew:使用与Serial收集器相同的控制参数,如-XX:SurvivorRatio、-XX:PretenureSizeThreshold等。
Parallel Scavenge:具有额外的参数配置选项,如-XX:ParallelGCThreads(设置垃圾回收线程数)、-XX:MaxGCPauseMillis(设置最大的暂停时间)和-XX:GCTimeRatio(设置垃圾收集时间占总时间的比例)等。
总结来说,ParNew和Parallel Scavenge都是用于提高垃圾收集效率的并行收集器,但Parallel Scavenge更强调吞吐量优先,并具备自适应调节策略来动态平衡性能参数。在选择使用哪种收集器时,需要根据具体的应用场景和性能需求来进行合理的配置和调优。
串行收集器和吞吐量收集器的区别?
串行(Serial)收集器
工作方式:
- 串行收集器是一个单线程收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到垃圾收集完成。
性能特点:
优点:简单而高效,单线程对于单核CPU来说,由于没有切换线程的开销,所以性能高。
缺点:单线程收集,且必须暂停其他所有的工作线程,导致在垃圾收集期间应用程序会暂停响应。
适用场景:
- 主要适用于Client模式下的新生代垃圾收集,或者对于内存资源较少、对停顿时间要求不高的应用场景。
吞吐量(Throughput)收集器
吞吐量收集器,尤其是Parallel Scavenge收集器,与串行收集器在多个方面存在显著差异。
工作方式:
- 吞吐量收集器是并行的,可以同时多个线程一起执行垃圾收集任务。它关注的是吞吐量,即用户代码运行时间与总时间(用户代码运行时间+垃圾收集时间)的比值。
性能特点:
优点:吞吐量大,适用于后台运行多的任务。在多核处理器上,由于可以并行执行垃圾收集,因此能够显著提高垃圾收集的效率。
缺点:虽然停顿时间短,但不适合做交互型的任务,因为高吞吐量可能会牺牲一些响应性。
适用场景:
- 主要适用于Server模式下的新生代垃圾收集,或者对于内存资源充足、对吞吐量有较高要求的应用场景。
策略与配置:
- 吞吐量收集器允许用户通过指定最大暂停时间和垃圾收集时间占总时间的百分比,然后自适应调整JVM的参数来达到配置的目标。这种自适应调整策略使得用户无需关心新生代和老年代的具体设置,只需设定好最大最小堆内存以及上述两个参数,剩下的就交给虚拟机去处理。
总结
串行收集器和吞吐量收集器在工作方式、性能特点和适用场景上存在明显的区别。串行收集器简单高效但可能导致应用程序暂停响应,适用于对资源要求不高的场景;而吞吐量收集器则通过并行执行垃圾收集任务来提高效率,适用于对吞吐量有较高要求的应用场景。在选择使用哪种收集器时,需要根据具体的应用需求和系统环境来做出合理的决策。
Serial Old 垃圾收集器?
Serial Old是JVM(Java虚拟机)中的一个垃圾收集器,主要用于老年代的内Serial Old垃圾收集器是Java虚拟机(JVM)中的一种垃圾收集器,主要用于老年代(Old Generation)的内存管理。
工作方式:
- Serial Old是一个单线程的垃圾收集器,在进行垃圾收集时,它会暂停所有其他的工作线程,直到垃圾收集完成。这种暂停的状态通常被称为“Stop-The-World”(STW)。
内存回收算法:
- Serial Old采用标记-整理(Mark-Compact)算法进行内存回收。这种算法首先标记出所有存活的对象,然后移动所有存活的对象到一端,最后清理掉边界以外的内存空间。
性能特点:
由于是单线程收集,Serial Old在单核CPU上表现较好,因为它避免了线程切换的开销。然而,在多核CPU上,它的性能可能会受到限制,因为它不能充分利用多核的优势。
由于在垃圾收集期间会暂停所有工作线程,因此Serial Old可能会导致应用程序的短暂停顿。这种停顿的时间长度取决于堆的大小和垃圾收集的频率。
适用场景:
Serial Old主要在Client模式下作为默认的老年代垃圾收集器,也可能在某些Server模式下作为CMS收集器的后备方案,在CMS发生Concurrent Mode Failure时使用。
在JDK 1.5之前的版本中,Serial Old经常与Parallel Scavenge收集器搭配使用,或者作为CMS(Concurrent Mark-Sweep)收集器的备选方案。
与其他收集器的关系:
- Serial Old是Serial收集器的老年代版本,两者通常一起使用。Serial收集器负责新生代的垃圾收集,而Serial Old负责老年代的垃圾收集。
配置参数:
- 通过-XX:+UseSerialGC参数可以启用Serial和Serial Old作为新生代和老年代的垃圾收集器。
性能考虑
暂停时间:虽然Serial Old会导致应用程序的暂停,但它的暂停时间通常比并发收集器(如CMS)更短,因为它不需要等待其他线程完成其工作。
吞吐量:在单核CPU上,Serial Old通常比其他多线程收集器具有更高的吞吐量,因为它避免了线程之间的同步和竞争。
使用建议
小型应用程序:对于小型应用程序和客户端应用程序,Serial Old可能是一个合适的选择,特别是当对GC暂停时间要求不太严格时。
配合参数使用:可以使用JVM参数-XX:+UseSerialGC来指定新生代和老年代都使用Serial和Serial Old收集器。
总结来说,Serial Old是一个适用于简单场景和单核CPU环境的单线程垃圾收集器,但在多核CPU环境下可能会受到性能限制。在选择垃圾收集器时,应根据应用程序的具体需求和系统环境来做出决策。
Parallel Old 垃圾收集器?
Parallel Old垃圾收集器主要用于老年代的垃圾回收。
特点:
算法:基于“标记-整理”算法实现并支持多线程并发收集。
多线程:Parallel Old是一个多线程的垃圾收集器,它使用多个线程并行执行垃圾收集任务,以充分利用多核CPU的优势。
并行回收:与Serial Old不同,Parallel Old在进行垃圾收集时不会暂停所有工作线程,而是通过多线程并行的方式进行垃圾收集,以减少应用程序的停顿时间。
吞吐量优先:Parallel Old垃圾收集器主要关注于提高系统的吞吐量,即单位时间内完成的工作量。它通过减少垃圾回收的停顿时间,使系统能够更高效地运行。
STW(Stop-The-World)机制:虽然Parallel Old垃圾收集器采用了多线程并发收集,但在某些阶段(如初始标记和重新标记)仍然需要暂停用户线程,即STW。
减少停顿时间:通过多线程并行收集,Parallel Old能够减少应用程序的停顿时间,提高系统的响应性。
适用场景
Server模式:Parallel Old通常用于Server模式下的老年代垃圾收集,特别是在对吞吐量有较高要求的应用场景中。
与Parallel Scavenge配合使用:Parallel Old通常与Parallel Scavenge收集器配合使用,形成“Parallel Scavenge + Parallel Old”的组合,以提供高效的新生代和老年代垃圾收集。
配置参数
-XX:+UseParallelOldGC:启用Parallel Old垃圾收集器。
-XX:ParallelGCThreads:设置并行垃圾收集器使用的线程数。默认情况下,该值等于CPU核心数。
调优考虑
线程数设置:根据系统的CPU核心数和应用程序的特性,合理设置ParallelGCThreads的值,以达到最佳的垃圾收集效果。
堆内存大小:合理配置JVM的堆内存大小,避免内存溢出或垃圾收集过于频繁。
停顿时间:调整参数MaxGCPauseMillis和GCTimeRatio时,系统的吞吐量和垃圾回收时间可以得到进一步优化。
综上所述,Parallel Old垃圾收集器是一种高效、多线程的Java虚拟机垃圾收集器,适用于对吞吐量要求较高、且对CPU资源敏感的应用场景。
CMS 垃圾收集器?
CMS(Concurrent Mark-Sweep)垃圾收集器是针对老年代设计的并发收集器,它的主要目标是减少垃圾回收时的停顿时间,提高应用程序的响应性能。
CMS垃圾收集器使用“标记-清除”算法进行垃圾回收,其工作过程大致分为以下几个阶段:
初始标记(Initial Mark):
这是一个“Stop-The-World”(STW)过程,主要标记出GC Roots直接引用的对象。此阶段速度很快,对“Stop-The-World”影响不大。
暂停应用程序线程,遍历GC ROOTS直接可达的对象并将其压入标记栈(mark-stack)。标记完之后恢复应用程序线程。
并发标记(Concurrent Mark):
此阶段垃圾收集线程与用户线程并发执行,从GC Roots直接引用对象开始遍历整个对象图,标记存活对象。
由于此阶段用户线程仍然在运行,因此对象的引用关系可能会发生变化,CMS使用了卡表(card table)和写屏障(write barrier)等技术来记录这些变化。
重新标记(Remark):
这是一个STW过程,主要目的是修正在并发标记阶段因用户线程运行而导致的已标记对象状态改变的问题。
暂停用户线程,根据并发标记阶段收集的信息,重新标记在并发标记阶段中遗漏的对象。
并发清理(Concurrent Sweep):
此阶段垃圾收集线程与用户线程并发执行,清理掉在并发标记阶段标记为可回收的对象。
由于清理阶段耗时较长,但它是与用户线程并发执行的,因此不会对应用程序的响应性能产生太大影响。
特点
- 并发性:CMS垃圾收集器的大部分工作都是与用户线程并发执行的,从而减少了垃圾回收时的停顿时间。
- 低延迟:由于CMS的并发性,它适用于对响应时间要求较高的交互式应用,如Web服务器、电商平台等。
- 可能导致内存碎片:由于CMS使用“标记-清除”算法,可能会产生大量的内存碎片。为了解决这个问题,CMS提供了一个参数-XX:+UseCMSCompactAtFullCollection,在Full GC之后进行内存碎片整理。
参数配置
CMS垃圾收集器可以通过以下JVM参数进行配置:
- -XX:+UseConcMarkSweepGC:启用CMS垃圾收集器。
- -XX:+UseCMSCompactAtFullCollection:在Full GC之后进行内存碎片整理。
- -XX:CMSFullGCsBeforeCompaction:指定多少次Full GC之后进行一次内存碎片整理,默认是0,表示每次Full GC后都会进行碎片整理。
- -XX:CMSInitiatingOccupancyFraction:设置老年代使用率的阈值,当老年代的使用率达到这个阈值时,会触发CMS垃圾回收。默认值是92%。
注意事项
- CPU资源消耗:由于CMS的并发性,它可能会消耗较多的CPU资源。因此,在配置CMS时,需要根据应用程序的实际情况和服务器资源进行合理调整。
- 并发模式失败(Concurrent Mode Failure):在CMS进行并发清理时,如果老年代空间不足,会导致并发模式失败,此时JVM会启用“Serial Old”垃圾收集器进行Full GC,这会导致较长时间的停顿。为了避免这种情况,可以通过调整-XX:CMSInitiatingOccupancyFraction参数来提前触发CMS垃圾回收。
- 内存碎片问题:CMS使用“标记-清除”算法可能导致内存碎片问题。虽然可以通过配置参数进行碎片整理,但这也会增加额外的开销。因此,在使用CMS时需要注意内存碎片问题对应用程序性能的影响。
G1 垃圾收集器的特点?
G1垃圾收集器(Garbage-First Garbage Collector)是一款面向服务端应用的垃圾收集器,旨在为大内存、多处理器的机器提供高效的垃圾收集性能。
定义与特点
定义:G1是一款并行的、增量的、分代的垃圾收集器,它试图以很高的概率满足GC停顿时间目标,同时实现高吞吐量且几乎不需要配置。
目标:在延迟和吞吐量之间提供最佳平衡,主要适用于堆大小可达10 GB或更大,且超过50%的Java堆占用实时数据的应用场景。
特点
开创了收集器面向局部收集的设计思路和基于 Region 的内存布局。G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。
G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。
工作原理
堆内存分区:G1将堆内存划分为多个大小相同的分区(Region),每个Region是一个连续的虚拟内存地址。Region是内存分配和内存回收的基本单位。
Region大小:可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方。
Region数量:默认不超过2048个(实际可以超过该值,但不推荐)。
逻辑集合:Eden、Survivor和Permanent是这些分区的逻辑集合,并不相邻。
并行与并发:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。同时,G1也具备与应用程序交替执行的能力,部分工作可以和应用程序同时执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但还是保留了分代的概念。
垃圾优先:G1通过跟踪堆中各个Region的垃圾回收价值,在后台维护一个优先级列表。每次在允许的收集时间内,优先回收价值最大的Region,即垃圾最多的区域。
空间整合:G1将内存划分为Region,内存的回收是以Region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,有助于避免内存碎片。
可预测停顿时间模型:G1除了追求低停顿外,还能建立可预测的停顿时间模型。用户可以通过-XX:MaxGCPauseMillis参数明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
应用场景
- 大内存应用:G1特别适合大内存应用,能够高效地管理大量内存并减少停顿时间。
- 实时数据应用:对于超过50%的Java堆占用实时数据的应用,G1能够提供良好的性能保障。
命令行选项
- 启用G1:使用-XX:+UseG1GC来显式启用G1垃圾收集器。
- 设置最大GC暂停时间:使用-XX:MaxGCPauseMillis为最大GC暂停时间设置一个指标。
总结
G1垃圾收集器是一款高效、灵活的垃圾收集器,适用于大内存、多处理器的应用场景。它通过分区、并行与并发、垃圾优先、空间整合以及可预测停顿时间模型等技术手段,为Java应用提供了良好的垃圾收集性能。
G1 垃圾收集器的工作过程?
G1垃圾收集器的工作过程可以归纳为以下几个主要步骤,
初始标记(Initial Marking):
暂停所有的其他线程,并记录下GC Roots直接能引用的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象,修改TAMS指针的值。
这个阶段需要停顿线程 STW,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记(Concurrent Marking):
从初始标记的对象开始进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
并发标记的时机是在YGC后,只有内存消耗达到一定的阈值后才会触发。在G1中,这个阈值通过参数
**InitiatingHeapOccupancyPercent**
控制(默认值是45,表示的是当已经分配的内存加上本次将分配的内存超过内存总容量的45%时就可以开始并发标记)。这阶段耗时较长,**不需要STW,**可与用户程序并发执行。
多个标记线程进行扫描,每个线程每次只扫描一个分区,从而标记出存活对象。
在标记的时候还会计算存活对象的数量,同时会计算存活对象所占用的内存大小,并计入分区空间。并发标记子阶段会对所有分区的对象进行标记。
扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB(snapshot-at-the-beginning)算法来解决。
最终标记(Final Marking):
对用户线程做一个短暂的暂停(STW),用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。找出所有未被访问的存活对象,同时完成存活内存数据计算。
要结束标记过程,需要满足3个条件:
- 从根(survivor)出发并发标记子阶段已经标记出所有的存活对象。
- 标记栈是空的。
- 所有的引用变更对象都被处理了。这里的引用变更对象包括新增空间分配的对象和引用变更对象,新增空间所有对象被认为都是活跃的(即使对象已经“死亡”也没有关系,在这种情况下只是增加了一些浮动垃圾),引用变更处理的对象通过一个队列记录,在该子阶段会处理这个队列中所有的对象。前两个条件是很容易满足的,但是满足最后一个条件是很困难的。如果不引入一个STW的再标记过程,那么应用会不断地更新引用,也就是说,会不断地产生新的引用变更,因而永远无法达成完成标记的条件。
筛选回收(Live Data Counting and Evacuation):
对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。该子阶段也需要一个STW的时间段,也是并行执行的。
清理子阶段主要执行以下操作:
统计存活对象,统计的结果将会用来排序分区,以用于下一次的垃圾回收时分区的选择。
交换标记位图,为下次并发标记做准备。
把空闲分区放到空闲分区列表中。这里的空闲分区指的是全都是垃圾对象的分区,如果分区中还有活跃对象,则不会释放,真正释放的动作发生在混合回收中。
该阶段清理子阶段并不会清理垃圾对象,也不会执行存活对象的复制。也就是说,在极端情况下,该阶段结束之后,空闲分区列表将毫无变化,JVM的内存使用情况也毫无变化。
什么是卡表?
卡表(card table)是Java虚拟机(JVM)中用于垃圾收集的一种数据结构,尤其在某些垃圾收集器(如G1)中起到关键作用。
定义
- 卡表又称为卡片标记(card marking),是一种逻辑上将内存空间分割为若干个固定大小的连续区域的数据结构。这些被分割出来的区域被称为“卡片”(card),而所有卡片的集合则构成了卡表。
原理
- 内存空间分割:卡表将内存空间(如老年代)逻辑上分割为若干个固定大小的连续区域,每个区域称为一个卡片。每个卡片的大小通常介于128~512字节之间,一般使用2的幂字节大小,例如HotSpot JVM使用512字节。
- 标记位:为每个卡片准备一个与其对应的标记位。这个标记位通常由一个字节数组实现,以卡片的编号作为索引。
- 写屏障:当卡片内部发生引用变化时(例如指针写操作),JVM的写屏障(write barrier)机制会将该卡片在卡表中对应的标记位标记为“脏”(dirty)。
作用
- 卡表的主要作用是在垃圾收集过程中提高效率和减少停顿时间。通过只扫描卡表中标记为“脏”的卡片,JVM可以避免扫描整个内存空间,从而减少了垃圾收集的开销。
示例
- 以G1垃圾收集器为例,每个region内部被划分为若干个卡片,这些卡片的集合就构成了该region的卡表。当某个region中的卡片发生引用变化时,写屏障会将其在卡表中对应的标记位标记为“脏”。在垃圾收集过程中,G1只需扫描卡表中标记为“脏”的卡片,就可以确保不会遗漏任何需要回收的对象。
总结
- 卡表是JVM中一种重要的数据结构,它通过将内存空间分割为固定大小的卡片并为其设置标记位的方式,提高了垃圾收集的效率并减少了停顿时间。在G1等垃圾收集器中,卡表发挥着关键作用。
ZGC 垃圾收集器的特点?
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的垃圾收集器。以下是关于ZGC垃圾收集器的详细介绍:
背景和起源:
ZGC最初是在JDK 11中以实验性质引入的,并在JDK 15中被宣布为生产就绪(Production Ready)。
它旨在满足大规模堆内存和高吞吐量应用的需求,特别适用于处理TB级堆内存的情况。
核心技术特点:
基于Region的内存布局:ZGC将整个堆内存划分为多个小区域(Region),每个Region可以动态创建、销毁以及调整容量大小。
不设分代:ZGC不使用传统的分代收集策略,而是将整个堆内存看作一个整体进行垃圾收集。
读屏障和染色指针:ZGC使用读屏障来记录对象引用关系的变化,并通过染色指针技术将对象的引用信息存储在指针本身,从而避免了额外的内存开销。
内存多重映射:ZGC使用内存多重映射技术,将多个不同的虚拟内存地址映射到同一个物理内存地址上,提高了内存管理的灵活性。
执行过程:
并发标记:ZGC在标记阶段采用SATB(Snapshot-At-The-Beginning)算法,通过读屏障记录对象引用关系的变化。
再标记:在并发标记完成后,ZGC会暂停应用线程进行短暂的再标记操作,以处理在并发标记阶段未能处理的对象引用变化。
并发整理:在再标记完成后,ZGC会进入并发整理阶段,将存活对象复制到新的内存位置,并释放不再使用的内存空间。
性能优势:
低延迟:ZGC的垃圾收集停顿时间被控制在亚毫秒级别,对应用性能的影响微乎其微。
可伸缩性:ZGC的设计初衷就是支持大规模堆内存和高吞吐量应用,能够处理数TB级别的堆内存。
确定性:ZGC的垃圾收集时间与堆大小无关,最大停顿时间是可预测和固定的。
配置简单:从使用者的角度来看,ZGC的配置和管理相对简单,只需在启动JVM时添加相应的参数即可启用。
应用场景:
- ZGC垃圾收集器适用于需要处理大量常见对象和哈希表的大型Java应用程序,如电子商务系统、大数据应用程序、人工智能和机器学习系统、游戏和图形应用程序等。
配置和监控:
使用ZGC垃圾收集器非常简单,只需在启动JVM时添加-XX:+UseZGC参数即可。
开发者还可以通过JMX或其他监控工具来观察和调整ZGC的行为,如监控ZGC的性能指标、收集次数、收集时间等。
ZGC是一款具有革命性的垃圾收集器,它通过独特的设计和技术实现了低延迟、可伸缩性和高性能的目标,为处理大规模堆内存和高吞吐量应用提供了有力的支持。
ZGC 详细执行过程?
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的垃圾收集器。它基于动态Region内存布局,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法。以下是ZGC的详细执行过程:
初始阶段(Initial Phase):
- 初始标记(Initial Mark, STW):暂停所有的其他线程,并记录下GC Roots直接能引用的对象。这个过程只会执行一些小的操作,如设置一些标记位和确定全局颜色。在JDK 16之前,该阶段的耗时和GC Roots的数量成正比,但JDK 16之后通过JEP 376引入的新算法显著减少了该阶段的耗时。
并发标记(Concurrent Mark):
- 并发标记是遍历对象图做可达性分析的阶段,它的初始标记和最终标记之间会出现短暂的停顿。在这个阶段,ZGC将遍历整个对象图,并标记所有对象(根据GC周期不同,设置Marked0或Marked1标记)。同时,将上一个GC周期中尚未被重映射的对象(标记仍为Marked1或Marked0)进行重映射。
再标记(Remark):
- 这个阶段需要暂停(STW),主要处理漏标对象,通过SATB(Snapshot At The Beginning)算法解决。它确保了所有在并发标记阶段被遗漏的对象都能被正确标记。
并发转移准备(Concurrent Prepare for Relocate):
- 分析最有回收价值的GC分页(无STW)。在这个阶段,ZGC会根据特定的查询条件统计得出本次收集过程要清理哪些Region,并将这些Region组成重分配集(Relocation Set)。
初始转移(Initial Relocate):
- 转移初始标记的存活对象同时做对象重定位(有STW)。在这个阶段,ZGC会并发地迁移对象,压缩堆中的区域,以释放空间。迁移后的对象的新地址会记录到转发表(Forwarding Table)中,用于后续重映射时获取对象的新的地址。
并发转移(Concurrent Relocate):
- 对转移并发标记的存活对象做转移(无STW)。这个阶段会将重分配集中的存活对象复制到新的Region上,并为每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
ZGC的运作过程中还涉及几个关键技术点:
读屏障(Read Barrier):
涉及对象:并发转移但还没做对象重定位的对象。
触发时机:在两次GC之间业务线程访问这样的对象。
当用户线程尝试加载一个对象时,如果该对象已被移动,读屏障会基于转发表将对象的地址重映射到新的位置。
染色指针(Colored Pointers):ZGC使用染色指针来标记对象的状态,如是否被移动过等。
内存多重映射(Multi-Mapping):ZGC使用内存多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,从而支持染色指针的使用。
ZGC通过动态Region内存布局、染色指针、内存多重映射和读屏障等技术,实现了低延迟的垃圾收集,特别适用于对停顿时间有严格要求的场景。
G1 和 ZGC 有什么异同
G1和ZGC作为JVM中的两种垃圾收集器,各自具有独特的特点和适用场景。
相同点:
- 目标:两者都旨在提供高性能的垃圾收集,满足现代应用对低延迟和高吞吐量的需求。
- 基于Region的内存布局:G1和ZGC都将堆内存划分为多个Region(或称为Page/ZPage),允许以更细粒度的单位进行垃圾收集。
- 并发性:两者都支持并发垃圾收集,以减少应用程序的停顿时间。
不同点:
设计理念:
G1:面向服务端应用,主要针对配备多核CPU及大容量内存的机器。其设计原则是首先收集尽可能多的垃圾,因此G1并不会等内存耗尽或者快耗尽的时候开始收集垃圾,而是内部采用启发式算法,在老年代找出具有高收集收益的分区进行收集。
ZGC:设计目标是提供极致的低延迟,并且支持堆范围为8MB~16TB。ZGC的GC停顿时间被控制在亚毫秒级别,且其停顿时间不会随着堆的大小或者活跃对象的大小而增加。
分代策略:
G1:虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的survivor堆作为复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
ZGC:不设分代,即不再区分年轻代和老年代,而是将整个堆内存看作一个整体进行垃圾收集。
内存管理:
G1:将内存划分为一个个大小相等的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。
ZGC:Region具有动态性(动态创建和销毁以及动态的区域容量大小),根据容量大小分为小、中、大三类容量。使用内存多重映射技术,把同一块儿物理内存映射为Marked0、Marked1和Remapped三个虚拟内存,并通过这三个视图空间的切换,完成并发的垃圾回收。
垃圾收集算法:
G1:对于年轻代,G1采用复制算法;对于老年代,G1主要采用标记-整理算法。
ZGC:利用全局空间视图的切换和对象地址视图的切换,结合SATB(Snapshot-At-The-Beginning)算法实现高效的并发处理。
配置与性能:
G1:配置相对简单,适用于大多数应用场景,但可能会增加CPU的负载和降低吞吐量。
ZGC:配置简单,适用于需要处理大量常见对象和哈希表的大型Java应用程序,如电子商务系统、大数据应用程序等。ZGC提供了极低的垃圾收集停顿时间,对应用性能的影响微乎其微。
停顿时间:
G1:停顿时间由用户设置的暂停时间目标自动调整。
ZGC:在JDK 11中,停顿时间控制在10毫秒以内;在JDK 16之后,停顿时间已经控制到1毫秒以内。
G1和ZGC在设计理念、分代策略、内存管理、垃圾收集算法、配置与性能以及停顿时间等方面存在显著的差异。开发者在选择使用哪种垃圾收集器时,应根据应用的具体需求和性能目标进行权衡。
SATB算法?
Snapshot-At-The-Beginning(SATB)是一种在垃圾收集过程中使用的算法,主要应用于如G1这样的垃圾收集器的并发标记阶段,用于解决并发标记过程中可能出现的对象漏标问题,维持并发GC的正确性。 三色标记算法:
- 它采用三色标记算法,将对象标记为三种颜色:white(未访问)、gray(已访问但尚未处理完其引用)和black(已访问且已处理完其引用)。
- 标记过程从GC roots(垃圾收集根)开始,遍历堆中的对象,将white对象标记为gray,然后再将gray对象标记为black。
- 当标记阶段完成后,所有black对象都被认为是存活的,而所有white对象则被认为是可回收的。
SATB算法的主要步骤和特点包括:
- 创建快照:在开始标记的时候生成一个快照图,标记存活对象。可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对象就认为是活的,从而形成一个对象图。
- 记录引用变化:在并发标记的时候,所有被改变的对象(主要指一个对象的成员被赋值为
null
的情况)会被记录下来。通过写屏障(write barrier)来实现,在写屏障里把所有旧的引用所指向的对象都变成非白的。这样的话对于之前用三色标记可能出错的白色对象就会变成灰色的,避免其正常的不被回收 。例如,如果一个灰色对象原本指向某个白色对象,但在并发标记过程中,灰色对象对该白色对象的引用被删除了,那么这个白色对象就有可能漏标(因为没有灰色对象指向它了,后续可能不会被标记为可达对象)。通过写屏障机制,会将这个被删除引用的白色对象记录下来,以便后续能正确处理。 - 处理浮动垃圾:SATB算法可能存在浮动垃圾,这些垃圾将在下次被收集。
SATB算法中的一些关键概念和操作如下:
- 相关指针和位图:Region(区域)包含了5个指针,分别是bottom(总是指向Region的起始位置)、previous TAMS(指向上一次并发处理后的地址)、next TAMS(指向并发标记开始之前内存已经分配成功的地址)、top(指向当前内存分配成功的地址)和end(总是指向Region的终点位置)。其中,TAMS全称top-at-mark-start,表示在标记开始时的顶部位置。通过这两个指针以及prevBitmap(记录上一次标记信息)和nextBitmap(创建并更新本次并发标记信息)位图来标记对象状态。并发标记过程中,位于堆分区的Bottom和PTAMS之间的对象都会被标记并记录在previous位图中;位于堆分区的Top和PATMS之间的对象均为隐式存活对象,同时也记录在previous位图中。并发标记结束时,nextBitmap 位图记录了本次标记的存活对象,然后将nextTAMS所在的地址赋给previousTAMS,并清空nextBitmap。下次并发标记开始时重复上述过程 。
- 识别新分配对象:在TAMS以上的对象即(next TAMS, top)之间的对象是新分配的,因而被视为隐式存活对象,SATB能够确保这部分的对象都会被标记,默认都是存活的 。
总之,SATB算法保证了在并发标记过程中新分配对象不会漏标,同时在remark阶段(重新标记阶段),通过处理之前记录的引用变化,能够正确标记那些可能被漏标的对象,从而提高垃圾收集的准确性。但它并不能完全消除所有的漏标情况,可能仍然会存在一些浮动垃圾留到下次收集 。
以下是一个SATB算法工作过程:
假设有两次并发标记过程,
第一次并发标记开始前:
- PrevBitmap为空,NextBitmap待标记。
第一次标记结束后:
- NextBitmap标记了分区对象存活情况,位图中黑色区域表示对应对象存活,并发标记过程中Mutator(应用线程)继续运行,产生新的对象,Top指针继续增长。 第二次并发标记开始前: - 重置指针,Prev指针指向Next指针位置,交换位图,PrevBitmap获取NextBitmap记录,NextBitmap清空。
第二次并发标记结束后:
- Next指针指向标记前已分配内存顶部,即Top指针位置,即完成上次标记时新增对象【Next,Top】区间的扫描标记,NextBitmap记录所有已扫描对象内存标记状态,Top指针持续增长。 最终标记-Remark开始之前:与第二次并发标记结束后状态相同。
最终标记-Remark结束之后:
- Remark阶段STW(暂停Mutator),Top不会继续增长,Prev指向Next,Next 和Top都指向了已分配对象顶部,NextBitmap记录所有对象标记情况。
通过以上步骤可以看出,每次并发标记后,将本次标记结果【Bottom,Prev】区间做了一次Snapshot快照,以Bitmap位图存储,所有垃圾对象通过快照被识别出来。并发标记中Mutator新增的对象都认为是存活对象,设置为灰色,因为SATB关注的是引用的删除,将 o1.filed = new O2()
修改为o1.field = null
这种,会将O2对象置为灰色,加入操作栈,重新进行扫描,解决漏标问题。
新生代垃圾回收器和老生代垃圾回收器都有哪些?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1, ZGC
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
Minor GC触发
新创建的对象一般会被分配在新生代中。某一时刻,创建的对象将 Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。
Minor GC 前的检查
在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。因为假如 Survivor区放不下存活对象,这些对象就要进入到老年代,所以要提前检查老年代是否够用。这样就有两种情况:
- 老年代剩余空间大于新生代中的对象大小,那就直接 Minor GC。
- 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了老年代空间分配担保规则,具体来说就是看-XX:-HandlePromotionFailure参数是否设允许(默认允许)。如果允许,会继续检查老年代最大可利用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尽管该次GC会有风险,仍将尝试进行一次Minor GC;如果小于或者HandlePromotionFailure不允许,那这时需要进行一次Full GC
Minor GC
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 标记存活对象,对象年龄 +1;
- 把Eden存活的对象放入 To Survivor 区;
- From Survivor 中存活对象:年龄达到阈值(-XX:MaxTenuringThreshold,默认15)的对象会被移动到年老代中,没有达到阈值的对象会被复制到 To Survivor区域,若 To Survivor 空间不足,则将对象复制到老年区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换:From Survivor 变 To Survivor,To Survivor 变 From Survivor。
FullGC 触发
- 分配担保机制关闭或失败
- 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。
OOM触发
紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM
MinorGC、MajorGC、FullGC有什么区别?
MinorGC:从年轻代空间(Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为Java对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。
Minor GC触发条件:新生对象需要分配到新生代的Eden,当Eden区的内存不够时需要进行MinorGC
MajorGC(不同情况不同):清理老年代,Major GC发生过程常常伴随一次Minor
FullGC:Full GC可以看做是Major GC+Minor GC共同进行的一整个过程,对整个Java堆和方法区的垃圾收集。Full GC的速度一般会比Minor GC慢10倍以上。一般用的是标记整理和标记清除算法。
Full GC触发条件:
- MinorGC后存活的对象超过了老年代剩余空间
- 方法区内存不足时
System.gc()
调用时,可用通过-XX:+ DisableExplicitGC
来禁止调用System.gc()
- CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC
Java是否存在内存泄露问题?
内存泄露是指一个不再被程序使用的对象或变量还在内存中占有存储空间。一般来讲,内存泄露主要有两种情况:
- 一是在堆中申请的空间没有被释放;
- 二是对象已不再被使用,但还仍然在内存中保留着。
垃圾回收机制的引入可以有效地解决第一种情况;而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。因此,Java语言中的内存泄露主要指的是第二种情况。在Java语言中,容易引起内存泄露的原因主要有:
- 静态集合类,例如HashMap和Vector。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄露,如上例所示。
- 各种连接,例如数据库连接、网络联接以及IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显式地关闭,将会造成大量的对象无法被回收,从而引起内存泄露。
- 监听器。在Java语言中,往往会使用到监听器。通常一个应用中会用到多个监听器,但在释放对象的同时往往没有相应地删除监听器,这也可能导致内存泄露。
- 变量不合理的作用域。一般而言,如果一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄露,另一方面如果没有及时地把对象设置为null,很有可能会导致内存泄露的发生。
- 单例模式可能会造成内存泄露。如果单例对象以静态变量的方式存储,因此它在JVM的整个生命周期中都存在,同时由于它有一个对对象的引用,这样会导致对象不能够被回收。
什么情况下对象会从年轻代进入老年代?
- 对象的年龄超过一定阀值,
-XX:MaxTenuringThreshold
可以指定该阀值 - 动态对象年龄判定,有的垃圾回收算法,并不要求 age 必须达到阀值才能晋升到老年代,
- 大小超出某个阀值的对象将直接在老年代上分配,值默认为 0,意思是全部首选 Eden 区进行分配,-XX:PretenureSizeThreshold 可以指定该阀值,部分收集器不支持
- 分配担保,当 Survivor 空间不够的时候,则需要依赖其他内存(指老年代)进行分配担保,这个时候,对象也会直接在老年代上分配