垃圾回收
Posted im向北
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了垃圾回收相关的知识,希望对你有一定的参考价值。
五、垃圾回收
为什么要垃圾回收?
计算机系统,包括内存最小的寻址单元是字节;说白了,虚拟机理论上最大内存就是硬件内存,硬件内存是有限的,你占用了,我就用不了了;所以对象不用的时候,回收其占用内存空间,以提高虚拟机资源利用率!让虚拟机有更高的产出!
垃圾回收作用的区域?
程序计数器,栈区,本地方法栈区的生命周期都是和线程绑定的;线程消失,其占用的内存也就释放;
所以,垃圾回收作用的区域是 堆内存(对象),方法区(常量);
基本类型在栈区,自动回收!
方法区如何回收常量?
String str=”abc”;在方法区常量池中会添加“abc”,后期如果有其他字符串值为”abc”,都会指向常量池中唯一的“abc”;
当没有String指向常量池中的“abc”时,就回收它了!
方法区回收性价比不高!
方法区如何回收无用的类?
类需要同时满足下面3个条件才能算是“无用的类”:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
如何判断对象已死?
引用记数法
在对象中,比如对象头中添加引用计数器,有一个地方引用它就+1;否则-1;计算引用计算器的值来确定对象是否被引用。
缺点:不能解决循环引用问题
Java JVM是否实现:否,python中实现了。
可达性算法分析
什么是可达性算法?
从垃圾回收的根对象GC Root触发,搜索根对象持有的所有成员变量对象Objs;再从所有成员变量对象Objs出发,搜索Objs持有的所有成员变量对象Objs2;搜索不可到达的对象就是可以垃圾回收的对象!
算法特点Stop-the-world
Stop-the-world,是说GC停顿,在执行算法时要求整个程序暂停,目的是对象引用链不能改变。
因为任何垃圾回收算法判断对象是否存活都使用可达性算法来分析,所以,任何垃圾回收算法,标记阶段都必须Stop-the-world。
哪些对象可以作为GC Root对象?
1)虚拟机栈中引用的对象,(栈帧中本地变量表中引用的对象)
就是对象A的普通成员变量是对象B;A和B都在栈帧中,都可做GC Root对象
2)方法区中类静态属性引用的对象;
就是对象A的静态成员是对象static B,而 static B在方法区中,B中引用了对象的成员变量,那么B也作为GC Root对象
3)方法区中常量引用的对象
就是对象A的成员对象是final B,final修饰的B默认就是final static B;B中引用了对象的成员变量,那么B也作为GC Root对象
1) 本地方法栈中JNI(native方法中)引用的对象
不可达对象的逃脱
http://blog.csdn.net/mark2when/article/details/59162810
http://blog.csdn.net/w605283073/article/details/72757684
1)从GC Roots搜索所有不可到达对象
2)准备执行不可到达对象的finalize方法
如果finalize方法执行过,直接垃圾回收
如果finalize方法每执行过,执行它,然后垃圾回收
如果执行finalize方法的过程中,该对象被其他可到达对象引用了,则该对象逃脱!
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public 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 mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
运行结果:
finalize mehtod executed!
yes, i am still alive :)
no, i am dead :(
SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。
另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
三色标记算法
并发标记中,就是用户程序和标记算法同时执行的过程,使用三色标记算法!
它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
黑色:根对象,或者该对象与它的子对象都被扫描
灰色:对象本身被扫描,但还没扫描完该对象中的子对象
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色,没有引用的对象被置为白色
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理
问题来了:并发标记时用户程序更改对象引用关系了怎么办?
如何解决并发标记时用户更改对象引用问题?
程序代码更改对象引用有2种方式:
1)增加对象引用,创建对象Object o=new Object(); 或者o1=new Object()
2)删除对象引用,o=null;
所以并发标记保证应用程序在运行的时候,GC标记的对象不丢失,有如下2中可行的方式:
1)在新增对象时,记录对象的reference关系到可到达的对象结构中;
2)在删除的时候,从可到达对象结构中,删除对象的reference
刚好这对应CMS和G1的2种不同实现方式:
1)在CMS中,记录新增,不记录删除,采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
2)在G1中,记录删除,不记录新增,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
第1,在开始标记的时候生成一个快照图标记存活对象
第2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
第3,可能存在游离的垃圾,将在下次被收集
Write Barrier
https://stackoverflow.com/questions/19154607/how-actually-card-table-and-writer-barrier-works
Write Barrier是并发标记时,对用户程序更改对象引用关系的一种监听机制,会把用户程序对对象-引用关系的更改记录到remember set log中,并在后期处理remember set log时告知Garbage Collector;方便垃圾回收器GC。
不同的是,你记录新增还是删除。CMS就记录新增,G1就记录删除!
write barrier - a piece of code executed whenever a member variable (of a reference type) is assigned/written to. If the new reference points to a young object and it\'s stored in an old object, the write barrier records that fact for the garbage collect. The difference lies in how it\'s recorded.
具体可以看我之前的回答整理
https://www.zhihu.com/question/37028283
GC的本质
GC的本质就是,从GC Roots触发搜索所有不可到达对象,然后执行这些对象的finalize()方法,然后再垃圾回收这些对象!
所以执行finalize()时,如果要回收的对象被可到达对象引用,则该对象逃脱GC。
引用类型
Reference引用存储的是堆内存的地址
引用有哪些类型?
1)强引用Strong reference
2)软引用Soft reference
什么是软引用类型?
软引用类型就是有用但非必须的对象
3)弱引用Weak reference
4)虚引用Phantom reference
为什么有引用类型?
引入引用类型的级别,是为了根据是否可以回收内存,把对象进行分类;哪些可以回收,哪些不能回收,这样让JVM的垃圾回收的目的更具体,更高效!
强引用类型
什么是强引用类型?
强引用类型就是这种,Object obj=new Object()
如何回收强引用类型对象?
当内存空间不足,抛出OutOfMemoryError错误
除非obj=null;否则只要强引用还在,垃圾回收器就不会回收引用的对象!
分2种情况
1)方法中使用强引用类型
public void test(){
Object o=new Object();
// 省略其他操作
}
此时,执行test()方法时,test方法进入栈帧,对象o的reference引用在线程栈区创建,而reference指向的对象在堆区;test()方法执行完毕,test方法退出栈帧,对象o的引用自动随之清除,堆内存的Object对象也会被垃圾回收!
结论:不用管,自动清除!
2)成员变量使用强引用类型
成员变量Object obj=new Object();
在成员变量使命达成后,未来不再使用,则手动设置obj=null,则对象将被垃圾回收!
如何使用强引用类型?释放数组元素空间
一句话:除非你把我置为null,否则JVM是不会回收强引用类型的!(方法内除外)。
你可以保留我的位置,这样就不用重新分配内存了!
强引用在实际中有非常重要的用处,举个ArrayList的实现源代码:
private transient Object[] elementData;
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
注意ArrayList私有成员变量elementData,在clear()执行时,把elementData数组中每个成员置为null;而不是把elementData置为null;这样数组中的对象都将被垃圾回收;而elementData数组因为是强引用,它的栈reference,在栈内存,数组对应(数组对象)空间在堆内存不变,避免后续调用add()或其他方法时 还得重新分配数组对象空间!
软引用类型
什么是软引用类型?
软引用类型修饰的对象是 有用但非必须的对象!什么意思?缺了软引用对象,程序也照常运行!
JVM内存不足时,会清除软引用类型修饰的对象!
生命周期:创建到JVM内存不足,内存充足的时候也可能进行垃圾回收!
如何回收软引用类型对象?
String str=new String("abc"); // 强引用
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
虚拟机会在内存溢出之前,回收掉软引用类型的对象str!内存不足时软引用类型对象自动被垃圾回收!
相当于,内存不足,JVM 自动把str置为null,然后等到垃圾回收str。
If(JVM.内存不足()) {
str = null; // 转换为软引用
System.gc(); // 垃圾回收器进行回收
}
软引用类型可以用来做什么?
可以用来实现内存敏感的高速缓存!
如何使用软引用类型?浏览器缓存
一句话:有内存,我存在,没内存,我奉献我的内存!你可以在你需要时使用我,不需要我时清除我!
虚引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新请求;
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
这时候就可以使用软引用
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。?
弱引用类型
什么是弱引用类型?
弱引用类型的对象是更不必须的对象!
软引用类型对象会在jvm内存不足时被垃圾回收!
弱引用类型对象会在垃圾回收线程发现它时就被回收,不论jvm内存是否不足!
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
生命周期:创建到第一次垃圾回收!
如何回收弱引用类型?
自动回收str,不论内存是否不足
String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
弱引用类型转化成强引用类型
String abc = abcWeakRef.get(); //一句代码就把str转成强引用类型了
什么时候使用弱引用类型?
1)偶尔使用,随用随取的对象
如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。
2)引用一个对象又不想改变它的生命周期时,使用弱引用类型
弱引用类型举例
public class ReferenceTest {
private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();
public static void checkQueue() {
Reference<? extends VeryBig> ref = null;
while ((ref = rq.poll()) != null) {
if (ref != null) {
System.out.println("In queue: " + ((VeryBigWeakReference) (ref)).id);
}
}
}
public static void main(String args[]) {
int size = 3;
LinkedList<WeakReference<VeryBig>> weakList = new LinkedList<WeakReference<VeryBig>>();
for (int i = 0; i < size; i++) {
weakList.add(new VeryBigWeakReference(new VeryBig("Weak " + i), rq));
System.out.println("Just created weak: " + weakList.getLast());
}
System.gc();
try { // 下面休息几分钟,让上面的垃圾回收线程运行完成
Thread.currentThread().sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
checkQueue();
}
}
class VeryBig {
public String id;
// 占用空间,让线程进行回收
byte[] b = new byte[2 * 1024];
public VeryBig(String id) {
this.id = id;
}
protected void finalize() {
System.out.println("Finalizing VeryBig " + id);
}
}
class VeryBigWeakReference extends WeakReference<VeryBig> {
public String id;
public VeryBigWeakReference(VeryBig big, ReferenceQueue<VeryBig> rq) {
super(big, rq);
this.id = big.id;
}
protected void finalize() {
System.out.println("Finalizing VeryBigWeakReference " + id);
}
}
最后的输出结果为:
Just created weak: com.javabase.reference.VeryBigWeakReference@1641c0
Just created weak: com.javabase.reference.VeryBigWeakReference@136ab79
Just created weak: com.javabase.reference.VeryBigWeakReference@33c1aa
Finalizing VeryBig Weak 2
Finalizing VeryBig Weak 1
Finalizing VeryBig Weak 0
In queue: Weak 1
In queue: Weak 2
In queue: Weak 0
虚引用类型
什么是虚引用类型?
与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。
虚引用类型必须和引用队列一起使用!PhantomReference类实现虚引用!
虚引用类型有什么作用?
唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。
虚引用类型的使用场景有哪些?
1)虚引用类型可以让你知道它指向的对象什么时候从内存中移除
设置了虚引用的对象在被垃圾回收时会接到系统发送的通知!
而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后在继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。
2)避免析构问题
参考https://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
总结与参考
引用类型 |
被垃圾回收时间 |
用途 |
生存时间 |
强引用 |
从来不会 |
对象的一般状态 |
JVM停止运行时终止 |
软引用 |
在内存不足时 |
对象缓存 |
内存不足时终止 |
弱引用 |
在垃圾回收时 |
对象缓存 |
gc运行后终止 |
虚引用 |
Unknown |
Unknown |
Unknown |
Java 7之基础 - 强引用、弱引用、软引用、虚引用
http://blog.csdn.net/mazhimazh/article/details/19752475
译文:理解Java中的弱引用- 技术小黑屋
https://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
垃圾回收算法
标记-清除法
最基础的算法,其他算法都是它的改进版!
算法过程
1)标记所有没有引用的对象
2)统一回收所有被标记的对象
适用区域
无,理论算法
缺点是什么?
1)标记和清除的效率都不高,为什么?
2)产生大量的内存碎片,重点
复制回收法
算法过程
标记-复制;
1) 将内存分为等大小2块区域A和B,
2) 只使用一块区域A,
3) 等A区域满了,对A进行标记,把所有存活对象集中复制到B区域
4) 清理A区域
适用区域
年轻代 ,对象存活率较低,复制的开销低!
如果使用在年老代,对象存活率高,复制的开销也高!
优缺点
优点:解决内存缝隙问题
缺点:内存使用率低
商业虚拟机大部分使用这种算法,Hotspot虚拟机年轻代Eden和2个Suivivor比例为
8:1:1就是为了避免内存碎片问题
标记-整理法
适用区域
老年代,对象存活率较高,不用复制算法
算法过程
1) 遍历GC Roots,对所有存活对象标记
2) 把所有存活对象,集中复制到内存某一端(最前或者最后)连续区域A
3) 清理除了A外所有区域
优缺点
优点:连续空间
缺点:效率不高,标记所有存活对象,并记录所有对象引用地址
JVM垃圾回收策略-分代收集
经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation) 的空间。新创建的对象存放在称为 新生代(young generation) 中(一般来说,新生代的大小会比 老年代 小很多),随着垃圾回收的重复执行,生命周期较长的对象会被 提升(promotion) 到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。
把Java堆分为年轻代(Eden:Survior1:Survivor2=8:1:1),老年代,再加上方法区的永久代;一共3个区域;
按照区域的特点,分别有适合垃圾回收算法!
对象存活率低,复制算法;
对象存活率高,标记清理,或者标记整理
垃圾回收过程
查找GC Root过程
GC Roots主要存在于常量池全局变量,线程栈本地变量表的引用;但现在应用方法区有上百兆,如何高效查找到所有GC Root?
GC停顿后,并非遍历所有线程栈和方法区查找GC Roots;而是在类加载的过程中虚拟机就把对象什么偏移量上是什么类型变量计算出来,放在OopMap数据结构中,JIT编译过程也会记录栈和寄存器中哪些位置是引用,这样JVM遍历OopMap就知道哪些是GC Roots了。
什么是OopMap?
https://stackoverflow.com/questions/26029764/what-does-oop-maps-means-in-hotspot-vm-exactly
OopMap是记录对象索引在Java栈的哪些位置的数据结构,OopMap的首要作用是查找GC Roots;当对象从堆中删除时,其索引也被删除了,所以OopMap要在需要的时候更新reference纪律!
问题来了:什么时候更新OopMap?
安全点GC safepoint
http://blog.ragozin.info/2012/10/safepoints-in-hotspot-jvm.html
查找OopMap就知道GC Roots了,问题是程序一直在运行,可以导致OopMap变化的指令很多,什么时候记录(更新)OopMap?
你不知道什么时候应该GC,如果时刻记录OopMap,那么内存成本很高!
所以这个问题可以转化成-JVM应该什么时候GC?
因为GC的时候才要查找GC Roots,查找GC Roots才需要OopMap!
那什么时候才应该GC呢?
答案:safepoint的时候!
http://www.sczyh30.com/posts/Java/jvm-gc-safepoint-condition/
什么是GC safepoint?
https://stackoverflow.com/questions/19201332/java-gc-safepoint
safepoint的实现机制取决于JVM的实现机制!
HotSpot中safepoint指的是: JVM在GC之前暂停程序来进行垃圾回收的机制!
Safepoint期间,所有java线程停止,native如果不和JVM交互则不用停止,所有GC Roots是确定的,引用和对象的关系链也是确定的,在这个期间垃圾回收程序回收java堆上无引用的对象!
什么时候是safepoint呢?
如果要触发一次GC,那么JVM中所有Java线程都必须到达GC Safepoint。
JVM只会在特定位置放置safepoint,比如:
1)内存分配的地方(allocation,即new一个新对象的时候)
2)长时间执行区块结束的时刻(如方法调用,循环跳转,异常跳转等)
Safepoint的时刻就是GC的时刻!GC的时刻就是Stop-the-world时刻!
所以safepoint时刻就是STW时刻!
这样,JVM只在程序运行到1),2)情况时才记录/更新OopMap;
问题来了,如何让所有线程都到达自己最近Safepoint呢?
如何让所有线程同时到达自己最近Safepoint?
两种方式,抢占式和主动式是从线程的角度说的!
1)抢占式中断-理论方法没有JVM实现
GC发生时,首先把所有线程全部中断,遍历检查所有线程,如果发现有线程不在safepoint,则恢复该线程,知道它跑到安全点上!直到所有线程到safepoint上,进行垃圾回收。
2)主动式中断-大部分JVM实现
GC发生时,JVM不中断所有线程;所有线程检查自己是否在safepoint上,如果在则在自己线程上做一个标志,并且线程自动暂停;所有线程依自身情况先后主动暂停自己;
然后进行垃圾回收!
Safepoint有什么用呢?
Garbage collection pauses垃圾回收暂停
Code deoptimization代码优化
Flushing code cache刷新代码缓存
Class redefinition (e.g. hot swap or instrumentation)类别重新定义(例如热插拔或仪器)
Biased lock revocation有偏见的锁定撤销
Various debug operation (e.g. deadlock check or stacktrace dump)
各种debug操作,如死锁检查,堆栈dump
GC safepoint机制有何漏洞?
主动式中断线程的方式Safepoint被大部分JVM实现,主动式中断要求一点:线程是清醒的执行的状态,如果线程处于阻塞状态或者等待状态怎么办?
JVM中程序不等人啊,程序要求一致运行!怎么办?
用Safe Region安全区域解决!
安全区域GC Safe region
什么是安全区域safe region?
Safe region指一段代码区域,这个区域中不会更改引用-对象的关系,所以在这个区域中任意位置都可以开始GC。
线程阻塞或者等待状态就不会更改引用对象的关系!
Safe region如何结合safe point工作?
1)在GC前,所有线程标注自己是否进入safepoint状态,是否进入safe region状态!
2)JVM不会去管进入safe region状态的线程;只需等待其他线程进入safepoint状态即可开始GC回收;
3)如果进入safe region状态线程要离开safe region状态,JVM先查看是否完成GC任务,完成则可以离开,否则不能离开!
垃圾收集器
图片来自https://crowhawk.github.io/2017/08/15/jvm_3/
http://zqhxuyuan.github.io/2016/07/26/JVM/ 多图
GC的一些概念
在垃圾回收中;
并行,指垃圾回收器并行多个线程同时执行;但垃圾回收器工作期间,程序STW;
并发,指用户程序与垃圾回收器并发执行;不一定是并行的,可能交替执行!
用户程序与垃圾回收器能并行同时执行吗?
不能全程同时执行;某些时刻t,需要用户程序STW,
吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值!
吞吐量=cpu运行用户代码时间/(cpu运行代码时间+cpu垃圾回收时间)
虚拟机运行100分钟,垃圾回收1分钟,吞吐量就是99%
GC的类型
https://www.zhihu.com/question/41922036
https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc
针对HotSpot的GC,分为两大类:
1)Partial GC:并不收集整个堆的模式
Young GC
只收集young代的GC
Old GC
只收集old代的GC,只有CMS是这个模式,其他说收集old的都会带着收集young
Mixed GC
收集整个young代+部分old代的GC,只有G1是这个模式
2)Full GC,收集整个堆,young代,old代,perm代(jdk8把perm移到了堆区)
一般Major GC指Full GC,也有人认为Major GC指Old GC,所以要问清楚他时候的到底是哪个!
Young GC又称Minor GC
什么是Minor GC?
清理年轻代(Eden,2Survivor)的Young GC又叫做Minor GC
何时触发Minor GC?
当Eden区满了,不能分配内存给new的对象时;
Minor GC的特点
1.Minor GC频繁发生
Eden经常满,对象经常死亡,所以Minor GC频繁发生
2.Minor GC后没有内存碎片
年轻代Minor GC采用复制算法,经过标记后,Eden区的存活对象被复制到2个(0,1)Survivor区的1个;
对于Eden区,内存指针可以从0开始,没有内存碎片;
对于Survivor区,2个区域是没有先后顺序的,一个使用,另外一个就用来复制;所以Survivor的1区域内存指针也总是从0开始的,1个Survivor区没有内存碎片;
3.Minor GC清理年轻代,老年代也不会被清理。
Minor GC采用复制算法,标记-复制的标记阶段中,从年老代指向年轻代的引用被认为是有效的引用,而从年轻代指向年老代的引用则认为是无效的引用!
4.Minor GC的 STW时间和Eden中垃圾对象的多少有关
Full GC
Major GC和Full GC是非官方的说法。
Major GC说的就是Full GC
Full GC是清理young+old+perm(如果属于java 堆)的GC;
何时触发Full GC
https://stackoverflow.com/questions/24766118/when-is-a-full-gc-triggered
1)准备触发Minor GC时,发现Old区剩余的空间不够,如果不使用CMS,则触发Full GC。
因为只有CMS是单独收集Old区的!其他收集Old去的都会收集整个堆!
2)如果堆中有Perm代(jdk8),当Perm区不够时也会触发Full GC
3)System.gc( )触发Full GC
4)Heap dump带的GC也是Full GC
把Heap中数据生成dump文件来分析JVM故障。
5)调节young,old区域size时也触发Full GC;
Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。
回收策略
年轻代serial,parNew,Parallel Scavenge使用复制算法;
老年代Serial old,Parallel old使用标记-整理算法,CMS使用标记-删除算法;
垃圾收集器组合使用
不同厂商,不同JVM,实现的垃圾收集器也不同!
用户也会根据应用特点组合各年代所使用的的垃圾收集器!
有连线,说明可以组合使用!
否则,不能组合! Tenured gen老年代
注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge+Parallel Old收集器
Serial回收器
什么是serial垃圾回收器?
Serial是一个使用复制算法的单线程垃圾回收器,只使用单cpu,单线程执行GC,而且执行GC的过程必须暂停程序。
Serial垃圾回收适用情景?
1)JVM client模式下默认垃圾回收器!新生代收集器!
什么时候使用Serial垃圾回收期?
2)小应用,占用java堆内存少200M以内,停顿时间可以控制在100毫秒以内;这样不影响客户端体验
3)单核cpu或2核cpu;serial收集器没有线程交互的开销,专心做垃圾收集对于少核cpu来说效率较高!
4)与CMS配合使用
Serial垃圾回收器如何工作?
1)垃圾回收前STW,单线程对新生代Eden使用复制算法收集;
2)垃圾回收前STW,单线程对老年代Old使用标记整理算法收集;
ParNew 收集器
什么是ParNew垃圾回收器?
ParNew就是Serial收集器的多线程版本!暂停程序!也是复制算法
Serial垃圾回收适用情景?
1)新生代收集器
什么时候使用ParNew垃圾回收器?
JVM Server模式下默认垃圾回收器!
2)与CMS配合使用
CMS老年代收集器只能和Serial,ParNew收集器使用!
为什么使用ParNew回收器?
ParNew垃圾回收器如何工作?
1)垃圾回收前STW,多线程对新生代Eden使用复制算法收集;
2)垃圾回收前STW,单线程对老年代Old使用标记整理算法收集;
它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置
如何使用ParNew回收器?
1)使用-XX:+UseConcMarkSweepGC选项设置使用CMS进行老年代收集后,新生代默认就使用ParNew回收器!
2)强制指定使用ParNew回收器
-XX:+UseParNewGC选项
Parallel Scavenge收集器
Scavenge是清除的意思
什么是Parallel Scavenge垃圾回收器?
Parallel是一个使用复制算法的新生代垃圾回收器;并行多线程收集;
关注点
吞吐量优先
Parallel Scavenge收集器的目标是可控的吞吐量!
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值!
吞吐量=cpu运行用户代码时间/(cpu运行代码时间+cpu垃圾回收时间)
虚拟机运行100分钟,垃圾回收1分钟,吞吐量就是99%
停顿时间越短,越适合需要与用户交互的程序!需要高吞吐量!!
Parallel Scavenge的应用情景?
新生代收集器
什么时候使用Parallel Scavenge垃圾回收器?
为什么使用Parallel Scavenge垃圾回收器?
Parallel Scavenge垃圾回收期如何工作?
如何使用Parallel Scavenge回收器?
Parallel Scavenge提供了2个参数,用于控制吞吐量,分别是:
1)最大垃圾收集停顿时间:
-XX:MaxGCPauseMillis 数值>0
收集器尽量保证回收时间不超过该值,具体停顿时间由实际情况而定!
正常情况下,垃圾回收的停顿时间是不会改变的,除非你调小新生代内存的大小!如收集500M和收集300M相比,肯定后者时间少,但是后者肯定收集的更频繁!原来10S收集一次,每次停顿100毫秒;现在每5秒收集一次,每次停顿70毫秒;停顿时间将下来,但整体吞吐量可能也降下来了!
2)吞吐量大小,直接设置吞吐量
-XX:GCTimeRatio 数值大于0小于100,默认是99
吞吐量=代码时间 /(代码时间+停顿时间)
3)Parallel Scavenge有自适应调节策略GC Ergonomics
-XX:+UseAdaptiveSizePolicy 是一个开关参数
设置后,虚拟机按照实际情况自动调节新生代Eden与Survivor的比例,自动调节今生老年代对象的年龄等参数!
动态调整这些参数来获得最大的吞吐量!
Parallel Scavenge收集器的特点
具有自适应调节策略!
Serial Old收集器
Serial收集器的老年代版本!使用复制-整理算法!
适用场景
1)Client模式的 老年代回收器!
2)Server模式下,作为CMS的备案。当CMS发生concurrent mode Failure使用!
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用标记-整理算法
关注点
吞吐量优先
应用情景
注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器组合!
如何使用Parallel Old收集器?
CMS收集器
什么是CMS收集器?
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
Concurrent Mark Sweep清扫,CMS使用并发的标记-清除算法
CMS的关注点
最小化GC停顿时间。
如何做到的?--这是寻找信息时给别人的回答
http://www.cnblogs.com/littleLord/p/5380624.html
首先,CMS垃圾回收器的设计目标 是 最小化 STW的时间,也就是最小化用户程序的暂停时间!
然后,要让程序运行,又要垃圾回收,想一个这种的办法就是让程序和垃圾回收同时运行!
但是,垃圾回收的可达性分析阶段,对不可到达对象的标记时必须要STW(因为对动态的reference-对象关系标记是没有用的);
问题来了:如何减少STW呢?
CMS的做法:
第一,初始标记,初始标记不是递归标记所有不可到达对象;而是从GC Roots出发,只标记第一级关联对象;需要STW,但是时间很短,因为第一级对象对于所有对象来说数量很少
第二,并发标记,让CMS垃圾回收程序和用户程序同时运行;所以没有STW;这次标记肯定是从第一级关联对象出发;搜索所有不能到达的对象;
这是一个很消耗时间的过程,但是因为用户程序在运行,所以没影响;
第三,重新标记,因为第二阶段用户程序在运行,可能产生新的对象;这一阶段需要把这些新产生的对象找出来;所以需要STW;
如果是我设计这个阶段的搜索,我一定会记录第二阶段什么时间点,用户新增了对象;这样查找新增的对象时间也不会很久;
第四,并发清理;直接删除第三阶段确定的所有不可到达对象;产生了内存碎片;
CMS如何工作?
工作流程分为4步:
1)初始标记CMS initial mark -STW
标记GC Roots能直接关联到的所有一级关联对象,会STW,时间短,因为GC Roots数量和整体对象数量相比还是小!
暂停,时间短
2)并发标记CMS concurrent mark
从GC Roots的一级关联对象出发,递归标记所有能标记的对象;
时间长,不暂停
3)重新并发标记CMS remark -STW
重新标记可到达对象;
为什么要重新标记?为什么还要STW重新标记?
因为2)中并发标记时程序运行,可能改变reference-对象的关系;所以如果想要一个确定的reference-对象的关系,必须STW。
时间长,暂停
4)并发清除CMS concurrent sweep
并发清除所有垃圾对象
如何使用CMS?
1)使用CMS收集Old
-XX:+UseConcMarkSweepGC选项,则老年代使用CMS,然后默认年轻代使用ParNew收集器。
2)设置Old内存占满多少触发CMS垃圾回收
可以用-XX:CMSInitiatingOccupancyFraction值来设定老年代内存达到多少百分比来触发CMS垃圾回收!
但是,该值设置多少合适呢?
该值设置太低,CMS垃圾回收太频繁;
设置太高,预留给程序内存不足,CMS回收失败,触发Serial Old回收,停顿时间更长
3)解决CMS碎片问题
使用-XX:+UseCMSCompactAtFullCollection(默认开启);用于CMS顶不住要进行FullGC时开启内存碎片的合并整理过程,空间碎片没有了,但是STW停顿时间变长了。
4)设置执行多少次不压缩的FullGC后压缩
使用-XX:CMSFullFCsBeforeCompaction,默认值为0,每次进入Full GC都碎片整理。
CMS启动多少线程进行并发回收?
CMS默认启动回收线程数 tnum=( Cpu数量+3 ) / 4
也就是说,即便不+3, CMS也要占用1/4的Cpu资源。
CM优缺点
优点:
1)用户程序STW停顿时间少;
缺点:
提升CMS的吞吐量(提升程序代码运行时间)是以牺牲程序的性能为前提的
是由于GC和程序同时运行导致。
1)CMS收集器对Cpu资源敏感,并发收集阶段会发生于用户程序抢Cpu资源情况。
并发收集垃圾过程一般占用1/4 cpu资源!
2)CMS失败Concurrent Mode Failuer用户程序也暂停,可能导致另一次Full GC的产生!
Why CMS失败导致Full GC?
3)CMS使用并发标记-清除算法,产生大量内存碎片。
为什么CMS失败会触发Full GC?
1)并发收集阶段,由于用户程序也要运行,所以,需要给程序预留足够内存空间;
2)因此,CMS收集器不能等老年代填满再收集,因为老年代也需要预留空间给程序。
JDK1.5默认设置下,当老年代使用68%空间则CMS就会被激活;68%的设置可以用
-XX:CMSInitiatingOccupancyFraction值来设定。
JDK1.6,默认92%老年代使用激活CMS回收;这时如果CMS运行期间程序所需老年代内存>8%,则会发生Concurrent Mode Failure失败,
这时,JVM启动备案,临时启用Serial Old收集器来重新进行老年代垃圾回收!
什么是浮动垃圾Floating Garbage?
CMS并发清理回收垃圾的阶段,程序是运行的;运行就可能产生新的reference-对象;
所以,浮动垃圾,就是CMS并发清理过程,由用户程序新生成的对象!
CMS无法处理浮动垃圾Floating Garbage,只能下次CMS时回收!
增量式并发收集器I-CMS
在并发标记,清理的时候让GC线程,用户程序交替运行,尽量减少GC线程独占资源的时间!
后果:整个GC过程会更长,但是对用户程序影响就显得少一些,速度下降没有那么快!
使用效果也不太好!
官方设置为deprecated,不再提倡用户使用!
G1收集器-通用收集器
什么是G1收集器?
Garbage First Collector,简称G1 Collector;是Hotspot1.7以后面向大内存(Heap区n~10G)、多核系统的收集器。
那到底什么是G1收集器?
G1是一种可以操作全堆的,并发、并行、部分Stop The World、使用Copying算法收集region的分代的增量式收集器!
G1在JDK1.9中成为默认收集器!
Region?啥是Region?
Region是区域的意思,就是内存的一段地址区间;G1算法将Heap区域分成独立等大的Region,G1兼收代的概念,每个Region属于一个代(可能不同部分),如E代表Eden的region,O代表Old代的region等等。
操作全堆?
能收集young和old,而不是像CMS只能收集old。
并发?
未
并行?
部分STW?
使用Copying算法?
G1将全堆内存分割成等大小独立的Region,在GC阶段,A的region存活对象被Copying到B Region。
问题来了?
Region是内存,JVM如何记录对象内存地址的变化的?
分代?
G1中兼收Young代(Eden,Survior),Old代的概念;不过G1分割过的堆,代的地址空间不是连续的了!很可能一个region是Eden的,右边相邻的region是Old代的!
增量收集?
G1的设计目标/关注点是最小化STW;为了实现这一目标,G1把Heap堆切分为很多Region,然后根据Region回收的价值(如最多垃圾对象最先回收)回收相应Region来解决内存不足的问题,而不是以往老套算法整代区域都要GC。
为什么叫G1收集器?怎么不叫G2呢?
Garbage First,First是啥意思?
这里的First是指回收价值最高;
那怎么叫回收价值最高呢?
肯定是垃圾对象占比越高的Region回收价值越高;
每次G1收集时会判断各Region的活性(存活对象的占比),垃圾对象占比越多回收价值越高,然后G1会参考按照之前回收某些Region消耗的时间,来估算这次回收哪些Region。用最小时间获取最大收益!所以叫Garbage First!
以什么为单位回收什么呢?
Region;
合起来就是:对回收价值最高的Region进行回收!
为什么要分region呢?
这个思想来源是分治思想,还是要回归到G1的设计目标:最小化STW!
先不说标记,光说回收!收集一整代时间长?还是收集一整代的某一部分时间长?
肯定是后者时间短,所以就分region了!
这种将Heap区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。
为什么需要G1收集器?GC三个性能指标
Hotspot之前已经携带了Serial, Paralel, CMS等收集器,为什么还需要研发一个新的G1呢?垃圾收集的三个性能指标: footprint, max pause time, throughput似乎像CAP一样不能同时满足。
在服务端更注重的是短停顿时间,也就是stop-the-world的时间,另外一段时间内的总停顿时间也是一个衡量指标。
Mark-Sweep, Mark-Compact均需要和清理区域大小成比例的工作量,而Copying算法则需要一般是一半的空间用于存放每次copy的活对象。CMS的Initial Marking和Remarking两个STW阶段在Heap区越来越大的情况下需要的时间越长,并且由于内存碎片,需要压缩的话也会造成较长停顿时间。所以需要一种高吞吐量的短暂停时间的收集器,而不管堆内存多大。
标记:从某一时刻t的对象图快照开始标记;
然后标记程序和用户程序同时执行;需要记录t时刻用户程序新增了哪些对象,并发标记过程中这些对象都被认为是存活对象,不会对它们进行标记
G1的实现方式
Region
避免长暂停时间,可以考虑将堆分成多个部分,一次收集其中一部分,这样的方式又叫做增量收集(incremental collection), 分代收集也可以看成一种特殊的增量收集。
G1收集器将堆内存划分为一系列大小相等的Region区域,Region大小在1MB到32MB在启动时确定,G1同样也使用分代收集策略,将堆分为Eden, Survivior, Old等,只不过是按照逻辑划分的,每个Region逻辑上属于一个分代区域,并且在物理上不连续,当一个Old的Region收集完成后会变成新可用Region并可能成为下一个Eden Region。当申请的对象大于Region大小的一半时,会被认为是巨型对象,巨型对象默认会被放在Old区,但如果巨型对象知识短期存在,则会被放入一个Humongous Region(巨型区域)中。当一个Region中是空的时,称为可用Region或新Region。
为什么要把堆切分为region?
JVM指标:
1)JVM的吞吐量,
2)GC暂停时间,
3)垃圾对象回收时间;
4)程序占用内存;
G1需要在这4个JVM指标中取舍!
为什么需要G1时说明白一点,需要一种不管堆内存多大情况下都能够高吞吐量,短暂停时间的垃圾回收器!
所以G1选择了吞吐量,暂停时间;舍弃了垃圾对象回收时间;程序占用内存;
高吞吐量怎么获得?提升年轻代大小,从程序开始运行到垃圾回收的的时间就变长了;
年轻代变大了,回收年轻代时间也变久了,STW时间也变大了!怎么办?
分治,把堆内存分为等大小的内存区域Region;然后按照Region垃圾对象占比多少选择回收价值最高的region来进行GC;对于一次GC,不会回收整代内存(不连续也没必要,因为涉及目标是缩短暂停时间),而是只回收某些Region,回收的内存足以支撑程序运行即可;
这样以回收某些region,回收多次的方式,这样维持了一个较高JVM的吞吐量,但减小了单次GC暂停时间;
如何找到所有的GC Roots对象?
大部分GC Roots对象都是Old对象吗?
是
是否要扫描Old代的全部region?
老年代的所有对象都是根么?这样扫描下来会耗费大量的时间
Remember Set
G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个区域内的对象引用。这样就可以用RS以Region为单位GC,而不用扫描整个堆GC。
一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
HashTable结构:
RememberSet数据结构:region起始地址(0~11)-region的CartTable
因为1个region对应1个CartTable,所以hashTable中每个key只有1个item,item是CartTable,CartTable是字节数组,字节数组记录了被外部region引用的Cart
RS主要存放:1)old到young的引用;2)old到old的引用
CMS中的RSet-Point Out
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
G1中的RSet-Point In
其他的region中对象引用我自己region中的对象,自己region中对象属于哪个卡表,记录哪些卡表的索引
从region角度,有2种信息,分别是:我引用了谁?point-out,谁引用了我point-in?
从回收region的角度,肯定是谁引用了我更有价值!!
所以在G1中,并没有使用point-out,这是由于G1分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
RSet for Refuib2中每个红格结构: region开始地址-该region的cardTable
Card Table卡表
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间,用来存放对象?应该是。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即
以上是关于垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章