堆
:对于一个JVM进程来说是堆唯一的,但是一个JVM进程可以包含多个线程,是所以这些线程共享同一个堆空间。
下图中线程间共享
的有:方法区(Method Areas)和堆(Heap)
每个线程独有
的是:程序计数器(PC Register),本地方法栈(Native Method Stack)和虚拟机栈(Java Virtual Machins Stack)
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间
。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区
(Thread Local Allocation Buffer,TLAB),注意:并不是所有堆空间都是共享的,TLAB为线程私有
-Xms10m:最小堆内存
-Xmx10m:最大堆内存
下图就是使用:Java VisualVM
查看堆空间的内容,如果是JDK1.8版本该工具就在bin目录下,JDK1.8以上的版本就要自己下载了,下载地址
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除,也就是触发了GC的时候,才会进行回收。
如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
新生区
Young/New
养老区
Old/Tenure永久区
PermJava 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
新生区
Young/New
养老区
Old/Tenure元空间
Meta约定:新生区<-> 新生代 <-> 年轻代
、 养老区 <-> 老年区 <-> 老年代
、 永久区 <-> 永久代
堆空间内部结构,JDK1.8之前从永久代 替换成 元空间
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项-Xmx
和-Xms
来进行设置。
-Xms
:用于表示堆区的起始内存,等价于-xx:InitialHeapSizeXmx
:则用于表示堆区的最大内存,等价于-XX:MaxHeapSize一旦堆区中的内存大小超过-Xmx
所指定的最大内存时,将会抛出outofMemoryError
异常。
通常会将
-Xms和-
Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
。
默认情况下
/**
* 1.设置对空间大小参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -X:是jvm运行参数
* ms:memory start
* -Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
*
* 2.默认堆空间大小
* 初始内存大小:物理电脑内存大小/64
* 最大内存大小:物理电脑内存大小/4
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
// 返回Java虚拟机中的堆内存总量(字节)
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机试图使用的最大堆内存
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
System.out.println("系统内存大小为:"+initialMemory*64.0/1024+"G");
System.out.println("系统内存大小为:"+maxMemory*4/1024+"G");
}
}
输出结果:
-Xms:54M
-Xmx:784M
系统内存大小为:3.375G
系统内存大小为:3G
方式一:
/**
* -Xms10m -Xmx10m
*/
public class HeapDemo01 {
public static void main(String[] args) {
System.out.println("start....");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end....");
}
}
方式二:使用这个命令-XX:+PrintGCDetails
/**
* -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class HeapDemo01 {
public static void main(String[] args) {
System.out.println("start....");
System.out.println("end....");
}
}
/**
* 演示堆内存溢出:OOM
* VM options: -Xms100m -Xmx100m
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list=new ArrayList<>();
while(true){
//在此处设置一个线程休眠,为了便于在jvisualvm中观察GC线程情况
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024*1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels=new byte[length];
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.aismall.demo.Picture.<init>(OOMTest.java:29)
at com.aismall.demo.OOMTest.main(OOMTest.java:20)
存储在JVM中的Java对象可以被划分为两类:
Java堆区进一步细分的话,可以划分为年轻代
(YoungGen)和老年代
(oldGen)
其中年轻代又可以划分为Eden
空间、Survivor0
空间和Survivor1
空间(有时也叫做from区
、to区
)
下面这些参数开发中一般不会调:
Eden:From:to <-> 8:1:1
新生代:老年代 <- > 1 : 2
配置新生代与老年代在堆结构的占比:
默认:-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改:-XX:NewRatio=4
,表示新生代占1,老年代占4,新生代占整个堆的1/5
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整 老年代的大小,来进行调优
配置新生代中Eden空间和另外两个survivor空间的占比:
-XX:SurvivorRatio=8
,表示在Eden空间占8,survivor0空间占1,survivor1空间占1-XX:SurvivorRatio=4
,表示在Eden空间占4,survivor0空间占1,survivor1空间占18:1:1
,我们可以再VM options
中关闭自适应内存分配策略:-XX:-UseAdaptiveSizePolicy
几乎所有的Java对象都是在Eden区被new出来的
。绝大部分的Java对象的创建和销毁
都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)
为新对象分配内存是一件非常严谨和复杂的任务,JM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
MinorGC
),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区-Xx:MaxTenuringThreshold= N
进行设置我们创建的对象,一般都是存放在Eden区的,当我们Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作
当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,一次回收后幸存下来就是1。
同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象 进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1
我们继续不断的进行对象生成 和 垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion晋升的操作,也就是将年轻代中的对象晋升到老年代中
注意:from区和头区不是固定不变的,在一次回收的时候向那个区存放幸存者,那个区就是to区
。
特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作
如果Survivor区满了后,将会触发一些特殊的规则,也就是可能
直接晋升老年代
/**
* 演示堆内存溢出:OOM
* VM options: -Xms100m -Xmx100m
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list=new ArrayList<>();
while(true){
//在此处设置一个线程休眠,为了便于在jvisualvm中观察GC线程情况
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024*1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels=new byte[length];
}
}
-Xms100m -Xmx100m
jvisualvm
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.aismall.demo.Picture.<init>(OOMTest.java:29)
at com.aismall.demo.OOMTest.main(OOMTest.java:20)
Minor GC:新生代的GC
Major GC:老年代的GC
Full GC:整堆收集,收集整个Java堆和方法区的垃圾收集
垃圾收集是JVM调优的一个环节,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(Stop the world)的问题
而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上
JVM在进行GC时,并非每次都对上面三个内存区域 (新生代,老年代,方法区【永久带或者元空间】) 一起回收的,大部分时候回收的都是指新生代。
针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集
(Partial GC),一种是整堆收集
(FullGC)
部分收集
:不是完整收集整个Java堆的垃圾收集。其中又分为:
单独收集
老年代的行为。注意,很多时候Major GC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收
。整堆收集
(Full GC):收集整个java堆和方法区的垃圾收集。
当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden区满,Survivor满不会引发GC
。(每次Minor GC会清理年轻代的内存。)
因为Java对象大多都具备 朝生夕灭
的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
Minor GC会引发STW
,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
指发生在老年代的GC,对象从老年代消失时,我们说 Major Gc
或 Full GC
发生了
出现了MajorGc,经常会伴随至少一次的Minor GC
(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
触发Full GC执行的情况有如下五种:
System.gc()
时,系统建议执行Full GC,但是不必然执行说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
举个栗子:GC
/**
* GC测试:Minor GC Major GC Full GC
* VM options参数: -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "AISMALL";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Exception e) {
e.getStackTrace();
}
}
}
为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同,70%-99%的对象是临时对象
。
其实不分代完全可以,分代的唯一理由就是优化GC性能
。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储朝生夕死
对象的区域进行回收,这样就会腾出很大的空间出来。
如果对象在Eden出生并经过第一次Minor GC
后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
-XX:MaxTenuringThreshold
来设置针对不同年龄段的对象分配原则如下所示:
动态对象年龄判断
无须等到
MaxTenuringThreshold 中要求的年龄。空间分配担保: -XX:HandlePromotionFailure
TLAB:Thread Local Allocation Buffer,也就是为每个线程单独分配了一个缓冲区
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略
。
据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
。
在程序中,开发人员可以通过选项-XX:UseTLAB
设置是否开启TLAB空间。
默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
不一定,因为还有TLAB这个概念,在堆中划分出一块区域,为每个线程所独占
测试堆空间常用的jvm参数:
-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
-XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志
-XX:HandlePromotionFalilure:是否设置空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
。
检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
。
在JDK7之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略
,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则
变为只要老年代的连续空间【大于新生代对象总大小】或者【历次晋升的平均大小】就会进行Minor GC,否则将进行FullGC。
通过上面的规则
我们可以认为更新后HandlePromotionFailure参数默认为True
所有的对象实例都是创建在堆上
。逃逸分析小结:逃逸分析并不成熟
中我们提到了,oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上
。以下均为了解内容
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
逃逸分析技术
逐渐成熟,栈上分配
、标量替换优化技术
将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对
了。在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识
。但是,有一种特殊情况
,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
此外,前面提到的基于openJDk深度定制的TaoBaovm,其中创新的GCIH(GC invisible heap)技术实现off-heap(堆外),将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域
:
例如作为调用参数传递到其他地方中
。public void my_method() {
V v = new V();
// use v
// ....
v = null;
}
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
/**
* 逃逸分析
* 如何快速的判断是否发生了逃逸分析,大家就看new的对象是否在方法外被调用。
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
/**
* 方法返回EscapeAnalysis对象,发生逃逸
* @return
*/
public EscapeAnalysis getInstance() {
return obj == null ? new EscapeAnalysis():obj;
}
/**
* 为成员属性赋值,发生逃逸
*/
public void setObj() {
this.obj = new EscapeAnalysis();
}
/**
* 对象的作用于仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis() {
EscapeAnalysis e = new EscapeAnalysis();
}
/**
* 引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis2() {
EscapeAnalysis e = getInstance();
// getInstance().XXX 发生逃逸
}
}
逃逸分析参数设置
逃逸分析结论
使用逃逸分析,编译器可以对代码做如下优化:
栈上分配
:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配同步省略
:如果发现一个对象只被一个线程访问到,那么对于这个对象的操作可以不考虑同步。分离对象或标量替换
:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的不可栈上分配的场景
测试栈上分配(开启逃逸才会考虑栈上分配):
/**
* 栈上分配
* VM options参数:
* 关闭逃逸:
* -Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
* 开启逃逸:
* -Xmx256m -Xms256m -XX:+PrintGCDetails -XX:+PrintGCDetails
*/
class User {
private String name;
private String age;
private String gender;
private String phone;
}
public class StackAllocation {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
Thread.sleep(10000000);
}
private static void alloc() {
User user = new User();
}
}
我们不开启逃逸的时候
-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 65536K->728K(76288K)] 65536K->736K(251392K), 0.0053295 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 66264K->696K(76288K)] 66272K->704K(251392K), 0.0027422 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
花费的时间为:54 ms
我们开启逃逸的时候
-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
花费的时间为:15 ms
想更具体的观测程序运行后内存的占用情况,可以使用Visual VM工具
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程
。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略
,也叫锁消除
。
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
标量
(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量
(Aggregate),Java中的对象就是聚合量
,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
分离对象
class Point {
private int x;
private int y;
public Point(int x,int y){
this.x=x;
this.y=y;
}
}
public static void main(String args[]) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x" + point.x + ";point.y" + point.y);
}
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x = " + x + "; point.y=" + y);
}
代码优化之标量替换
-XX:+EliminateAllocations
,开启标量替换,默认打开,允许将对象打散分配到栈上-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟的。
其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程
。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段
。
注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代
。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上
。
年轻代
是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
老年代
放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。
当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。
当GC发生在老年代时则被称为MajorGc或者FullGC。一般的,MinorGc的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。