JVM02_垃圾回收
GC 的相关 VM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
一、何时回收对象
(一)引用计数法(Reference Counting)
该方法通过在对象中维护一个引用计数器,记录对象被引用的次数。当引用计数器为0时,表示对象不再被引用,可以被回收。然而,引用计数法无法解决循环引用的问题,即使对象之间相互引用,但无法被外部访问,引用计数器也不会为0,导致内存泄漏。
⏬A 和 B 成为了一个整体垃圾,相互引用无法被回收。
(二)可达性分析法(Reachability Analysis)
该方法基于 “根可达性” 来判断对象是否存活。垃圾回收器从一组称为"GC Roots"的对象开始,通过可达性分析遍历对象引用链,如果对象不可达(即无法通过GC Roots访问到),则被判定为不存活,可以被回收。
⏬除了 7 之外的节点都可以把 1 作为 ROOT 达到。
常见的GC Roots包括虚拟机栈中的本地变量、静态变量、方法区中的类静态属性等。因为它们是直接或间接引用其他对象的起点,保证了被引用对象的可达性。
1)虚拟机栈中的本地变量:虚拟机栈中的本地变量是线程私有的,用于存储方法的局部变量和参数。当一个方法被调用时,会创建一个栈帧,其中包含了方法的局部变量。这些局部变量可以引用堆中的对象,因此它们被认为是GC Roots。只要线程执行该方法,这些局部变量就会存在,并保持对对象的引用。
2)静态变量:静态变量属于类,存储在方法区中。静态变量在类加载时被初始化,并且在整个程序的生命周期中都存在。静态变量可以直接通过类名访问,因此它们可以引用其他对象,这些被引用的对象也会被视为存活对象。
3)方法区中的类静态属性:方法区中存储了类的元数据信息、常量池、静态变量等。类静态属性在类加载时被初始化,并且在整个程序的生命周期中都存在。类静态属性可以直接通过类名访问,因此它们可以引用其他对象,这些被引用的对象也会被视为存活对象。
二、Java 中的引用
(一)强引用(Strong Reference)
强引用是最常见的引用类型,它是指通过普通的对象引用进行的引用。只要强引用存在(能沿着GCRoots找到,上图的实线),垃圾回收器就不会回收被引用的对象。
(二)软引用(Soft Reference)
软引用是用来描述还有用但非必需的对象。在内存不足时,垃圾回收器会尝试回收软引用对象。使用软引用可以实现内存敏感的缓存等功能。所以在 GC 时可能被回收。
(三)弱引用(Weak Reference)
弱引用是用来描述非必需的对象。在垃圾回收时,(不论是否内存充足)只要对象只有弱引用指向,就会被回收。弱引用通常用于实现辅助数据结构,如WeakHashMap。和软引用类似,在 GC 时可能被回收。
(?)引用队列
引用队列(Reference Queue)是Java中的一种特殊数据结构,用于跟踪对象的引用状态并提供通知机制。它是 java.lang.ref.ReferenceQueue 类的实例。
引用队列主要用于监测对象的生命周期和垃圾回收行为。当一个对象被垃圾回收器标记为即将回收时,会将其放入与之关联的引用队列中。通过监测引用队列,可以得知对象何时被回收,从而进行相应的处理。
在使用引用队列时,通常会将需要监测的引用对象与引用队列进行关联。当对象不再被强引用引用时,如果被标记为即将回收,就会被放入引用队列中。开发者可以通过不断地从引用队列中获取引用对象,判断对象的状态变化,并进行相应的操作,如清理资源、更新缓存、解除对象之间的关联等。
(四)虚引用(Phantom Reference)
虚引用是最弱的引用类型,需配合引用队列完成其功能。虚引用的存在主要是为了在对象被回收时收到系统通知。虚引用无法通过引用获取对象,必须配合引用队列(Reference Queue)使用。
在使用直接内存创建 ByteBuffer 对象时,也会创建 Cleaner 的虚引用对象,使用 ByteBuffer 创建出的直接内存会将地址传给 Cleaner 虚引用对象。当 BB 被垃圾回收,虚引用对象进入引用队列, ReferenceHandler 线程检测到 Cleaner 调用 Unsafe.freeMemory()
释放直接内存。
(五)终结器引用(Finalizer Reference)
终结器引用是指对象的finalize()
方法在垃圾回收之前的引用。当对象即将被回收时,垃圾回收器会将其放入一个待处理的队列中,等待终结器线程执行finalize()
方法。即第一次回收时并没有做到完全回收,效率较低。
引用类型 | 特点 |
---|---|
强引用 | 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 |
软引用 | 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象;可以配合引用队列来释放软引用自身 |
弱引用 | 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象;可以配合引用队列来释放弱引用自身 |
虚引用 | 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由 Reference Handler线程调用虚引用相关方法释放直接内存 |
终结器引用 | 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次 GC 时才能回收被引用对象 |
(六)软引用的应用
对于一些不重要但是非常占内存的数据,可以使用软引用进行管理,以便在内存不足时触发垃圾回收。
List
强引用 SoftReference
,SoftReference
对 Byte
数组就是一个软引用,当内存不足的时候,软引用被 GC。
//虚拟机参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc
//强引用
public static void strong() throws IOException {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
//软引用
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
//System.gc(); 查看和弱引用的区别打卡即可
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
⏬两种不同的结果:强引用导致了 OOM;软引用则因为内存不足回收了部分对象。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.purearc.ref.Demo2_3.main(Demo2_3.java:21)
[B@135fbaa4
1
[B@45ee12a7
2
[B@330bedb4
3
//循环三次已经开始进行MinorGC
[GC (Allocation Failure) [PSYoungGen: 2091K->488K(6144K)] 14379K->13056K(19968K), 0.0011743 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//再次触发
[B@2503dbd3
4
[GC (Allocation Failure) --[PSYoungGen: 4809K->4809K(6144K)] 17377K->17409K(19968K), 0.0014461 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Minor效果不佳,触发 FullGC
[Full GC (Ergonomics) [PSYoungGen: 4809K->4572K(6144K)] [ParOldGen: 12600K->12555K(13824K)] 17409K->17128K(19968K), [Metaspace: 3437K->3437K(1056768K)], 0.0040962 secs] [Times: user=0.06 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 4572K->4572K(6144K)] 17128K->17136K(19968K), 0.0007782 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// PSYoungGen: 4572K->0K(6144K)] [ParOldGen: 12563K->717K 软引用被 GC
[Full GC (Allocation Failure) [PSYoungGen: 4572K->0K(6144K)] [ParOldGen: 12563K->717K(8704K)] 17136K->717K(14848K), [Metaspace: 3437K->3437K(1056768K)], 0.0046903 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@4b67cf4d
5
循环结束:5
null
null
null
null
//只剩一个 4MB 大小,占用75% eden space 5632K, 75%
[B@4b67cf4d
Heap
PSYoungGen total 6144K, used 4263K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 75% used [0x00000000ff980000,0x00000000ffda9db0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
(七)软引用清理
JVM内存不足时,垃圾回收器会尝试回收软引用对象。如果软引用对象没有被任何强引用对象引用,并且没有被垃圾回收器标记为"可达",则软引用对象会被回收并释放内存。
然而,即使JVM内存不足导致软引用被回收,软引用对象在被回收之前会存在于引用队列中。开发者可以通过引用队列来跟踪软引用对象的回收情况。在软引用对象被回收之前,可以从引用队列中取出软引用对象,进一步处理或记录相关信息。
虽然内存不足时将软引用指向的对象释放(null),但是引用本身还存在 List 中,下面是将软引用本身也清理掉:
使用引用队列将软引用入队, 当 Byte 对象被回收,引用对象本身也被加入到引用队列中,既然在队列中,就说明改引用即将指向 null,这时候就可以依次将里面的引用抛出。
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
[B@135fbaa4
1
[B@45ee12a7
2
[B@330bedb4
3
[GC (Allocation Failure) [PSYoungGen: 1978K->488K(6144K)] 14266K->13104K(19968K), 0.0006268 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@2503dbd3
4
[GC (Allocation Failure) --[PSYoungGen: 4922K->4922K(6144K)] 17538K->17546K(19968K), 0.0005672 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4922K->4513K(6144K)] [ParOldGen: 12624K->12588K(13824K)] 17546K->17102K(19968K), [Metaspace: 3405K->3405K(1056768K)], 0.0034507 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) --[PSYoungGen: 4513K->4513K(6144K)] 17102K->17110K(19968K), 0.0004466 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4513K->0K(6144K)] [ParOldGen: 12596K->692K(8704K)] 17110K->692K(14848K), [Metaspace: 3405K->3405K(1056768K)], 0.0040550 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@4b67cf4d
5
===========================
//使用 引用队列 处理之后,为 null 的引用消失
[B@4b67cf4d
Heap
PSYoungGen total 6144K, used 4318K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 76% used [0x00000000ff980000,0x00000000ffdb7a48,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 8704K, used 692K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
object space 8704K, 7% used [0x00000000fec00000,0x00000000fecad088,0x00000000ff480000)
Metaspace used 3430K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 367K, capacity 388K, committed 512K, reserved 1048576K
(八)弱引用对象回收
/**
* 演示弱引用
* VM参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
System.gc();
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
⏬全是 null,总内存20MB,即使没有满,在我们主动触发 GC 后对象也被回收。
//前面的也都是 null,和软不同,即使内存足够,由于我们主动 GC,弱引用的对象也被回收。
[GC (System.gc()) [PSYoungGen: 1864K->488K(6144K)] 5960K->4864K(19968K), 0.0008865 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 488K->0K(6144K)] [ParOldGen: 4376K->707K(13824K)] 4864K->707K(19968K), [Metaspace: 3331K->3331K(1056768K)], 0.0042680 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null
//此处省略很多 null
[GC (System.gc()) [PSYoungGen: 4208K->64K(6144K)] 4909K->765K(19968K), 0.0003785 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(6144K)] [ParOldGen: 701K->701K(13824K)] 765K->701K(19968K), [Metaspace: 3424K->3424K(1056768K)], 0.0053032 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
null null null null null null null null null null
循环结束:10
Heap
PSYoungGen total 6144K, used 198K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 3% used [0x00000000ff980000,0x00000000ff9b1948,0x00000000fff00000)
三、垃圾回收算法
(一)标记-清除算法(Mark and Sweep)
该算法分为两个阶段。首先,从根对象开始,标记所有与根对象直接或间接可达的对象。然后,清除未被标记的对象,释放其占用的内存空间。
标记-清除算法速度快,只需要记录要回收对象的起始位置放入"空闲空间表" 中就可以;
标记-清除算法会产生内存碎片,对于大对象或长时间存活的对象,可能会导致效率问题(参考操作系统就很好理解)。
(二)标记-整理算法(Mark and Compact)
该算法结合了标记-清除算法和复制算法的优点。首先,标记所有可达对象,并将它们向一端移动。然后,清除未被标记的对象,并更新内存分配指针。标记-整理算法消除了内存碎片,但是需要移动对象,可能会增加回收时间。
(三)复制算法(Copying)
该算法将堆内存分为两个相等的区域,一次只使用其中一个区域。当发生垃圾回收时,将存活的对象复制到另一个区域,并且内存分配指针复位,这样 TO 就一直是空闲的空间。 复制算法避免了内存碎片,但是需要额外的内存空间。
四、分代垃圾回收
分代收集算法(Generational Collection):该算法根据对象的存活时间将堆内存划分为不同的代(Generation)。一般将新创建的对象(或用完即丢的对象)放入新生代,而经过多次垃圾回收仍然存活的对象(即可能要长时间使用的对象)会被晋升到老年代。相较于老年代,新生代的垃圾回收更为频繁。
年轻代使用复制算法
进行垃圾回收,而老年代使用标记-整理算法
或标记-清除算法
。分代收集算法利用了对象的特性,提高了垃圾回收的效率。
下面使用一段空的程序查看内存空间 :
VM 参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
Heap
//新生代:伊甸园和FROM、TO 二八分,其中伊甸园的 24%占用是类加载的必要资源,默认的 1024K 的 TO 区域不会被算入总量
def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf6bf8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
//老年代,10240k
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
//元空间,并不属于堆的区域
Metaspace used 3124K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 330K, capacity 388K, committed 512K, reserved 1048576K
(一)分代回收的执行过程
(1)新生代空间不够,触发第一次 MinorGC;
使用复制算法将未被回收的对象放入幸存区TO中,为被回收的对象存活年龄 +1
,我们管这个值叫 晋升阈值
;同时交换 FROM 和 TO 的位置。
MinorGC 会引发 Stop the World,当进行垃圾回收时必须停止其他的用户线程,当垃圾回收线程结束,用户线程才能重新开始。
(2)伊甸园区再度内存不足,触发第二次 MinorGC;
MinorGC 会对伊甸园和幸存区的对象进行垃圾回收,没有被回收的对象晋升阈值 +1
,交换 FROM 和 TO。
(3)当对象经历的 MinorGC 次数达到一定界限,JVM 认为该对象是长期使用的,移动进入老年代。
(4)当老年代区域内存不足,会先尝试MinorGC,若空间仍然不足,JVM 触发 FullGC 回收所有内存(FullGC 自然也会引起咋瓦鲁多,且时间更长)。
(二)大对象的GC
在JVM中,当一个对象被认定为大对象时,如果老年代空间足够,垃圾回收器可以选择直接将该对象晋升到老年代。这个过程被称为直接晋升(Direct Promotion)。
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
//第二次放入8M的对象,新生代老年代内存都不足,而对象是强类型
//list.add(new byte[_8MB]);
}
}
⏬加载类时已经占用的20%以上的空间,8M的大对象是不能直接放入新生代,但老年代空间充足
⏬新生代、老年代空间都不足,而大对象又能由 GCRoots 找到无法回收,在进行自救(GC和FullGC)后无果
(三)子线程OOM
当一个子线程发生OOM时,它只会影响到该子线程本身,而不会对其他线程产生直接的影响。
在Java中,每个线程都有自己的栈空间和堆空间。当子线程发生OOM时,通常会抛出OutOfMemoryError
异常,并且该子线程的执行会被中断,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行。其他线程(包括主线程)仍然可以继续执行,除非发生了其他严重的问题。
然而,子线程的OOM可能会间接地影响到主线程的执行。例如,如果子线程是主线程的子任务,而子线程的OOM导致任务无法完成,那么主线程可能会在等待子线程完成的过程中被阻塞,或者因为子线程异常退出而无法获取到子线程的执行结果。
当子线程发生OOM时,通常会抛出OutOfMemoryError异常,并且子线程的执行会被中断。如果子线程在执行某个任务,而主线程依赖子线程的结果或者需要等待子线程完成某个操作,那么主线程可能会被阻塞,无法继续执行。此外,如果子线程的OOM导致整个堆内存空间不足,可能会触发JVM的垃圾回收机制(例如Full GC),尝试回收内存空间。这可能会导致主线程在等待垃圾回收完成的过程中被阻塞,影响主线程的执行。
五、垃圾回收器
(一)串行垃圾回收器(Serial GC)
JVM中的串行垃圾回收器是一种单线程的垃圾回收器,也被称为Serial GC。它是JVM默认的垃圾回收器,并且只能使用单个CPU核心进行垃圾回收。
在串行垃圾回收器中,新生代采用复制算法进行垃圾回收,而老年代采用标记-清除-整理算法。
(1)相关VM参数
-XX:+UseSerialGC = Serial + Serial0ld
(2)执行过程
串行垃圾回收器的工作方式是在进行垃圾回收时,暂停所有应用程序的线程,使他们到达一个 "安全点" ,然后单线程地扫描整个堆内存,标记和清除不再使用的对象。由于只有一个线程在执行垃圾回收,因此会造成其他用户线程较长的停顿时间,可能会影响应用程序的响应性能。
(3)适用情况
串行垃圾回收器适用于小型应用程序或者单核系统,因为它不需要额外的线程和内存开销。但是在多核系统上,串行垃圾回收器无法利用多个CPU核心的优势,因此可能会导致较长的垃圾回收时间。
(二)吞吐量优先(Throughput-Oriented)[并行垃圾回收器Parallel GC]
吞吐量优先的目标是最大化系统的总体处理能力,即在单位时间内完成尽可能多的工作量。
在吞吐量优先的垃圾回收器中,新生代采用复制算法进行垃圾回收,而老年代采用标记-清除-整理算法。
(1)相关VM参数
//1.8 默认开启,只要开启一个另一个也会开启
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
//自适应的大小(新生代空间、阈值、新生代的内存占比)
-XX:+UseAdaptiveSizePolicy
/**
二者需要取合适的值,若吞吐量变大,则会间接导致进行GC的暂停时间边长
吞吐量变小(堆内存变大),则会间接使暂停时间变小
*/
//调整吞吐量目标(垃圾回收的时间和总时间的占比 1/1+radio,若达不到目标,则 ParallelGC 会对内存大小进行调整(堆变大))
-XX:GCTimeRatio=ratio
//最大暂停时间(stw的时间) 默认200ms
-XX:MaxGCPauseMillis=ms
//控制GC时的线程数量
-XX:ParallelGCThreads=n
(2)执行过程
(3)适用情况
这种优化目标适合于后台运行的批处理任务或者需要处理大量数据的应用程序。通常,吞吐量优先的垃圾回收器会更关注垃圾回收的效率,尽可能减少垃圾回收的时间开销,即减少应用程序停顿的时间。
(三)响应时间优先(Latency-Oriented)[并发垃圾回收器Concurrent Mark Sweep GC]
响应时间优先的目标是最小化应用程序的停顿时间,以提供更好的用户体验和实时响应性能。在响应时间优先的垃圾回收器中,新生代采用复制算法进行垃圾回收,而老年代采用标记-清除-整理算法。
(1)相关VM参数
//老年代和新生代的垃圾回收器 若并发失败(老年代内存碎片过多可能导致) ConcMarkSweepGC 则会变成 SerialOld 的单线程垃圾回收器(和G1垃圾回收器的老年代内存回收相同,不过现在的G1即使是FullGC也已经是多线程)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
//并行的垃圾回收线程数 ~ 设置为并行线程数的 1/4 部分做GC,剩下的线程占用CPU用于用户线程
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
//执行CMS的内存占比:CMS由于和用户线程并发进行,在GC的同时用户可能产生“浮动垃圾”,所以需预留空间
-XX:CMSInitiatingOccupancyFraction=percent
//在重新标记前对新生代进行GC,:重新标记时新生代可能引用老年代的对象,而新生代大多是可能要进行回收的,标记了纯纯浪费时间
-XX:+CMSScavengeBeforeRemark
(2)执行过程
并发的垃圾回收器是指在垃圾回收过程中,与应用程序的执行并发进行,即垃圾回收和应用程序的线程可以同时运行。与串行垃圾回收器相比,它能够减少应用程序的停顿时间,提高系统的响应性能和用户体验。
1)初始标记(Initial Mark):在这个阶段,垃圾回收器会暂停应用程序的执行(Stop The World),然后从根对象开始,标记所有直接可达的存活对象。这个阶段通常比较短暂,只标记了一部分对象。
2)并发标记(Concurrent Marking):在初始标记完成后,垃圾回收器会启动一个或多个线程,并发地进行标记工作。这些线程会遍历对象图,标记所有可达的存活对象。与此同时,应用程序的线程可以继续执行,垃圾回收器的线程与应用程序的线程并发运行。
3)重新标记(Remark):并发标记阶段结束后,垃圾回收器会再次暂停应用程序的执行(Stop The World),进行重新标记。这个阶段的目的是标记在并发标记期间产生的新的存活对象。重新标记是一个短暂的阶段,通常比并发标记要快速。
4)并发清理(Concurrent Cleaning):在重新标记完成后,垃圾回收器会启动一个或多个线程,并发地进行清理工作。这些线程会清理已经标记为垃圾的对象,并回收它们占用的内存空间。与此同时,应用程序的线程可以继续执行,垃圾回收器的线程与应用程序的线程并发运行。
(3)适用情况
这种优化目标适合于交互式应用程序或者需要快速响应的任务。通常,响应时间优先的垃圾回收器会更关注降低应用程序停顿的时间,即减少垃圾回收的影响。
六、G1 垃圾回收器
G1(Garbage-First)垃圾回收器是一种并发的、分代的垃圾回收器,它在JDK 7u4 版本中引入,并在 JDK9 中成为默认的垃圾回收器。
-
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是
200 ms
-
超大堆内存,会将堆划分为多个大小相等的
Region
-
整体上是
标记+整理算法
,两个区域之间是复制算法
G1垃圾回收器的设计目标是在满足低停顿时间的同时,提供高吞吐量和可预测的性能。它使用了分代回收和区域化内存管理的方式,将堆内存划分为多个大小相等的区域(Region),每个区域可以是新生代或老年代。
(一)相关VM参数
//1.8下默认不采用,打开G1
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
(二)执行过程
G1(Garbage-First)垃圾回收器是一种用于Java虚拟机的垃圾回收器。它的垃圾回收过程分为以下三个阶段:
(1)Young Generation Collection(年轻代回收):在这个阶段,G1回收器主要关注年轻代的内存区域,即Eden区和Survivor区。在该阶段会JVM会进行GCRoots的初始标记,同时暂停用户线程(STW)。它使用复制算法来回收垃圾对象。当Eden区满时,将触发一次年轻代回收。在这个阶段,存活的对象会被复制到Survivor区或者老年代中。
(2)Concurrent Marking(并发标记):在年轻代回收阶段完成后,且老年代占用堆内存空间达到阈值 时,G1垃圾回收器会进行并发标记(通过改变参数XX:InitiatingHeapOccupancyPercent=percent (默认45%)
实现)。它会遍历整个堆,标记所有存活的对象。这个过程是与应用程序的执行同时进行的,尽量减少对应用程序的停顿时间。
(3)Mixed Generation Collection(混合代回收):在并发标记阶段完成后,应用程序会被暂停,然后垃圾回收器会再次扫描堆内存,标记出在并发标记阶段中被遗漏的存活对象。这个阶段(最终标记)的目的是确保所有的存活对象都被正确标记,以便后续的垃圾回收操作。
在最终标记阶段完成后,就可以进行 拷贝存活 操作了。拷贝存活操作是将存活对象从年轻代拷贝到老年代的过程。在年轻代的存活区域中,存活对象会被复制到老年代,而未存活的对象将被回收在并发标记阶段完成后,G1回收器开始执行混合代回收。
G1回收器会根据预设的回收目标来选择回收哪些区域。混合代回收是基于分代的思想,它会同时回收年轻代和老年代的内存区域。回收过程中,G1会优先回收垃圾最多的区域,也就是 Garbage-First
的含义。这个阶段可能会暂停应用程序的执行,但是尽量保持在可接受的范围内。
(三)YongCollection跨代引用
在Minor GC过程中,如果有对象在年轻代中被引用,并且这些对象的引用链跨越到了老年代(Old Generation),那么这些引用就被称为"跨代引用"(Cross-generation Reference)。
举个例子,假设有一个Java类Person,它的实例对象person1在年轻代中创建,并被引用到了老年代中的一个集合对象中。此时,集合对象中对person1的引用就是一个跨代引用。
javaList<Person> list = new ArrayList<>();
Person person1 = new Person("John");
list.add(person1);
跨代引用的存在导致对象的存活时间延长:当一个对象在年轻代被引用,但在老年代中没有被引用时,这个对象的存活时间可能会被延长。
跨代引用的存在会对垃圾回收造成一些影响。当进行Minor GC时,年轻代中的对象会被复制到另一个Survivor区,而老年代中的对象不会被处理。如果跨代引用存在,那么在复制对象时,需要更新跨代引用的指向,以确保引用关系的正确性。
JVM通过卡表(CardTable)来处理跨代引用的问题。卡表是一种数据结构,用于记录老年代中的页面是否包含跨代引用。卡表的基本思想是将老年代内存划分为固定大小的页面(通常是512字节),并为每个页面分配一个对应的卡表项。卡表项可以是一个位图或者一个字节,用来表示对应页面是否包含跨代引用。
在年轻代进行垃圾回收时,垃圾回收器只需要扫描卡表中标记为有跨代引用的页面(DirtyCardQueue),而不需要扫描整个老年代。这样可以减少扫描的开销。
具体的处理过程如下:
(1)初始标记阶段:在初始标记阶段,垃圾回收器会暂停应用程序的执行,并扫描堆内存,标记出所有的根对象和直接与根对象有关联的对象。在扫描过程中,如果发现一个对象的引用指向了老年代中的页面,就会将对应的卡表项标记为有跨代引用。
(2)并发标记阶段:在并发标记阶段,应用程序可以继续执行,而垃圾回收器则在后台进行标记工作。在这个阶段,垃圾回收器会遍历堆内存中的对象图,标记出所有与根对象可达的存活对象。同时,如果发现一个对象的引用指向了老年代中的页面,就会将对应的卡表项标记为有跨代引用。
(3)并发清除阶段:在并发清除阶段,垃圾回收器会清除所有未标记的对象,并将存活的对象向内存的一端移动,以便为新的对象分配连续的内存空间。在这个阶段,垃圾回收器会根据卡表中标记的跨代引用信息,只处理标记为有跨代引用的页面。
(四)重新标记(Remark)
重新标记发生在并发标记阶段之后,用于修正在并发标记期间发生变化的对象标记状态。当进行并发标记时用户线程也可能对引用进行修改,重标记阶段的作用就是保证在并发标记期间发生变化的对象能够被正确地标记为存活或垃圾。通过重标记阶段,垃圾回收器能够修正并保持堆内存的一致性,为后续的垃圾回收操作打下基础。
(1)三色标记法
JVM的三色标记法(Three-Color Marking)是一种垃圾回收算法中常用的标记方式,用于标记对象的存活状态。它基于对象的可达性来判断对象是否为存活对象,并在垃圾回收过程中进行相应的处理。
三色标记法的基本思想是将对象分为三种不同的颜色:白色、灰色和黑色。每个对象都有一个对应的颜色,用于表示其存活状态。
1)白色:初始状态下,所有的对象都被标记为白色,表示它们尚未被访问和扫描。
2)灰色:当一个对象被访问到时,它的颜色会被标记为灰色。灰色表示该对象已经被访问,但其引用的对象尚未被扫描和标记。
3)黑色:当一个对象及其引用的对象都被扫描和标记后,它的颜色会被标记为黑色。黑色表示该对象及其引用的对象都是存活的。
(2)读屏障
读屏障:读屏障是一种机制,用于在重新标记阶段期间,检测对象引用的读取操作。它的目的是确保在读取对象引用之前,垃圾回收器能够正确地获取对象的标记状态。
在重新标记阶段期间,读屏障会在读取对象引用之前被触发。它会检查对象的标记状态,以确保读取的对象引用是准确的。如果对象的标记状态发生变化,读屏障会通知垃圾回收器进行相应的处理,以保证标记的一致性。
读屏障(Read Barrier)的工作流程:
- 当程序在重新标记阶段读取一个对象的引用时,读屏障会被触发。
- 读屏障会检查对象的标记状态,以确保读取的对象引用是准确的。
- 如果对象的标记状态为白色,表示该对象尚未被访问和扫描,读屏障会触发垃圾回收器进行相应的处理,例如将对象标记为灰色,以确保对象的正确标记。
- 如果对象的标记状态为灰色或黑色,表示该对象已经被访问和扫描,读屏障会继续执行正常的读取操作。
读屏障的作用是保证在重新标记阶段期间读取的对象引用是准确的,以确保垃圾回收器能够正确地获取对象的标记状态。
(3)写屏障
写屏障:写屏障是一种机制,用于在重新标记阶段期间,检测对象引用的写入操作。它的目的是捕获对象引用的变化,以便垃圾回收器能够正确地处理引用关系的变化。
在重新标记阶段期间,写屏障会在对象引用发生变化时被触发。它会记录下引用的变化,并通知垃圾回收器进行相应的处理,以保证标记的一致性。写屏障可以捕获对象引用的变化,并确保垃圾回收器能够正确地跟踪和处理这些变化。
写屏障(Write Barrier)的工作流程:
- 写入对象引用之前,预写屏障会被触发。
- 预写屏障记录引用的变化,并将变化的引用添加到SATB标记队列中。
- 在重新标记阶段,垃圾回收器会处理SATB标记队列中的引用变化。
- 垃圾回收器遍历SATB标记队列,将队列中的引用进行标记,以确保引用关系的准确性和一致性。
- 处理完SATB标记队列中的引用变化后,垃圾回收器继续进行其他的标记和回收操作。
写屏障的作用是捕获对象引用的变化,并通知垃圾回收器进行相应的处理,以保证标记的一致性和准确性。
(五)JDK8/9各个版本对G1的优化
(1)JDK8U20 对字符串去重
JDK8的 String 类底层实现为 char[] ,在这篇文章中有提到过当 String 对象很多的时候可以通过 String.intern() 方法将对象放入 Table 进行去重。
使用-XX:+UseStringDeduplication
参数启用字符串去重功能时,JVM会在堆中进行字符串的去重操作。它会遍历堆中的所有字符串,将重复的字符串对象替换为引用相同内容的共享对象。这样可以减少内存中相同内容的字符串的存储量,提高内存利用率。
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
//启动去重垃圾回收时会将两个的引用指向同一个char数组
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
过程如下:
-
将所有新分配的字符串放入一个队列
-
当新生代回收时,G1并发检查是否有字符串重复
-
如果它们值一样,让它们引用同一个 char[]
与 String.intern() 不一样:
-
String.intern() 关注的是字符串对象,而字符串去重关注的是
-
char[] 在 JVM 内部,使用了不同的字符串表
(2)JDK8U40并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -XX:+ClassUnloadingWithConcurrentMark
默认启用。这意味着类的卸载可以与并发标记过程并行进行,而不是等待到Full GC阶段。
这个参数的使用可以提高垃圾回收的效率,减少Full GC的暂停时间,从而改善应用程序的响应性能。然而,需要注意的是,启用并发类卸载可能会增加一些额外的CPU和内存开销。需要注意的是,-XX:+ClassUnloadingWithConcurrentMark
参数只在支持并发标记的垃圾回收器(如CMS垃圾回收器)中才有效。在使用其他垃圾回收器时,该参数可能会被忽略。
(3)JDK8U60回收巨型对象
一个对象大于 region
的一半时,称之为巨型对象 G1 不会对巨型对象进行拷贝,回收时巨型对象被优先考虑。
在JDK 8u60之前,巨型对象会被直接分配在老年代中,并且垃圾回收器不会对其进行自动回收。这意味着,如果一个巨型对象不再被引用,它仍然会占用大量的内存空间,直到发生Full GC(全局垃圾回收)时才会被回收。
然而,自JDK 8u60开始,引入了一项新的特性,即巨型对象的自动回收。这个特性可以通过使用参数-XX:+UseG1GC
来启用。当启用G1垃圾回收器时,巨型对象将会被分配在堆中的一个特殊区域,称为"Humongous"区域。G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉。
(4)JDK 9 并发标记起始时间的调整
-XX:InitiatingHeapOccupancyPercent
用于设置触发并发标记周期的堆占用阈值百分比。在JVM的垃圾回收过程中,当堆占用达到一定阈值时,会触发并发标记阶段。
在JDK 8中,InitiatingHeapOccupancyPercent
参数仅用于设置并发标记周期的触发条件,即当堆占用达到或超过指定的百分比时,触发并发标记。 JDK 9 可以动态调整 -XX:InitiatingHeapOccupancyPercent
用来设置初始值,进行数据采样并动态调整,这样能够保证总会添加一个安全的空档空间用于容纳浮动垃圾。
七、GC调优
查看当前 GC 的信息 java -XX:+PrintFlagsFinal -version | findstr "GC"
(一)最快的GC
"最快的GC就是不发生GC",因为垃圾回收(GC)是一种资源消耗较大的操作。当应用程序的内存管理得当,不产生大量垃圾对象,或者能够充分重用对象并及时释放不再使用的对象时,就能减少GC的频率和开销。
以下是为什么不发生GC被认为是最快的GC的几个原因:
(1)减少停顿时间:垃圾回收器在执行GC时需要暂停应用程序的执行。这些暂停被称为“停顿时间”或“GC暂停时间”。当不发生GC时,应用程序可以连续运行,无需因GC而停顿,从而提供更好的响应性能和用户体验。
(2)减少CPU开销:垃圾回收是一个资源密集型的操作,会消耗CPU资源。当不发生GC时,CPU可以用于执行应用程序的业务逻辑,而不必用于垃圾回收,从而提高应用程序的吞吐量和性能。
(3)减少内存压力:当发生GC时,垃圾回收器会扫描和清理不再使用的对象,释放内存空间。如果应用程序频繁产生大量垃圾对象,会增加内存压力,导致更频繁的GC操作和更长的停顿时间。避免产生大量垃圾对象可以减轻内存压力,提高应用程序的性能和稳定性。
(二)新生代调优
GC调优通常从新生代开始,主要有以下原因:
(1)新生代中的对象生命周期较短:大部分对象在应用程序中的生命周期很短,很快就会被回收。因此,通过优化新生代的GC,可以更快地回收这些短命对象,减少老年代的压力。
(2)新生代的GC开销相对较低:与老年代相比,新生代的GC开销通常较低,可以更快地完成垃圾回收。因此,通过优化新生代的GC,可以更快地恢复可用的内存空间,并减少GC的停顿时间。
(3)大部分对象在新生代中被回收:根据大部分应用程序的特性,大部分对象的生命周期较短,很快就会被回收。因此,通过优化新生代的GC,可以更好地处理这些短命对象,减少老年代的压力和GC的频率。
进行新生代调优时,有以下几个要点:
(1)设置合适的新生代大小:新生代是堆内存中的一部分,用于存放新创建的对象。通过调整新生代的大小,可以控制对象的生命周期和垃圾收集的频率。一般来说,新生代的大小应该根据应用程序的特点和内存需求进行调整,以避免频繁的垃圾收集。
(2)选择合适的垃圾收集器:JVM中有多种垃圾收集器可供选择,如Serial、Parallel、CMS、G1等。不同的垃圾收集器有不同的特点和适用场景,选择合适的垃圾收集器可以提高垃圾收集的效率和吞吐量。
(3)设置合适的垃圾收集策略:垃圾收集器可以根据不同的策略进行垃圾收集,如标记-清除、复制、标记-整理等。选择合适的垃圾收集策略可以提高垃圾收集的效率和内存利用率。
(4)设置合适的垃圾收集参数:JVM提供了一些垃圾收集相关的参数,如堆大小、新生代大小、Eden空间大小等。通过调整这些参数,可以进一步优化垃圾收集的效果和性能。
(5)进行性能测试和监控:在进行新生代调优之前,应该先进行性能测试和监控,以了解应用程序的内存使用情况和垃圾收集的性能指标。通过分析测试结果和监控数据,可以确定是否需要进行新生代调优,以及如何进行调优。
(三)老年代调优
老年代(Old Generation)的调优可以从以下几个要点入手:
(1)老年代的大小:老年代的大小对于应用程序的性能和GC的效率有重要影响。如果老年代较小,可能会导致频繁的Major GC(老年代垃圾回收)和Full GC(全局垃圾回收),增加GC的开销和停顿时间。如果老年代较大,可以减少Major GC和Full GC的频率,但也会增加每次GC的时间。需要根据应用程序的内存需求和性能要求来调整老年代的大小。
(2)对象晋升策略:老年代中的对象通常是存活时间较长的对象。通过调整对象的晋升策略,可以控制对象在老年代中的存活时间和老年代的使用情况。例如,可以调整年龄阈值和晋升速度来控制对象晋升到老年代的频率。
(3)Full GC的优化:Full GC是对整个堆进行回收的操作,包括新生代和老年代。Full GC的开销通常比Minor GC更大,停顿时间更长。通过优化Full GC的触发和执行,可以减少GC的频率和停顿时间。例如,可以调整Full GC的触发条件、调整GC算法和策略等。
(4)内存泄漏的排查和处理:内存泄漏是指应用程序中的对象无法被垃圾回收器回收,导致内存占用不断增加。及时排查和处理内存泄漏问题对于老年代的调优至关重要。通过使用内存分析工具和代码审查,可以发现和修复内存泄漏问题,减少老年代的压力。
(5)使用合适的垃圾回收器:选择适合应用程序需求和硬件环境的垃圾回收器,并根据应用程序的特性和性能要求进行相应的配置。不同的垃圾回收器有不同的特点和适用场景,如串行回收器、并行回收器、CMS回收器、G1回收器等。
(6)监控和分析GC日志:通过监控和分析GC日志,可以了解GC的情况,包括GC的频率、停顿时间、内存使用情况等。这有助于发现GC问题和瓶颈,并进行相应的调整和优化。
(7)测试和验证:进行老年代调优时,需要进行充分的测试和验证。通过压力测试和性能测试,验证调优的效果和稳定性。同时,注意监控应用程序的内存使用情况和GC的表现,及时调整和优化。