您的位置:首页 > 其它

垃圾收集器与内存分配策略

2018-01-04 19:55 162 查看

对象是否死亡

垃圾收集器在对堆进行会收前,需要确定这些对象哪些依然“存活”,哪些已经死亡。

引用计数法

算法描述

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

问题

很难解决对象间相互循环引用的问题。Java虚拟机没有采用引用计数法。

可达性分析算法

算法描述

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

GC Roots对象选择范围

虚拟机栈中(栈帧的本地变量表)引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(即一般的Native方法)引用的对象

再谈引用计数法

引用的传统定义

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

引用的扩充定义

强引用

强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用

软引用是用来描述一些还有用但并非必需的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

弱引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

生存还是死亡

对象死亡条件

至少经历两次标记过程

如果对象在进行可达性分析后发现没由于GC Roots相连的引用链,将会被第一次标记。

稍后,虚拟机将对F-Queue中的对象进行第二次小规模的标记。

F-Queue

如果一个对象被判定为有必要执行finalize()方法,那么这个对象将被放在F-Queue中,并稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。

对象重新生存方法

在finalize()方法中与引用链中的任何一个对象建立关联。

注意

任何一个对象的finalize()方法都只会被系统调用一次,也就是说只有一次自救机会。
ea42

回收方法区

主要工作

废弃常量

回收废弃常量与回收Java堆中的对象非常类似。 以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。 常量池中的其他类(接口)、 方法、 字段的符号引用也与此类似。

废弃的类

类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记清除算法

算法描述

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。



问题

效率问题

标记和清除的两个过程效率都不高。

空间问题

标记和清除后会产生大量不连续的内存空间。容易频繁触发垃圾回收操作。

复制算法

算法描述

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。



优点

实现简单、运行高效。

不用考虑内存碎片等复杂情况。

问题

将可用内存缩小为原来的一半,代价有点大。

应用

年轻代垃圾回收策略。

优点

对象存活率较高时就要进行较多的复制操作,效率将会变低。

标记-整理算法

算法描述

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。



应用

老年代垃圾回收策略。

分代收集算法

算法描述

根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率高、 没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

HotSpot算法实现

枚举根节点

可作为GC Roots的结点

全局性引用(常量、静态属性)

执行上下文中(栈帧中的本地变量表)

可达性分析对时间敏感

Stop the world

安全点

在特定的地方生成OopMap,这些地方被称为安全点。程序执行将在安全点停下来进行GC操作。

选择标准

是否有让程序长时间执行的特征。长时间执行最明显的特征就是指令序列复用。如:方法调用、循环跳转、异常跳转等。

目的

在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。

停顿策略

抢先式中断

在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。

主动式中断

当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域

在一段代码片段之中,引用关系不会发生变化。 在这个区域中的任意地方开始GC都是安全的。 我们也可以把Safe Region看做是被扩展了的Safepoint。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。



Serial收集器

特点

单线程,只使用一个CPU或一条收集线程完成垃圾收集工作。

暂停其他所有工作线程呢个,直到收集结束。

缺点

Stop the world

优点

简单而高效

应用

新生代采用复制算法

老年代采用标记-整理算法

运行在Client模式下的虚拟机优先选择

ParNew收集

Serial收集器的多线程版本。是许多运行在Server模式下的虚拟机中首选的垃圾收集器。

* 并行

多条垃圾收集线程并行工作,并行的基础是有多个CPU(自己的理解,不知道是否有偏差)。

* 并发

多条垃圾收集线程、用户线程同时执行,通过时间片控制是否获得CPU的使用权。

Parallel Scavenge收集器

特点

使用复制算法

控制用户程序运行的吞吐量(吞吐量 = 用户程序运行时间 / (用户程序运行时间 + 垃圾收集时间))。

策略

-XX:MaxGCPauseMillis : 控制最大垃圾收集停顿时间

-XX:GCTimeRatio:直接设置吞吐量大小,取值应为(0,100).

-XX:UseAdaptiveSizePolicy:GC自适应调整策略

Serial Old收集器

特点

单线程

使用标记整理算法

作为CMS收集器的后备预案

应用

Client模式下的虚拟机使用。

Parallel Old收集器

特点

是Parallel Scavenge老年代的版本,使用多线程和标记整理算法。

优点

充分发挥了服务器多CPU的特点,与新生代Parallel Scavenge组合可以使系统吞吐量最大化。

CMS收集器

目标

获取最短回收停顿时间,提高应用的响应速度,给用户良好的体验。

算法

标记-清除算法。

流程

初始标记(CMS initial mark)

需要stop the world。

标记GC Roots能直接关联的对象,速度很快。

并发标记(CMS concurrent mark)

需要stop the world。

进行GC Roots Tracing,应该是GC Roots链路上还没有被标记的对象。

重新标记(CMS remark)

修正并发期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录。这个阶段的停顿时间会比初始标记时间阶段稍长一些,但远比并发标记时间短。

