JVM学习03:垃圾回收
1、如何判断对象可以回收
1.1、引用计数法
- 记录当前对象被引用的次数,当引用次数为0时则进行垃圾回收。
- 缺点:当两个对象互相引用但并没有其他对象再引用它们时,他们的引用次数都为1,无法对其进行回收释放。如图所示:
- 早期的python使用这种方法,但是java而是采用可达性分析算法。
1.2、可达性分析算法
-
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
-
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
-
哪些对象可以作为 GC Root ?
- System Class:系统类,例如Object类、HashMap类等。
- Native Stack:本地方法栈中引用的java对象。
- Thread:活动线程中使用的对象,即栈帧中的局部变量等引用的对象。
- Busy Monitor:所有被同步锁(synchronized关键字)持有的对象。
1.3、四种引用
注意:图中实现为强引用,虚线为其他引用。
强引用
- 只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收。
- 例如图中A1对象,当B、C对象都不引用它时才会被垃圾回收。
软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会回收软引用引用的对象,回收软引用对象可以配合引用队列来释放软引用自身占用的内存。
- 例如图中A2对象,当B对象不再引用A2对象,并且垃圾回收后内存不足时,软引用所引用的A2对象会被回收。但是软引用自身没有被清理,可以使用引用队列进行处理,释放软引用占用的内存。
测试代码1:
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class demo02 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
soft();
}
// list --> SoftReference --> byte[]
public static void soft() {
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());
}
}
}
测试结果:
可以看出,软引用对象(4MB的byte数组)在垃圾回收后,内存不足时进行清理。
测试代码2:引用队列
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用, 配合引用队列
*/
public class demo03 {
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());
}
}
}
测试结果:
使用引用队列后,原来结果中的软引用对象(4个null)被去除掉了。
弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用引用的对象,可以配合引用队列来释放弱引用自身占用的内存。
- 例如图中A3对象,当B对象不再引用A3对象,只要进行垃圾回收,A3对象就会被清理。和软应用一样,可以使用引用队列释放弱引用自身占用的内存。
测试代码:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class demo04 {
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);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
测试结果:
当垃圾回收时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用(PhantomReference)
-
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。
-
例如图中的ByteBuffer对象,当ByteBuffer对象实力创建时,会创建一个虚引用对象Cleaner来引用它,这时会分配一块直接内存,并且会把直接内存地址传递给Cleaner对象,当ByteBuffer对象被清理时,虚引用对象Cleaner会放入引用队列,当 ReferenceHandler 线程监测到有对象进入队列时,会调用相关方法释放直接内存。(上一节直接内存中讲过)
终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象。
- 例如图中的A4对象。所有的类都继承自Object类,Object类有一个finalize方法。当A4对象重写了finalize()方法后,此对象不再被其他的对象所强引用时,JVM会创建一个终结器引用,垃圾回收时会先将终结器引用对象放入引用队列中,然后Finalizer 线程会查看引用队列,若其中有终结器引用,则会通过终结器引用找到它所引用的对象并调用该对象的finalize方法。调用以后,再进行一次GC,该对象才会被垃圾回收。
- 不推荐使用finalize()方法进行内存释放,其条件太复杂了。
2、垃圾回收算法
2.1、标记清除
定义: Mark Sweep
-
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
-
注意:这里的清理出来的内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始和结束地址,下次分配内存的时候,会直接覆盖这段内存。
-
优点:速度较快。
-
缺点:会造成内存碎片。
2.2、标记整理
定义:Mark Compact
-
先采用标记算法确定可回收对象,然后将让所有存活的对象都向内存空间一端移动,最后直接清理掉边界以外的内存。
-
优点:没有内存碎片。
-
缺点:速度慢。
2.3、复制
定义:Copy
-
它将可用内存按容量划分为大小相等的两个区域:from区和to区。当from内存满了的时候,首先标记存活的对象,然后把存活的对象从from区复制到to区,然后将from区清空,最后交换from区和to区。
-
优点:不会有内存碎片。
-
缺点:需要占用双倍内存空间。
3、分代垃圾回收
-
对象首先分配在伊甸园区域。
-
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1,并且交换 from to。
-
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
-
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
-
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长。
-
新生代一般采用复制算法,老年代一般使用标记整理算法。
-
如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。
3.1、相关 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 |
测试代码:
package com.jvm.lesson03;
import java.util.ArrayList;
/**
* 演示内存的分配策略
*/
public class demo05 {
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 {
/*ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);*/
//当一个线程抛出OOM异常后,不会影响其他线程的运行。
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);//如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
结果分析:
- 如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。
- 当一个线程抛出OOM异常后,不会影响其他线程的运行。
4、垃圾回收器
这部分结合书《深入理解java虚拟机第三版》看。
4.1、串行
- 单线程
- 堆内存较小,适合个人电脑
- 参数:
-XX:+UseSerialGC
:开启 Serial + SerialOld 收集器。
- 串行的收集器比如 Serial 和 SerialOld 收集器。Serial收集器工作在新生代,采用复制算法;SerialOld收集器工作在老年代,采用标记整理算法。
4.2、吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高。
- 参数:
-XX:+UseParallelGC
~-XX:+UseParallelOldGC
:分别为开启新生代和老年代的并行垃圾回收器,开启一个另一个自动开启。-XX:+UseAdaptiveSizePolicy
:使用自适应的调整策略,来调整新生代的大小、伊甸园区和幸存去的比例、晋升老年代的阈值等。XX:GCTimeRatio=ratio
:根据 1/(1+ratio)(垃圾收集时间占总时间的比率) 尝试调整堆的大小,进而调整吞吐量的大小。例如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19))。-XX:MaxGCPauseMillis=ms
:最大垃圾回收暂停的毫秒数,默认为200ms。-XX:ParallelGCThreads=n
:指定并行垃圾回收线程数,一般与CPU核数相同。
-
吞吐量:就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间)
-
吞吐量优先的收集器比如 Parallel Scavenge 和 Parallel Old 收集器。Parallel Scavenge收集器工作在新生代,采用复制算法;Parallel Old收集器工作在老年代,采用标记整理算法。
4.3、响应时间优先
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短。
- 参数:
-XX:+UseConcMarkSweepGC
:开启并发的使用标记清除算法的垃圾回收器。-XX:ParallelGCThreads=n
:设置并行的垃圾回收线程数,一般与CPU核数相同。-XX:ConcGCThreads=threads
:设置并发的垃圾回收线程数,一般设置为并行垃圾回收线程数的1/4。-XX:CMSInitiatingOccupancyFraction=percent
:设置CMS垃圾回收触发的百分比。当老年代使用的内存占比到了这个数值后才会触发立即回收,预留出一定的空间给其他用户线程和浮动垃圾(并发清理时新产生的垃圾)。-XX:+CMSScavengeBeforeRemark
:在重新标记阶段前对新生代进行一次垃圾回收,减少时间浪费。因为在重现标记阶段会扫描整个堆,从新生代查找其引用老年代的对象做可达性分析,但新生代的对象非常多且其中包含很多并发标记阶段产生的垃圾,这就造成浪费大量的时间查找无用的引用。
- 并行和并发的区别:
- 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。
- 响应时间优先的收集器例如CMS收集器,它工作在老年代,使用标记清除算法实现。一般在新生代配合ParNew(
-XX:+UseParNewGC
)收集器使用。由于CMS收集器在老年代使用的标记清除算法,会产生大量的内存碎片,当预留的内存无法满足程序分配新对象的需要时,导致并发失败,这时会使用 SerialOld 收集器作为补救措施。
4.4、G1
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms。
- 超大堆内存,会将堆划分为多个大小相等的Region,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
- 整体上是标记+整理算法,两个区域之间是复制算法。
相关 JVM 参数:
-XX:+UseG1GC
:开启G1收集器。
-XX:G1HeapRegionSize=size
:每个Region的大小。
-XX:MaxGCPauseMillis=time
:设定允许的收集停顿时间,默认为200ms。
4.1.1、G1垃圾回收阶段
4.2.2、Young Collection
- 会 STW
- 对象首先放到伊甸园区中,当伊甸园(E)的空间占满时,会触发Young Collection,将伊甸园的幸存对象以复制算法放入幸存区(S)。当幸存区的空间快要占满或幸存区对象年龄达到一定阈值,会再次触发Young Collection,幸存区的部分对象会晋升至老年代,年龄不够的会复制到另一块幸存区。
4.2.3、Young Collection + CM
-
在 Young GC 时会进行 GC Root 的初始标记(会 STW)。
-
老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定:
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
4.2.4、Mixed Collection
会对 E、S、O 进行全面垃圾回收。
-
最终标记(Remark)会 STW
-
拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms
:设定允许的收集停顿时间,默认为200ms。使得在有限的时间里优先回收老年代中回收价值较高的(垃圾多的)。
4.2.5、Full GC
-
SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足 - 当老年代使用的内存占比到了设定的百分比会进行并发收集。当内存碎片足够多时导致新的对象放入时内存不足,会造成并发失败,并发收集失败时退化为串行收集,此时才会进行full gc。
-
G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足 - 老年代占用堆空间比例达到阈值(默认45%)时,进行并发标记(不会 STW)与混合收集,当回收速度快于垃圾产生速度时会进行并发收集。只有当垃圾产生速度快于回收速度时并发收集失败,退化为串行收集,此时才会进行full gc。
4.2.6、Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题:
在进行Young Collection时,首先要找到GC Root根对象,根据其进行可达性分析找到存活的对象复制入幸存区。但根对象一部分是来自老年代的,且老年代的存活对象较多,对老年代进行遍历查找效率很低。因此采用卡表技术对老年代进行分区,每个card大小约为512k。若此card中有对象引用了新生代的对象,则将其标记为脏卡。这时新生代会有一个 Remembered Set
来存放被标记的脏卡,因此在Young Collection查找根对象时只需要根据 Remembered Set
来查找脏卡即可,提高效率。
在引用变更时通过 post-write barrier + dirty card queue
的方法来重新标记脏卡,这是一个异步操作, Remembered Set
可以通过concurrent refinement threads
来更新。
4.2.7、Remark(重新标记)
pre-write barrier
+satb_mark_queue
图中:黑色为已被处理的;灰色为正在处理中的;白色为还未处理的。
如果对象的引用发生改变,JVM就会给其加入写屏障pre-write barrier
,并执行写屏障指令,将C加入队列中 satb_mark_queue
,并把C标记为灰色。并发标记结束后,进入重新标记阶段,此时会STW,并将satb_mark_queue
中的对象一个个取出来进行检查,如此对象有强引用引用时,将其修改为黑色,进行对象保留,否则被修改成白色进行清理。
4.2.8、JDK 8u20 字符串去重
-
优点:节省大量内存。
-
缺点:略微多占用了 cpu 时间,新生代回收时间略微增加。
-
参数:
-XX:+UseStringDeduplication
(默认是打开的)
//连个字符串对象引用同一个字符数组
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()
不一样。String.intern()
关注的是字符串对象。- 而字符串去重关注的是字符串对象引用的 char[]。
-
在 JVM 内部,使用了不同的字符串表。
4.2.9、JDK 8u40 并发标记类卸载
-
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器(一般为自定义加载器)的所有类都不再使用,则卸载它所加载的所有类。
-
参数:
-XX:+ClassUnloadingWithConcurrentMark
(默认启用)
4.2.10、JDK 8u60 回收巨型对象
-
一个对象大于 region 的一半时,称之为巨型对象。
-
G1 不会对巨型对象进行拷贝。
-
回收时被优先考虑。
-
G1 会跟踪老年代所有 incoming 引用(脏卡引用巨型对象的次数),这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉。
4.2.11、JDK 9 并发标记起始时间的调整
-
并发标记必须在堆空间占满前完成,否则退化为 FullGC。
-
JDK 9 之前需要使用:
-XX:InitiatingHeapOccupancyPercent
(默认45%) -
JDK 9 可以动态调整:
-XX:InitiatingHeapOccupancyPercent
用来设置初始值。- 进行数据采样并动态调整。
- 总会添加一个安全的空档空间。
4.2.12、JDK 9 更高效的回收
-
250+增强
-
180+bug修复
-
https://docs.oracle.com/en/java/javase/12/gctuning
5、垃圾回收调优
预备知识:
-
掌握 GC 相关的 VM 参数,会基本的空间调整。
查看虚拟机的运行参数:
"java的路径" -XX:+PrintFlagsFinal -version | findstr "GC"
-
掌握相关工具:jmap、jconsole、VisualVM等。
-
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则。
5.1、调优领域
-
内存
-
锁竞争
-
cpu 占用
-
io
5.2、确定目标
-
【低延迟】还是【高吞吐量】,选择合适的回收器。
-
低延迟的虚拟机:CMS,G1,ZGC
-
高吞吐量的虚拟机:ParallelGC
-
其他的的虚拟机:Zing
5.3、最快的 GC
答案是不发生 GC。
查看 FullGC 前后的内存占用,考虑下面几个问题:
-
数据是不是太多?
- 数据库里读取数据:
resultSet = statement.executeQuery("select * from 大表 limit n")
。可以后面加上limit n
。
- 数据库里读取数据:
-
数据表示是否太臃肿?
- 对象图
- 对象大小 :比如Integer占24个字节,int占4个字节,尽量使用基本类型。
-
是否存在内存泄漏?
- 把数据全放入一个map中,垃圾得不到即使清理,会导致内存泄漏。
- 可以使用软引用、弱引用或者第三方缓存实现,比如redis。
5.4、新生代调优
-
新生代的特点:
-
所有的 new 操作的内存分配非常廉价。
- TLAB(thread-local allocation buffer):线程分布局部缓冲区,作用是让每个线程用自己私有的内存进行对象分配,因此多个线程同时创建对象时不会产生内存冲突。
-
死亡对象的回收代价是零(一般使用复制算法)。
-
大部分对象用过即死。
-
Minor GC 的时间远远低于 Full GC。
-
-
新生代内存越大越好吗?
-Xmn参数:
设置新生代的初始和最大内存。在这个区域执行GC的频率要高于其他区域。如果新生代的内存太小,那么就会执行大量的minor GC。如果内存太大,那么就会执行full GC,full GC可能需要很长的时间来完成。Oracle建议你将年轻一代的大小保持在整个堆大小的25%以上,50%以下。
补充:吞吐量随着新生代内存增大先增大再减小,一开始内存增大后,垃圾回收频率变低,吞吐量增加;到了一定阶段,内存太大时,垃圾回收占的时间也增加,此时吞吐量开始减小。
-
新生代内存估算:【并发量 * (请求-响应)】(每个请求相应占用的内存量*并发数)。
-
幸存区大到能保留**【当前活跃对象+需要晋升对象】**。幸存区太小会导致一部分存活时间较短的对象晋升至老年代,老年代对象full gc时才会回收,进而造成内存浪费。
-
晋升阈值配置得当,让长时间存活对象尽快晋升,否则就会在幸存区反复被复制。
-XX:MaxTenuringThreshold=threshold
:设置最大晋升阈值。-XX:+PrintTenuringDistribution
:打印晋升区存活对象的详情。
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...
5.5、老年代调优
以 CMS 为例:
-
CMS 的老年代内存越大越好。预留更多的空间,避免浮动垃圾造成并发失败。
-
先尝试不做调优,如果没有 Full GC ,那先尝试调优新生代。
-
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3。
-
-XX:CMSInitiatingOccupancyFraction=percent
:设置CMS垃圾回收触发的百分比。
5.6、案例
案例1:Full GC 和 Minor GC 频繁。
可能是新生代内存太小引发的Minor GC 频繁,可以尝试增大新生代的内存大小;然后可以适当的增大晋升老年代的阈值,防止Full GC频繁。
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)。
CMS垃圾回收器在初始标记和并发标记消耗的时间较短,但是重新标记消耗的时间较长,因为他需要重新扫描老年代和新生代。可以使用-XX:+CMSScavengeBeforeRemark
参数在重新标记阶段前对新生代进行一次垃圾回收,减少时间浪费。
案例3:老年代充裕情况下,发生 Full GC(CMS jdk1.7)。
可能是永久代内存不足导致的Full GC。在 JDK 1.7 及以前,JVM中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。