垃圾回收与内存分配
垃圾收集器与内存分配策略
概述
我们先提出三个问题:
哪部分内存需要回收?
什么时候进行回收?
如何进行回收?
生存还是死亡?
where
我们都知道在Java中,栈,本地方法区,程序计数器都是线程私有的,随着线程的创建和结束,内存也会自动的分配和销毁,执行的方法也随着栈帧的插入和弹出而创建和销毁。所以这部分区域我们不必担心。
而方法区(元空间)和堆空间,是线程共享的,这部分区域的内存就是我们需要去进行垃圾回收的区域。
when
什么时候进行垃圾回收,就需要我们判断这个对象是否仍被引用,如果没有一个指针指向它,那我们就可以放心的进行垃圾回收,如果仍被引用,那就不行(你杀根本干嘛,这人我正用着呢),关于对象引用判断分为两种:
引用计数法
就是说一个人引用,就对一个引用计数器加一,如果这个人不引用了,我们就减一,当引用计数器等于0的时候,我们就可以判定,可以被垃圾清理了。
该方法易理解,且实现简单。但一个缺点就是遇到循环依赖,不好处理。在主流的java虚拟机中都没有使用该方法进行判断。下面用代码进行说明。
class referenceCountingGC {
Object instance = null;
/**
* 这里创建一个字节数组只是为了,占一些内存,方便后面看GC情况。
*/
static final int _1mb = 1024*1024;
private byte[] bigSize = new byte[2*_1mb];
/**
* -XX:+PrintGCDetails
* 输出GC详情
* @param args
*/
public static void main(String[] args) {
referenceCountingGC A = new referenceCountingGC();
referenceCountingGC B = new referenceCountingGC();
A.instance = B;
B.instance = A;
A=null;
B=null;
/**
* [Full GC (System.gc())
* [PSYoungGen: 496K->0K(38400K)]
* [ParOldGen: 8K->424K(87552K)]
* 504K->424K(125952K
* ), [Metaspace: 3208K->3208K(1056768K)],
* 0.0135739 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
*
* 我们看到即使两个对象相互引用,但JVM还是进行回收了,侧面反映出JVM没有使用
* 引用技术法来判断对象是否存活
*/
System.gc();
}
}
可达性分析法
简单来说我们将对象之间相互的引用想象成一个树结构,其中根对象为GC roots
(有一系列的根对象),树结构中节点与节点之间形成链,当我们寻找一个对象时,从GC roots往下走,该路径为引用链
,如果一个对象无法在引用链中找到,就是不可达的,我们就对可以进行垃圾回收。
这里我们可以看到虽然object5,object6,object7之间相互引用,但是在引用链外,所以也是要判定为需要垃圾回收的。
有哪些可以对象可作为GC roots呢?
- 在虚拟机栈(局部变量表)中引用的对象,也就是当前方法正在使用的参数,局部变量,临时变量。
- 方法区中静态属性引用对象。(类的引用型静态变量)
- 方法区中常量引用对象。譬如字符串常量池中引用的对象。
- 本地方法栈中JNI(Native方法)引用对象
- Java虚拟机内部引用对象,如基本数据类型对应的Class对象,一些异常对象,类加载器对象。
- 被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存。
除了上述固定的GC Roots集合外,根据用户所选的垃圾回收器
以及当前回收的内容区域
不同,还有其他对象临时
加入,共同构建GC Roots集合。比如:分代收集,局部回收。
再谈引用
最初在JKD1.2版本之前,引用的概念就是引用与非引用
,但现在我们希望能够再细分一些,比如一些对象我们希望在内存足够时,你就继续待着,如果内存不够了,那我们就清理掉(这么看与裁员是一样的)。很多系统的缓存功能都符合这样的应用场景。
在JDK1.2后,Java对引用的概念进行扩充,分为强引用,软引用,弱引用,虚引用
- 强引用就是最原始的概念
Object o1 = new Object()
,只要强引用关系还在,就不会清除 - 软引用是指一些有用,但非必要的对象,在系统放生内存溢出前,将这些对象放入回收范围之中,进行第二次的回收,如果这次回收没有足够的内存,就会抛出内存溢出异常,java中使用
SoftReference类
实现软引用。 - 弱引用指分必要对象,只会撑到下次垃圾回收之前,无论内存够不够,都会清理,java中使用
WeekReference类
实现 - 虚拟用(幽灵引)一个对象有没有虚引用都不影响是否被清理,它的作用是指当清理时,系统收到一个通知。java中使用
PhantomReference类
实现
how
当进行可达性分析后,一个对象被判定为不可达,也不是非死不可,第一次判断完后,该对象被一次标记,现在可以算的上缓期
,随后再进行一次筛选,判断此对象是否有必要执行finalize()方法?
如果对象没有覆盖finalize()方法,或者finalize()方法已经被执行了,就判定为
没必要执行
如果该对象被判定为有必要执行执行finalize()方法,对象会被放到F-Queue
的队列中,随后一条由虚拟机创建的,低调低优先级的Finalizer线程会去执行他们的finalize()方法,这里的执行时指启动finalize()方法,并不一定等待到运行结束,因为如果finalize()执行缓慢,甚至发生死循环,则其他对象无法被删除,可能导致内存回收子系统的崩溃,finalize()是最后一次救自己的机会,随后还会有第二次标记,只要在这之前,将自己与引用链上重新关联,那再第二次标记时就会踢出即将回收的队列
,如果这个时候还没逃走,那就得被清理了。下面用代码演示:
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK=null;
public void isAlive() {
System.out.println("yes i am still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed !" );
//进行自救
FinalizeEscapeGC.SAVE_HOOK = this;
System.out.println("进行自救:"+FinalizeEscapeGC.SAVE_HOOK.hashCode());
}
public static void main(String[] args) throws InterruptedException {
FinalizeEscapeGC escapeGC = new FinalizeEscapeGC();
System.out.println("创建的对象:"+escapeGC.hashCode());
escapeGC = null;
//第一次成功救出自己
System.gc();
//因为 Finalizer方法优先级低,我们等一下
Thread.sleep(500);
if (FinalizeEscapeGC.SAVE_HOOK != null) {
FinalizeEscapeGC.SAVE_HOOK.isAlive();
} else {
System.out.println("i am dead");
}
//下面代码和上面一样
FinalizeEscapeGC.SAVE_HOOK = null;
System.gc();
//因为 Finalizer方法优先级低,我们等一下
Thread.sleep(500);
if (FinalizeEscapeGC.SAVE_HOOK != null) {
FinalizeEscapeGC.SAVE_HOOK.isAlive();
} else {
System.out.println("i am dead");
}
}
}
运行后我们发现第一次gc后,该对象还存活,因为第一次执行了finalize()方法时,我们将对象本身赋值给了类变量,但是当第二次gc时,对象就彻底清除了,因为finalize()只执行一次。
最后声明一点,不要使用finalize()来拯救对象,官网也不推荐,只是在这里让大家知道它是干嘛的就行了。
回收方法区
在《Java虚拟机规范》中并未要求堆方法区实现垃圾回收,所以有的虚拟机版本确实没有回收方法区,而且方法区回收也是比较苛刻的。
方法区垃圾回收主要回收两类:废弃的常量,类型信息。
常量的垃圾回收和上面的类似,主要判断一个类型是否不再被使用比较苛刻,需要满足下面三点:
- 该类所有实例,包括派生子类的实例都已经被回收。
- 加载该类的类加载被回收了。
- 该类对应的java.lang.Class对象没有被任何引用。
满组上面三点,也不一定被回收,下面介绍一些相关虚拟机参数:
- -Xnoclassgc 控制是否对类型回收。
- -verbose:class -XX:+TraceClassLoading查看类加载和卸载信息(需要Product版虚拟机)
- -XX:+TraceClassUnLoading(需要FastDebug版虚拟机)
什么时候需要回收方法区?
在大量使用反射,动态代理,CGLib等字节码框架,会动态创建很多类,通常都需要Java虚拟机具有类型卸载的能力。保证不会堆方法区有太大的压力
垃圾收集算法
在上面我们已经知道判定一个对象是否会被回收可通过引用计数式
和追踪式(可达性分析)
两种算法,而引用计数式在主流的垃圾回收策略中都没使用,所以我们下面讨论的还是追踪式算法。
第一个问题:我们每次都是直接对整个堆进行可达性分析和垃圾回收吗?
不可行,这会导致消耗大量的时间。大多数的垃圾回收都遵从“分代收集”的理论进行设计,而它是建立在两个分代假说上的。
> 若分代假说:绝大多数对象都是朝生夕死的
> 强分代假说:熬过越多次垃圾回收的对象,越难以消亡
这两个分代假说共同奠基了一个设计原则:收集器应该将Java堆划分出不同的区域。通俗的讲我们把新生对象
放在一起,把老不死的(戏称)
放在一起,通过回收对象的年龄(熬过的回收次数)放在不同的区域,每次只针对新生代
进行垃圾回收,老年代
偶尔进行一次回收。针对不同的区域收集,也就有了Minor GC ,Major GC ,Full GC
回收类型的划分。
在划分区域后,根据不同区域以及区域内元素的特点,我们使用不同的垃圾收集算法
,且他们都是基于分代收集理论。后面我们会详细说明。
> 标记-清除算法
> 标记-复制算法
> 标记-整理算法
第二个问题:对象不是孤立存在的,如果对象之间存在跨代引用怎么办?
当我们针对新生代垃圾回收时,因为存在跨代引用,我们还得遍历老年代保证可达性分析结果的正确。这肯定也会对内存回收带来负荷,为了解决这个问题,我们对分代收集理论添加第三条经验法则:
> 跨代引用假说:跨代引用相比同代引用要少的多。
其实根据前面法则,我们也能知道如果针对存在跨代引用,那么新生代的对象在经历过多轮垃圾回收后,也会被放到老年代的,这样问题就解决了,但在之此前我们怎么减少对老年代分析的性能消耗?,我们可以依据跨代引用假说,我们就可以不用对整个老年代进行扫描,只需要在新生代上建立一个全局的数据结构(记忆集,Remembered Set),这个结构把老年代划分成若干个小块儿,标识出老年代的哪一块儿内存存在跨代引用,这样每次对新生代垃圾回收时,只用把老年代这片内存的对象加入到GC Roots即可。虽然在对象改变关系时维护数据的准确性,但比起扫描整个老年区还是划算的。
这里我们需要确定一些回收类型的概念
部分收集(Partial GC):目标不是完整的收集整个Java堆,其中又细分为:
新生代收集(Minor GC):只收集新生代
老年代收集(Major GC):只收集老年代,只有CMS收集器有单独收集老年代的行为(这里有一些概念上的混淆,有人会把Major GC 和 Full GC混淆,需要我们自己判断说的到底是哪个)
混合收集(Mixed GC):收集新生代和部分老年代,目前只有G1收集器有
整堆收集(Full GC):收集整个堆区域和方法区
标记-清除算法
这是一个最早期最基础的算法,也就是我们先标记哪些对象需要清除,然后再统一进行清理,当然我们也可以只标记哪些需要保留,将没有标记的进行统一的清理,但是这个算法存在两个问题:
- 执行效率不稳定:如果有大量对象需要清理或者大量对象需要留下,那我们在标记和清除上花的时间就越多。
- 内存空间碎片化问题,也就是说在我们标记,清除后会产生大量不连续的内存碎片,当后面再分配大对象时,无法分配到空间,而再进行一次GC。
标记-复制算法
也被称为复制算法,最初理论为内存按照容量分成1:1两块,每次只使用一块儿,当一块儿不够用时,在分析后,将保留的对象标记并复制到另一半区域,移动堆顶指针,按照顺存放,然后直接清除之前的区域。这样就不用担心空间零散的碎片。不过缺点也很明显,你直接分一半,也太浪费空间了,导致频繁的标记复制。
后面又进行了一次优化:Appel式回收
,新生代分为一个较大的Eden区域
,和两个较小的Survivor区
,默认比例为8:1:1,也就是每次我们只使用Eden区和一个Survivor区,当垃圾回收时,将保留的对象都放在另一个Survivor区域,然后直接堆之前的区域清理。
第三个问题:那假如这次清理完,发现有大量的对象需要保存,但是一个Survivor区不够,怎么办?
这里就设计了一个安全设计,当一个Survivor区不够,就需要使用老年代来做分配担保,将这些对象(超出的部分)直接交给老年代。不过我们最好还是用代码测试一下:
public class demo {
private static byte[] a1;
private static byte[] a2;
private static byte[] a3;
/**
* -XX:NewSize=10485760—新生代大小为10m
* -XX:MaxNewSize=10485760—新生代最大大小为10m
* -Xms20M—初始堆大小为20m
* -Xmx20M—最大堆大小为20m
* -XX:+UseParNewGC:新生代使用ParNewGC垃圾回收器
* -XX:+UseConcMarkSweepGC---老年代使用CMS
* -XX:+PrintGCDetails---打印GC详细日志
* -XX:+PrintGCTimeStamps—打印GC时间
* -XX:SurvivorRatio=8—设置eden区和survivor区的比例为8:1:1
* -XX:PretenureSizeThreshold=10485760—设置最大对象的阈值为10m
*
* @param args
*/
public static void main(String[] args) {
/**
* 当前为6m时,可以将对象全部放在eden区
* par new generation total 9216K, used 8000K
* eden space 8192K, 97% used
* from space 1024K, 0% used
* concurrent mark-sweep generation total 10240K, used 0K
*/
a1 = new byte[1024 * 1024];//1m
/**
* 可以发现当再添加189k时,发生了第一次gc,
* 如果Survivor区放不下存活对象,存活对象并不是全都进入老年代,而是部分对象进入老年代,部分对象继续被分配到Survivor区
* par new generation total 9216K, used 720K
* eden space 8192K, 1% used
* from space 1024K, 62% used
* concurrent mark-sweep generation total 10240K, used 6146K
*/
a2 = new byte[189* 1024];
//直接分配一个5m的对象,由于eden区只有8m,之前已经分配了的再加上一些未知对象也会占据一定的内存空间,此时必然会引起新生代gc
a3 = new byte[5 * 1024 * 1024];//5m
}
}
可以发现在上面的实验情况与我们想象的并不一样,结论就是:如果Survivor区放不下存活对象,存活对象并不是全都进入老年代,而是部分对象进入老年代,部分对象继续被分配到Survivor区。
下面还有一种情况我们用代测试:
class demo2 {
/**
* 此时从红框中的信息可以清晰的发现,from区被占用率为0%,
* 而老年代空间则被使用了26m左右,存活对象还是25m,逻辑没变,
* 那么这情况就可以表明25m对象在新生代gc后都进入了老年代。
*
*
* 结论:新生代gc后,如果触发了老年代gc,即使survivor区放的下部分存活对象,对象也会全部进入老年代。
* @throws InterruptedException
*/
public static void loadData() throws InterruptedException {
//一开始我们就分配了80m,相当于一个eden区
byte[] data0 = new byte[80 * 1024 * 1024];
data0 = null;
byte[] data = null;
//请求40m
for (int i = 0; i < 4; i++) {
//每个请求10m
data = new byte[10 * 1024 * 1024];
}
data = null;
byte[] data1 = new byte[5 * 1024 * 1024];
byte[] data2 = new byte[10 * 1024 * 1024];
byte[] data3 = new byte[10 * 1024 * 1024];
data3 = new byte[10 * 1024 * 1024];
data3 = new byte[10 * 1024 * 1024];
}
/**
* -XX:NewSize=104857600—新生代大小为100m
* -XX:MaxNewSize=104857600—新生代最大大小为100m
* -Xms200M—初始堆大小为200m
* -Xmx200M—最大堆大小为200m
* -XX:+UseParNewGC:新生代使用ParNewGC垃圾回收器
* -XX:+UseConcMarkSweepGC---老年代使用CMS
* -XX:+PrintGCDetails---打印GC详细日志
* -XX:+PrintGCTimeStamps—打印GC时间
* -XX:SurvivorRatio=8—设置eden区和survivor区的比例为8:1:1
* -XX:PretenureSizeThreshold=104857600—设置最大对象的阈值为100m
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
/**
* 上述代码一开始就被分配了80m的大对象,所以这个对象会直接进入老年代占据80m空间,
* 那老年代就只剩20m空间了。肯定不够分配新生代GC后的存活下来的25m对象,就会触发full gc。
*/
loadData();
}
}
上面测试的结论是:新生代gc后,如果触发了老年代gc,即使survivor区放的下部分存活对象,对象也会全部进入老年代。
标记-整理算法
现在我们是针对的老年代
对象的存亡特征,其中标记的过程仍然是”标记-清除”,但后续不是直接清除可回收对象,而是让所有存活对象都向内存空间一端移动,然后直接清理掉边界值以外的内存,这样也不用担心空间碎片的问题了。但是缺点也很明显,老年代中大部分都向都是存活的,如果移动并更新引用它们的地方,也是非常负重的操作。而且必须暂停所有的用户应用程序才能进行。
所以如果是使用标记-清除算法,内存分配会很复杂,使用标记-整理算法,内存回收时很复杂。Hotspot虚拟机里面关注吞吐量的Parallel Old收集器
基于标记-整理算法的,关注延迟的CMS收集器
是基于标记-清除的。具体详细内容可以去深入了解。
还有一种和稀泥的方法,前面一直使用标记-清理算法,暂时容忍空间碎片,当影响到对象分配时,再进行标记-整理算法。前面提到的CMS收集器
遇到碎片过多时,采用的就是这个方式。
经典垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那垃圾回收器就是内存回收的实践者,在《Java虚拟机规范》并未声明垃圾收集器规定,所以各个厂商,不同版本的垃圾收集器都会有一些差别,不同的虚拟机也会提供不同的参数来选择适合自己场景的收集器。
上图为各个收集器直接可行的匹配关系。
Serial 收集器
它是最基础的,最早的收集器,基于标记复制
算法实现,特点:单线程,当进行垃圾收集的时候,必须暂停其他所有工作线程,直到收集完毕,Stop The World
。对于大部分人来说是不可接受的,好比你每运行一小时,都得暂停是十五分钟。
但注意的是它依然是HotSpot虚拟机运行在客户端的默认新生代收集器,它的优点就是简单且高效,对于内存资源受限制的环境,它是所有收集器中额外内存消耗最小的,对于单核处理或者处理器较少时,Serial就很高效,没有线程交互的开销。
ParNew 收集器
相当于是一个Serial多线程并行版本,基于标记复制
算法实现,其实性能一般,只不过是后来出现的CMS收集器只能个Serial和ParNew收集器配合使用,CMS收集器的一个特点就是首次实现垃圾收集线程和用户线程(基本上)同时工作。
但是后来又出现了G1收集器,G1收集器是一个面向全堆的收集器,所以ParNew就算是退休了,不过当随着可以被使用的处理器核数的增加,效率也会优化。
Parallel Scavenge 收集器
一款新生代收集器,基于标记-复制
算法实现的收集器,也是一个多线程收集器,那我们为啥使用它?
是因为该收集器的特点是与其他收集器关注点不同
CMS收集器的关注点是尽可能的缩短垃圾回收时,用户线程停顿的时间,而Parallel Scavenge关注的是吞吐量
$$ 吞吐量 = {用户代码运行时间 \over 用户代码运行时间+运行垃圾收集时间} $$
举个例子,如果代码运行+运行垃圾收集时间用了100分钟,其中垃圾收集用了一分钟,那吞吐量就是99%。主要适合于后台运算而不需要太多交互的分析任务。所以它也被称为吞吐量优先收集器
。
可通过-XXLMaxGCPauseMilis
来控制最大垃圾收集时间,也可通过-XX:GCTimeRatio
参数设置吞吐量大小。还有一个重要参数-XX:+UseAdaptiveSizePolicy
,它是一个开关,用了我们就不用自己设定新生代各个区域划分问题,虚拟机会自己收集信息,来动态调整最合适的划分,被称为:垃圾收集的自适应的调节策略
。
实战:内存分配与回收策略
Java技术体系的自动内存管理,最核心的目标就是自动化的解决两个问题:内存分配
和内存回收
,在之前我们讲的都是内存回收,现在我们来看一下内存分配的细节,这里我是使用JDK1.8,HotSpot虚拟机,使用的ParallelGC
这里有一个很细的点:《深入理解Java虚拟机》使用的
Serial+Serial Old
客户端默认收集器组合下的内存分配和回收策略,但咱们的jvm是服务端的,说是只能在32位机jvm.cfg
修改文件使用客户端,所以当我运行书上第一个代码块儿时,就发现结果与书上不对,想不出原因,所以如果你也是ParallelGC,那么书本上本章上许多代码结果都是不一样的。一些VM参数也是无法使用的或者没有效果的。
- java -XX:+PrintCommandLineFlags -version 输出JVM默认垃圾收集器,第四行的
UseParallelGC
和最后一行的Server VM
[root@jeespring ~]# java -XX:+PrintCommandLineFlags -version -XX:InitialHeapSize=132500800 -XX:MaxHeapSize=2120012800 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC java version "1.8.0_211" Java(TM) SE Runtime Environment (build 1.8.0_211-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode
- /usr/libexec/java_home -V 这个是在mac上找我的jvm在哪
- find . -name “jvm.cfg” 如果你找不到jvm.cfg,通过它去找,表示在当前路径下找
- JVM的Server端和Client端区别网上有详细介绍,通俗的讲就是Client端轻量快速,Server端重量性能好(默认)。
对象的内存分配从概念上来讲,应该是在堆上分配,不过实际中也可能通过即时编译后拆分为标量间接存储在栈上(这个在我之前写的
逃逸分析
中有说明)
### 对象优先在Eden分配(书本原题目)
- Serial:当大对象要分配内存时,发现Eden区已经满了,而且Survivor区也不够,会将Eden区原来的对象直接放入老年代,再把新的对象放入Eden区。
- Parallel(我测试的):发现Eden区满了,Survivor区不够,会直接把大对象放到老年代,Eden区对象不变。读者可以自己再测试一遍
/**
* 对象分配
*/
public class no1 {
private static final int _1MB = 1024*1024;
/**
* VM: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* 堆空间分配20m,新生代10m,新生代中的Eden和Survivor为8:1:1
* @param args
*/
public static void main(String[] args) {
byte[] a1,a2,a3,a4;
a1 = new byte[2*_1MB];
a2 = new byte[2*_1MB];
a3 = new byte[2*_1MB];
/**
* 上面已经创建了8m,相当于Eden区已经满了,现在我们再放一个4M的对象
* Eden已经放不下了,会启动一次MinorGC,我们看一下会之前的对象和新来的对象如何分配
*
* PSYoungGen total 9216K, used 8001K
* eden space 8192K, 97% used
* from space 1024K, 0% used
* ParOldGen total 10240K, used 4096K
*/
a4 = new byte[4*_1MB];
}
}
大对象直接进入老年代
我们知道一般对象不会直接到老年区,得现在Eden区和Survivor区熬几轮,但如果有大的对象已经在里面了,那么就很容易触犯minor GC,导致对象进行标记-复制,所以可以可以通过设置XX:PretenureSizeThreshold
来指定超过多大的对象直接去了老年区(注意这个参数Parallel无法使用)
/**
* 对象分配
*/
public class no1 {
private static final int _1MB = 1024*1024;
/**
* VM: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728
* 堆空间分配20m,新生代10m,新生代中的Eden和Survivor为8:1:1
* -XX:PretenureSizeThreshold 表示 需要分配的对象只要大于3M,就直接分配到老年区,注意:这个参数只能Serial使用,ParallelGC无法使用
* @param args
*/
public static void main(String[] args) {
byte[] a4;
a4 = new byte[4*_1MB];
}
}
长期存活的对象进入老年代
意思就是我们给每个对象定义一个对象年龄计数器,存储在对象头中,每经历一次Minor GC后存活,并可以存储到Survivor区时,年龄就+1,当长到系统-MaxTenuringThreshold
设定值时(默认值15),就去老年代.
动态对象年龄判定
这里就是说系统可以动态判断,如果每个对象都得等15轮minor GC,那就会导致频繁的GC,影响性能,如果Survivor区中对低于或者等于某个年龄的对象总和超过了Survivor空间的一半,那这个年龄及其以上的对象直接进入老年区。
空间分配担保
我们在前面就提到,其实老年代就是新生代的担保人,如果新生代放不下的对象,我就把对象放到老年代。根据这第一点我们就可以推断出每次Minor GC
前,虚拟机都应检查老年代最大的连续空间是否有足够的空间装下新生代所有对象,如果空间足够,那我们就放心的Minor GC,如果不够了,虚拟机去查看-XX:HandlePromotionFailur
参数是否允许担保失败,如果允许,继续检查老年代之前进来对象的平均大小是否小于老年代最大连续空间(简单来说:通过以前来的,猜测后面来的应该差不多大),如果足够,进行Minor GC,虽然有风险,如果不够,或者是没有担保,则直接Full GC
,这就很影响性能了。所以一般都会将-XX:HandlePromotionFailur
打开,来避免频繁的FUll GC。这个参数在JDK6 Update24后就不再使用了。
也就是现在只要老年代的连续空间大于新生代对象总大小或者以往对象的平均值大小,就会进行Minor GC,否则将进行Full GC。
总结
垃圾收集器在许多场景都是影响系统停顿时间和吞吐能力的重要因素,虚拟机之所以提供各种不同的收集器和大量的调节参数就是因为各种有各种的适用场景,需要我们不断探索,不断组合。当然我们也需要知道每种收集器的特点以及范围。