并发清除(CMS concurrent sweep)

耗时也比较长。

优点

并发收集

低停顿

缺点

CMS收集器对CPU资源非常敏感。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”而导致另一次Full GC的产生。主要在CMS运行期间,内存无法满足程序需要。这时虚拟机会启动预备方案:临时启用Serial Old收集器来重新整理老年代的垃圾收集工作。

CMS收集器是一款基于“标记-清除”算法的收集器,意味着收集结束时会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有大量空间剩余,但是无法分配给大的对象。

策略

-XX:CMSInitiatingOccupancyFraction:激活CMS收集器的参数

-XX:UseCMSCompactAtFullCollection:用于CMS收集器顶不住要进行Full GC时,开启内存碎片的和并整理过程。

-XX:CMSFullGCsBeforeCompaction:表示多少次Full GC后才进行内存的碎片整理。

G1

关键点

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop The World的时间。通过并发的方式让Java程序继续运行。

分代收集:采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象。

空间整合:G1从整体上来看是基于“标记-整理”算法,从局部(两个region之间)上来看,是基于“标记-复制”算法。这下使得G1垃圾收集器不会产生内存碎片,不会因为无法找到连续内存而触发一次GC.

可预测的停顿:除了降低停顿时间,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间片内,消耗在垃圾回收上的时间不超过N毫秒。

特点

G1将Java整个堆分为多个大小相等的区域(region)。依然保留新生代、老年代的概念,但是已经不再是物理上隔离的内存空间,而是一部分Region(不需要连续)的集合。

G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域。这样就保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

流程

初始标记(Initial Marking)

标记GC Roots直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region区域中创建新对象。

并发标记(Concurrent Marking)

从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这段时间耗时较长,但可与用户程序并发执行。

最终标记(Final Marking)

修正并发标记阶段因用户线程继续运作而导致标记记录产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs里面的数据合并到Remembered Set里面。这段时间需要停顿用户线程,但是可以并行执行。

筛选回收(Living Data Counting and Evacuation)

对各个Region的回收价值和成本进行排序,根据过户期望的停顿时间制定回收计划。

内存分配与回收策略

内存分配就是是在堆上分配,对象主要分配在新生代的Eden区。如果启动了本地线程分配缓存,将按线程优先在TLAB上分配。少数情况直接分配在老年代。

对象优先在Eden分配

描述

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

策略

-XX:PrintGCDetails:打印内存回收日志

实验代码

package cn.codingstar.notes.gc;

/**
* @version: java8
* @author: CodingStar
* @contact: shixing.cs@gmail.com
* @file: MinorGCTest.java
* @time: 2018/1/7 10:35
* @software: Intellij Idea
* @desc:
*/
public class MinorGCTest {

private static final int _1MB = 1024 * 1024;

/**
* VM 参数:
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];// 出现一次Minor GC
}
}

// output
//[GC (Allocation Failure) [PSYoungGen: 6299K->824K(9216K)] 6299K->4928K(19456K), 0.0284873 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
//        Heap
//        PSYoungGen      total 9216K, used 7205K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
//        eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b5b0,0x00000000ffe00000)
//        from space 1024K, 80% used [0x00000000ffe00000,0x00000000ffece030,0x00000000fff00000)
//        to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
//        ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
//        object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
//        Metaspace       used 3496K, capacity 4498K, committed 4864K, reserved 1056768K
//class space    used 387K, capacity 390K, committed 512K, reserved 1048576K


Minor GC

发生在新生代的的垃圾回收动作,因为Java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

Major GC/Full GC

发生在老年代的GC,经常伴随至少一次Minor GC(并非绝对,如在Parallel Scavenge收集器的手机策略里就有直接进行Major GC的选择过程)。Major GC一般比Minor GC慢10倍以上。

大对象直接进入老年代

定义

大对象是指需要大量连续内存的对象。最典型的大对象就是那种很长的字符串以及数组。

问题

经常出现大对象容易导致内存还有不少空间时就直接触发垃圾收集以获得连续空间来“安置”它们。

策略

-XX:PretenureSizeThreshold:令大于这个值的对象直接进入老年代。

意义

避免在Eden区和Survivor区之间发生大量的内存复制

长期存活的对象将进入老年代

定义

如果对象出生在Eden区,并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并把对象年龄设为1。当它的年龄增加到一定程度(默认为15),将会被晋升到老年代中。

策略

-XX:MaxTenuringThreshold

实验代码

/**
* VM 参数:
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
*/
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 8];
// 什么时候进入老年代取决于 -XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
}


动态对象年龄判定

定义

为了更好的适应不同程序的内存情况,虚拟机并不是永远的要求对象年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保

定义

在发生Minor GC之前,虚拟机先检查老年代最大可用夫人连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续内存是否大于历次晋升到老年代的平均大小,如果大于,尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置为不允许毛线,那这时改为进行一次Full GC。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: