:::info
什么是垃圾回收
:::
垃圾回收是一种自动管理内存的机制,用于检测和释放程序中不再被引用的内存对象,以避免内存泄漏和提高程序性能。
垃圾回收的目标是优化内存使用,减少内存泄露的可能性,提高程序的性能和稳定性。开发人员通常无需手动释放对象,而是依赖于垃圾回收器来管理内存。这使得开发更加方便,同时避免了一些与手动内存管理相关的常见错误。
引用计数法是一种垃圾回收算法,主要思想就是为每个对象维护一个引用计数,每当有一个引用指向对象时,计数加一,当引用失效时,计数减一,当计数为零时,表示对象不再被引用,可以被回收。
但是,引用计数法有一些问题,其中最主要的问题就是无法解决循环引用的情况。比如A对象引用B对象,B对象又引用A对象,形成了一个环,这就是循环引用。即使这组对象不再被其他对象引用,它们之间的引用计数也永远不会变为零,导致这组对象无法被垃圾回收。
由于Java中存在复杂的数据结构和对象引用关系,引用计数法无法很好地处理这些情况。因此,Java使用可达性分析算法。
这个算法的核心思想是通过根集合(包括类变量、本地变量等)作为起点,通过一系列的引用关系追踪对象的可达性,确定哪些对象是可达的,哪些是不可达的,然后回收不可达对象。
基本原理和步骤:
这就好像是BFS算法,只要是从根节点搜索到的都保留,没搜索到的都舍去。
为啥解决了循环引用问题呢?
一组对象之间是循环引用,那么只要外界没有对象引用它们其中任意一个,那么这组对象就永远不会被跟踪到,那么就会被认为是不可达的,在清除阶段就会被回收。
:::info
强引用
:::
强引用时最常见、最普遍的引用类型,当一个对象具有强引用时,垃圾回收器不会回收这个对象,即使系统中内存不足。只有当不再有任何强引用指向对象时,才有可能被垃圾回收器回收。
举个例子:
public class StrongReferenceExample {
public static void main(String[] args) {
// 创建一个对象并赋予强引用
Object obj = new Object();
// obj 强引用仍然存在,对象不会被垃圾回收
System.out.println("Object is still reachable.");
// 将 obj 引用置为 null,此时没有强引用指向对象
obj = null;
// 此时对象变为不可达,但不一定会立即被回收
// 垃圾回收器会在适当的时候回收不可达的对象
// 一旦垃圾回收器决定回收对象,它的 finalize() 方法(如果有)将被调用
// 最终,对象的内存将被释放
System.gc(); // 不建议显式调用 System.gc(),这里仅作示例
}
}
在这个示例中,obj是一个强引用,它持有对一个Object对象的引用。只有当obj不再引用对象时,该对象才变为不可达,从而变得可以被垃圾回收。强引用通常在程序中广泛使用,因为它们提供了最直观的对象引用方式,但也需要开发者注意确保及时释放不再需要的对象引用,以便让垃圾回收器更好地管理内存。
:::info
软引用
:::
软引用用于描述有用但非必须的对象。在系统将要发生内存溢出之前,会尽可能地保留软引用指向的对象。如果垃圾回收器确定内存不足,就会回收这些对象。使用SoftReference
类创建软引用。
使用软引用的主要目的是允许在内存不足时释放一些可有可无的对象,从而避免内存溢出。软引用通常用于实现缓存、允许在系统内存不足时自动释放不必要的缓存对象。
import java.lang.ref.SoftReference;
public class SoftReferenceExample {
public static void main(String[] args) {
// 创建一个对象并赋予软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 获取软引用指向的对象,obj现在是强引用
Object obj = softRef.get();
// obj 引用仍然存在,对象不会被垃圾回收
System.out.println("Object is still reachable.");
// 将 obj 引用置为 null,此时没有强引用指向对象
// 当系统内存不足时就会释放掉软引用的对象
obj = null;
// 此时对象变为不可达,但不一定会立即被回收
// 垃圾回收器会在适当的时候回收不可达的对象
System.gc(); // 不建议显式调用 System.gc(),这里仅作示例
// 获取软引用指向的对象
Object objAfterGC = softRef.get();
// 如果对象被回收,objAfterGC 将为 null
if (objAfterGC == null) {
System.out.println("Object has been garbage collected.");
} else {
System.out.println("Object is still reachable after garbage collection.");
}
}
}
举个例子:
我们在读取一些文件时,文件大小可能会非常大,而我们如果使用强引用就会导致内存溢出。比如下面代码,将每一个new出来的对象加入到list集合中,这样每一个对象都是强引用,导致不能被垃圾回收。
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
我们可以使用软引用,当内存不足时把那些对象给回收掉。
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.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
我们只是清除掉了软引用指向的对象,软引用本身还没有处理。
我们还需要使用引用队列来回收软引用对象。将软引用对象关联引用队列,当软引用的引用对象被回收时,就会将软引用对象加入到引用队列中,我们在把这些软引用对象删除。
/**
* 演示软引用, 配合引用队列
*/
public class Demo2_4 {
private static final int _4MB = 4 * 1024 * 1024;
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());
}
}
}
:::info
弱引用
:::
弱引用是一种比软引用更弱的引用类型。与软引用一样,当一个对象只被弱引用指向时,它在垃圾回收时可能会被回收。即使系统内存充足,只要没有强引用指向对象,垃圾回收器就有可能回收被弱引用指向的对象。
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
public static void main(String[] args) {
// 创建一个对象并赋予弱引用
Object object = new Object();
WeakReference<Object> weakRef = new WeakReference<>(object);
// 获取弱引用指向的对象
Object referencedObject = weakRef.get();
// 输出对象是否还存在
System.out.println("Object exists: " + (referencedObject != null));
// 解除强引用
object = null;
// 弱引用指向的对象可能在下一次垃圾回收时被回收
System.gc(); // 不建议显式调用 System.gc(),这里仅作示例
// 获取弱引用指向的对象
referencedObject = weakRef.get();
// 输出对象是否还存在
System.out.println("Object exists after garbage collection: " + (referencedObject != null));
}
}
:::info
虚引用
:::
虚引用是最弱的一种引用关系,几乎没有直接的作用。虚引用的存在主要是为了在对象被垃圾回收时收到系统通知。如果一个对象经持有虚引用,那么他就和没有引用一样,随时可能会被垃圾回收。
虚引用通常用于监控对象被垃圾回收的时机,以便在对象被回收之前执行一些清理或资源释放的操作。虚引用是通过 PhantomReference 类实现的。
以下是虚引用的主要特性和使用方法:
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
public static void main(String[] args) {
Object object = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(object, referenceQueue);
// 解除强引用
object = null;
// 虚引用被创建后,将会被立即加入到 referenceQueue 中
Reference<? extends Object> reference = referenceQueue.poll();
if (reference != null) {
System.out.println("PhantomReference enqueued.");
}
// 垃圾回收器运行时,虚引用将被回收并加入 referenceQueue 中
System.gc(); // 不建议显式调用 System.gc(),这里仅作示例
// 从 referenceQueue 中获取被回收的虚引用
Reference<? extends Object> clearedReference = referenceQueue.poll();
// 如果 clearedReference 不为 null,则表示虚引用已经被垃圾回收
if (clearedReference != null) {
System.out.println("PhantomReference has been cleared.");
} else {
System.out.println("PhantomReference is still reachable.");
}
}
}
直接学的直接内存回收使用的就是虚引用,当ByteBuffer对象被回收时,引用队列就会得到通知然后通过Unsafe类释放内存。
:::info
终结器引用
:::
终结器引用指的是对象的中介方法即finalize()
方法被调用的引用。在Java中,每个对象都可以有一个与之相关联的终结器方法,这个方法会在对象被垃圾回收之前被调用。
当垃圾回收器准备回收一个对象时,首先会调用该对象的 finalize() 方法,如果该对象的 finalize() 方法内部没有重新引用对象(通过将自身赋值给某个类变量或实例变量),那么这个对象会被认为是可回收的。
public class FinalizerReferenceExample {
// 一个类带有终结器方法 finalize()
static class MyObjectWithFinalizer {
@Override
protected void finalize() throws Throwable {
System.out.println("Finalize method called.");
}
}
public static void main(String[] args) {
// 创建一个带有终结器方法的对象
MyObjectWithFinalizer myObject = new MyObjectWithFinalizer();
// 将对象设置为 null,让它变为不可达
myObject = null;
// 强制调用垃圾回收
System.gc(); // 不建议显式调用 System.gc(),这里仅作示例
// 等待一段时间以确保 finalize 方法执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
标记-清除算法:是一种最基本的垃圾回收算法。用于在程序中找到不再被引用的对象并释放其占用的内存。
优点和缺点:
它的清除是直接记录下来被清除对象的地址和大小,这些回收的内存块都是断断续续的,如果有更大的对象要进内存的话很可能会找不到合适大小的内存块。
它比标记清除算法多了一个操作,就是标记完可达对象后,将所有可达对象向一端移动,然后清理掉不可达对象,从而在另一端形成连续的内存块可以使用。解决了内存碎片的问题。
复制算法将堆内存分为两个区域,通常称为From区和To区。在垃圾回收过程中,所有存活的对象会从From空间被复制到To空间,然后清理From空间,最终实现内存的紧凑排列。
主要步骤:
分代垃圾回收是一种垃圾回收策略,根据对象的生命周期将堆内存划分为不同的代,不同代采用不同的垃圾回收算法。通常分为三代:新生代、老年代、永久代/元空间。
优点:
新手代的垃圾回收主要采用复制算法,这是因为新生代的对象生命周期较短,大部分对象在创建后很快就不再被引用。
Full GC 是一种全堆垃圾回收,它不仅包括老年代的回收,还会对新生代进行回收。 Full GC 通常是一种比较耗时的操作,因为它需要扫描整个堆内存,并清理所有的存活对象,包括新生代和老年代。
老年代的垃圾回收通常使用的是标记清理算法或者标记整理算法。
Full GC 通常发生在一下情况之一:
“Stop the world” 是指垃圾回收过程中,应用程序的所有线程都被暂停,即停止运行,一边垃圾回收器可以安全地执行工作而不受应用程序地干扰。还有就是垃圾回收过程中对象地内存地址会发生变化,如果程序继续执行,可能会报错。 我们之前所说的停顿时间就是只指应用程序被暂停执行的时间。
在Java中,垃圾回收过程中的停顿时间是由于某些垃圾回收算法需要在整个堆内存上执行操作,而这个操作需要停止所有应用程序线程。主要的停顿时间发生在老年代的垃圾回收,特别是在进行Full GC
时。
停顿时间会影响应用程序的性能和响应时间,尤其是对于对实时性要求高的应用。
/**
* 演示内存的分配策略
*/
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;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
}
}
我们设置内存大小为20M,新生代大小为10M
Heap
def new generation total 9216K, used 2228K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 27% used [0x00000000fec00000, 0x00000000fee2d0f8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3319K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
此时Eden区已经不到7M了,我们现在添加一个7M的对象到堆中,肯定会触发垃圾回收。
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
然后我们看打印信息:
出发了垃圾回收,新手代中的一些对象被回收了,有些对象被加入到From区了,7M对象被加入到了Eden区。
[GC (Allocation Failure) [DefNew: 2064K->692K(9216K), 0.0008704 secs] 2064K->692K(19456K), 0.0009013 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8106K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
from space 1024K, 67% used [0x00000000ff500000, 0x00000000ff5ad330, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3320K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
当添加的对象为8M时,会直接加入到老年代中,因为新生代的Eden区只有8M,但是新添加的对象比8M多,新生代就放不下了,所以直接放到老年代中。
Heap
def new generation total 9216K, used 2228K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 27% used [0x00000000fec00000, 0x00000000fee2d0f8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3319K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
当我们在添加一个8M的对象时,我们发现都放不下了了,就会出现堆内存溢出。
[GC (Allocation Failure) [DefNew: 2064K->692K(9216K), 0.0015750 secs][Tenured: 8192K->8883K(10240K), 0.0015420 secs] 10256K->8883K(19456K), [Metaspace: 3313K->3313K(1056768K)], 0.0031908 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 8883K->8866K(10240K), 0.0017135 secs] 8883K->8866K(19456K), [Metaspace: 3313K->3313K(1056768K)], 0.0017266 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8866K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffea8938, 0x00000000ffea8a00, 0x0000000100000000)
Metaspace used 3345K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 364K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at cn.itcast.jvm.t2.Demo2_1.main(Demo2_1.java:27)
:::info
线程出现堆内存溢出会导致整个线程结束吗
:::
答案是不会
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000000L);
}
Serial(串行) 收集器是一个单线程收集器,它的单线程不仅仅意味着它只会使用一条垃圾回收线程去完成垃圾回收工作,而且它在进行垃圾回收的时候必须暂停其他工作线程(STW),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法
ParNew 收集器其实就是 Serial 收集器的多线程版本,其他完全一样。
Parallel Scavenge 收集器 关注点是吞吐量(高效率的利用CPU),所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
新生代采用标记-复制算法,老年代采用标记-整理算法
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old
CMS收集器重点关注响应时间。主要目标是减少应用程序的停顿时间,并实现了让垃圾回收线程和用户线程同时工作。对新生代和老生代使用的都是 标记-清除算法。
步骤:
从上面的步骤我们可以看出: CMS 之所以能极大地降低 GC 停顿时间,本质上是将原本冗长的引用链扫描进行切分。通过 GC 线程与用户线程并发执行,加上重新标记校正的方式,减少了垃圾回收的时间。
缺点:
G1回收器是一种面向服务端应用的回收器,适用于需要较短停顿时间和高吞吐量的场景。
G1 对于 Java 堆的划分
G1回收器对于Java堆的划分不同于以往,之前都会划分为:新生代和老年代,新生代又划分为Eden区和Survivor区,Survivor区又分为from去和to区。
但是现在,G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆空间划分为多个大小相等的独立区域(Region),每个Region都可以称为 Eden空间 ,Survivor空间,老年代空间。
这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回首集来进行回收,衡量标准不再是它属于哪个代,二十哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的Mixed GC模式。
Region还有一类特殊的 Humongous 区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。如果是那些超过了整个Region容量的超大对象,将会放在连续 N 个 Humongous Region区域。
:::info
G1 回收器垃圾回收过程
:::
G1 GC 的垃圾回收过程主要包括以下三个环节:
(如果需要,单线程 、独占式、高强度的 Full GC 还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收)
顺时针young gc -> young gc + concurrent mark-> Mixed GC顺序,进行垃圾回收
:::info
记忆集和写屏障
:::
一个对象被不同区域引用的问题:
解决办法
:::info
卡表
:::
卡表是在分代垃圾回收算法中用于跟踪对象引用裱花的数据结构之一,其中包括G1回收器。卡表主要用于优化老年代与新生代之间的引用关系的追踪。
基本原理如下:
:::info
为啥要扫描老年代,而不是根据引用地址直接去找
:::
在垃圾回收的过程中,为了确保对象的存活状态,垃圾回收需要便利并标记所有存活的对象。直接根据引用地址去找对象的存活状态是可能的,但这种方式通常会导致一些问题,特别是在并发场景下。
在并发垃圾回收过程中,应用程序可能在垃圾回收的同时进行,如果直接根据引用地址去找对象,可能会出现并发修改对象状态的问题。通过扫描整个区域,垃圾回收器可以在不干扰应用程序执行的情况下获取一致的快照。
:::info
记忆集为啥记录老年代指向新手代的对象的引用,而不是新生代指向老年代
:::
在分代垃圾回收中,新手代的对象比老年代的对象更容易被回收。如果在对新生代执行垃圾回收时,我们还需要去老年代执行扫描,看有没有老年代的对象引用了新生代对象,给新生代对象标记为存活。所以说,为了提高效率,不让每次对新生代回收时都要扫描老年代,我们可以使用记忆集来记录哪些老年代对于引用了新生代对象。这样就可以不用去扫描老年代,而直接去扫描记忆集,从而提高了垃圾回收的效率。
:::info
Remark
:::
三色标记法:
:::info
写屏障
:::
G1垃圾回收器在其并发标记阶段使用了一种称为卡表的数据结构来实现写屏障。G1的写屏障的作用实在对象引用发生写操作时,通知垃圾回收器有关对象引用的变化,一边保持垃圾回收器的内部数据结构的一致性。
分为两种情况:
:::info
G1 回收器有哪些优化?
:::
字符串去重:
String s1 = new String("hello"); char[]{'h','e','l','l','o'}
String s2 = new String("hello"); char[]{'h','e','l','l','o'}
垃圾回收器会将所有新分配的字符串放入一个队列中,当新生代回收时,G1 并发检查是否有字符串重复,如果它们值一样,让它们引用同一个 char[],这样另一个就可以被回收了。
注意,与 String.intern() 不一样
并发标记类卸载:
所有对象都经过并发标记后,就能知道那些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
垃圾回收调优是一个重要的人物,它可以对应用程序的性能和内存利用率产生显著影响。在Java 中,有多种垃圾回收器和调优选项可供选择。以下是一些常见的 GC 调优策略。
:::info
最快的 GC 是不发生 GC
:::
查看 FullGC 前后的内存占用,考虑下面几个问题。
新生代是 Java 堆中的一部分,主要用于存放刚刚被创建的对象。
特点如下:
:::info
TLAB
:::
TLAB 是 Java 虚拟机中一种用于优化对象分配地技术。它在多线程环境下提高了对象分配的效率。
在 Java 中,对象地分配通常是线程共享的,所有线程都可以访问共享的堆内存区域。然而,为了减少线程之间的竞争和提高分配效率,Java 虚拟机引入了 Thread-Local Allocation Buffer
。
主要特点和作用包括:
:::info
新生代越大越好吗?
:::
如果新生代太小,那么就会导致 Minor GC
次数更加频繁,导致吞吐量变小。
如果新生代太大,那么就会导致老年代越小,老年代越小,那么触发 Full GC
的次数就越多,导致回收的时间就会越长,吞吐量变小。
Oracle 建议新生代大小在 25%-50%之间。
:::info
幸存区大小如何设置
:::
幸存区的大小会影响对象晋升到老年代的频率,如果幸存区过小,可能导致对象频繁晋升到老年代,增加 Full GC
的频率。也延长了对象的生存时间,本应该在新生代中就被回收的对于却等在老年代触发 Full GC
的时候回收。
一方面我们需要存活时间短的对象留在新生代以便下次垃圾回收,一方面我们也希望存活时间长的对象尽快晋升到老年代中。
这些存活时间长的对象会占用新生代的空间,在新生代垃圾回收时也要将这些对象进行复制,消耗了更多的资源。
:::info
老年代大小如何设置
:::
老年代是Java堆中的一部分,主要用于存放经过多次垃圾回收仍然存活的对象。老年代的调优涉及到一些关键的因素,包括垃圾回收器的选择、老年代大小的设置、Full GC的频率等。以下是一些建议的老年代调优策略:
-Xms
、-Xmx
等选项,可以设置Java堆的初始大小和最大大小,从而影响老年代的大小。合理设置老年代的大小有助于避免OutOfMemoryError和提高垃圾回收的效率。-XX:MaxTenuringThreshold
选项来设置对象晋升到老年代的年龄阈值。增大年龄阈值有助于减少对象频繁晋升到老年代。我们来看几个案例:
Full GC 和 Minor GC 频繁是为什么?
因为新生代的内存设置太小,导致新手代发生 Minor GC 就频繁,新生代垃圾回收频繁就会导致有更多的短生命周期对象晋升到老年代,老年代中内存一多,就会触发 Full GC。所以我们需要先调整新生代的大小,来逐步改善。