计算机系统,包括内存最小的寻址单元是字节;说白了,虚拟机理论上最大内存就是硬件内存,硬件内存是有限的,你占用了,我就用不了了;所以对象不用的时候,回收其占用内存空间,以提高虚拟机资源利用率!让虚拟机有更高的产出!
程序计数器,栈区,本地方法栈区的生命周期都是和线程绑定的;线程消失,其占用的内存也就释放;
所以,垃圾回收作用的区域是堆内存(对象),方法区(常量);
基本类型在栈区,自动回收!
Stringstr=”abc”;在方法区常量池中会添加“abc”,后期如果有其他字符串值为”abc”,都会指向常量池中唯一的“abc”;
当没有String指向常量池中的“abc”时,就回收它了!
方法区回收性价比不高!
类需要同时满足下面3个条件才能算是“无用的类”:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2)加载该类的ClassLoader已经被回收。
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
在对象中,比如对象头中添加引用计数器,有一个地方引用它就+1;否则-1;计算引用计算器的值来确定对象是否被引用。
缺点:不能解决循环引用问题
JavaJVM是否实现:否,python中实现了。
从垃圾回收的根对象GCRoot触发,搜索根对象持有的所有成员变量对象Objs;再从所有成员变量对象Objs出发,搜索Objs持有的所有成员变量对象Objs2;搜索不可到达的对象就是可以垃圾回收的对象!
Stop-the-world,是说GC停顿,在执行算法时要求整个程序暂停,目的是对象引用链不能改变。
因为任何垃圾回收算法判断对象是否存活都使用可达性算法来分析,所以,任何垃圾回收算法,标记阶段都必须Stop-the-world。
1)虚拟机栈中引用的对象,(栈帧中本地变量表中引用的对象)
就是对象A的普通成员变量是对象B;A和B都在栈帧中,都可做GCRoot对象
2)方法区中类静态属性引用的对象;
就是对象A的静态成员是对象staticB,而staticB在方法区中,B中引用了对象的成员变量,那么B也作为GCRoot对象
3)方法区中常量引用的对象
就是对象A的成员对象是finalB,final修饰的B默认就是finalstaticB;B中引用了对象的成员变量,那么B也作为GCRoot对象
1)本地方法栈中JNI(native方法中)引用的对象
1)从GCRoots搜索所有不可到达对象
2)准备执行不可到达对象的finalize方法
如果finalize方法执行过,直接垃圾回收
如果finalize方法每执行过,执行它,然后垃圾回收
如果执行finalize方法的过程中,该对象被其他可到达对象引用了,则该对象逃脱!
/**
*此代码演示了两点:
*1.对象可以在被GC时自我拯救。
*2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*@authorzzm
*/
publicclassFinalizeEscapeGC{
publicstaticFinalizeEscapeGCSAVE_HOOK=null;
publicvoidisAlive(){
System.out.println("yes,iamstillalive:)");
}
@Override
protectedvoidfinalize()throwsThrowable{
super.finalize();
System.out.println("finalizemehtodexecuted!");
FinalizeEscapeGC.SAVE_HOOK=this;
publicstaticvoidmain(String[]args)throwsThrowable{
SAVE_HOOK=newFinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK=null;
System.gc();
//因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no,iamdead:(");
//下面这段代码与上面的完全相同,但是这次自救却失败了
运行结果:
finalizemehtodexecuted!
yes,iamstillalive:)
no,iamdead:(
SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。
另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。
并发标记中,就是用户程序和标记算法同时执行的过程,使用三色标记算法!
它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。首先,我们将对象分成三种类型的。
黑色:根对象,或者该对象与它的子对象都被扫描
灰色:对象本身被扫描,但还没扫描完该对象中的子对象
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色,没有引用的对象被置为白色
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理
问题来了:并发标记时用户程序更改对象引用关系了怎么办?
程序代码更改对象引用有2种方式:
1)增加对象引用,创建对象Objecto=newObject();或者o1=newObject()
2)删除对象引用,o=null;
所以并发标记保证应用程序在运行的时候,GC标记的对象不丢失,有如下2中可行的方式:
1)在新增对象时,记录对象的reference关系到可到达的对象结构中;
2)在删除的时候,从可到达对象结构中,删除对象的reference
刚好这对应CMS和G1的2种不同实现方式:
1)在CMS中,记录新增,不记录删除,采用的是增量更新(Incrementalupdate),只要在写屏障(writebarrier)里发现要有一个白对象的引用被赋值到一个黑对象的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
2)在G1中,记录删除,不记录新增,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
第1,在开始标记的时候生成一个快照图标记存活对象
第2,在并发标记的时候所有被改变的对象入队(在writebarrier里把所有旧的引用所指向的对象都变成非白的)
第3,可能存在游离的垃圾,将在下次被收集
WriteBarrier是并发标记时,对用户程序更改对象引用关系的一种监听机制,会把用户程序对对象-引用关系的更改记录到remembersetlog中,并在后期处理remembersetlog时告知GarbageCollector;方便垃圾回收器GC。
不同的是,你记录新增还是删除。CMS就记录新增,G1就记录删除!
writebarrier-apieceofcodeexecutedwheneveramembervariable(ofareferencetype)isassigned/writtento.Ifthenewreferencepointstoayoungobjectandit'sstoredinanoldobject,thewritebarrierrecordsthatfactforthegarbagecollect.Thedifferenceliesinhowit'srecorded.
具体可以看我之前的回答整理
GC的本质就是,从GCRoots触发搜索所有不可到达对象,然后执行这些对象的finalize()方法,然后再垃圾回收这些对象!
所以执行finalize()时,如果要回收的对象被可到达对象引用,则该对象逃脱GC。
Reference引用存储的是堆内存的地址
1)强引用Strongreference
2)软引用Softreference
什么是软引用类型?
软引用类型就是有用但非必须的对象
3)弱引用Weakreference
4)虚引用Phantomreference
引入引用类型的级别,是为了根据是否可以回收内存,把对象进行分类;哪些可以回收,哪些不能回收,这样让JVM的垃圾回收的目的更具体,更高效!
强引用类型就是这种,Objectobj=newObject()
当内存空间不足,抛出OutOfMemoryError错误
除非obj=null;否则只要强引用还在,垃圾回收器就不会回收引用的对象!
分2种情况
publicvoidtest(){
Objecto=newObject();
//省略其他操作
此时,执行test()方法时,test方法进入栈帧,对象o的reference引用在线程栈区创建,而reference指向的对象在堆区;test()方法执行完毕,test方法退出栈帧,对象o的引用自动随之清除,堆内存的Object对象也会被垃圾回收!
结论:不用管,自动清除!
成员变量Objectobj=newObject();
在成员变量使命达成后,未来不再使用,则手动设置obj=null,则对象将被垃圾回收!
一句话:除非你把我置为null,否则JVM是不会回收强引用类型的!(方法内除外)。
你可以保留我的位置,这样就不用重新分配内存了!
强引用在实际中有非常重要的用处,举个ArrayList的实现源代码:
privatetransientObject[]elementData;
publicvoidclear(){
modCount++;
//Letgcdoitswork
for(inti=0;i elementData[i]=null; size=0; 注意ArrayList私有成员变量elementData,在clear()执行时,把elementData数组中每个成员置为null;而不是把elementData置为null;这样数组中的对象都将被垃圾回收;而elementData数组因为是强引用,它的栈reference,在栈内存,数组对应(数组对象)空间在堆内存不变,避免后续调用add()或其他方法时还得重新分配数组对象空间! 软引用类型修饰的对象是有用但非必须的对象!什么意思?缺了软引用对象,程序也照常运行! JVM内存不足时,会清除软引用类型修饰的对象! 生命周期:创建到JVM内存不足,内存充足的时候也可能进行垃圾回收! Stringstr=newString("abc");//强引用 SoftReference 虚拟机会在内存溢出之前,回收掉软引用类型的对象str!内存不足时软引用类型对象自动被垃圾回收! 相当于,内存不足,JVM自动把str置为null,然后等到垃圾回收str。 If(JVM.内存不足()){ str=null;//转换为软引用 System.gc();//垃圾回收器进行回收 可以用来实现内存敏感的高速缓存! 一句话:有内存,我存在,没内存,我奉献我的内存!你可以在你需要时使用我,不需要我时清除我! 虚引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。 (1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新请求; (2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出 这时候就可以使用软引用 Browserprev=newBrowser();//获取页面进行浏览 SoftReferencesr=newSoftReference(prev);//浏览完毕后置为软引用 if(sr.get()!=null){ rev=(Browser)sr.get();//还没有被回收器回收,直接获取 prev=newBrowser();//由于内存吃紧,所以对软引用的对象回收了 sr=newSoftReference(prev);//重新构建 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。? 弱引用类型的对象是更不必须的对象! 软引用类型对象会在jvm内存不足时被垃圾回收! 弱引用类型对象会在垃圾回收线程发现它时就被回收,不论jvm内存是否不足! 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 生命周期:创建到第一次垃圾回收! 自动回收str,不论内存是否不足 Stringstr=newString("abc"); WeakReference Stringabc=abcWeakRef.get();//一句代码就把str转成强引用类型了 1)偶尔使用,随用随取的对象 如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用WeakReference来记住此对象。 2)引用一个对象又不想改变它的生命周期时,使用弱引用类型 publicclassReferenceTest{ privatestaticReferenceQueue publicstaticvoidcheckQueue(){ Reference while((ref=rq.poll())!=null){ if(ref!=null){ System.out.println("Inqueue:"+((VeryBigWeakReference)(ref)).id); publicstaticvoidmain(Stringargs[]){ intsize=3; LinkedList for(inti=0;i weakList.add(newVeryBigWeakReference(newVeryBig("Weak"+i),rq)); System.out.println("Justcreatedweak:"+weakList.getLast()); try{//下面休息几分钟,让上面的垃圾回收线程运行完成 Thread.currentThread().sleep(6000); }catch(InterruptedExceptione){ e.printStackTrace(); checkQueue(); classVeryBig{ publicStringid; //占用空间,让线程进行回收 byte[]b=newbyte[2*1024]; publicVeryBig(Stringid){ this.id=id; protectedvoidfinalize(){ System.out.println("FinalizingVeryBig"+id); classVeryBigWeakReferenceextendsWeakReference publicVeryBigWeakReference(VeryBigbig,ReferenceQueue super(big,rq); this.id=big.id; System.out.println("FinalizingVeryBigWeakReference"+id); 最后的输出结果为: Justcreatedweak:com.javabase.reference.VeryBigWeakReference@1641c0 Justcreatedweak:com.javabase.reference.VeryBigWeakReference@136ab79 Justcreatedweak:com.javabase.reference.VeryBigWeakReference@33c1aa FinalizingVeryBigWeak2 FinalizingVeryBigWeak1 FinalizingVeryBigWeak0 Inqueue:Weak1 Inqueue:Weak2 Inqueue:Weak0 与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。 虚引用类型必须和引用队列一起使用!PhantomReference类实现虚引用! 唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。 1)虚引用类型可以让你知道它指向的对象什么时候从内存中移除 设置了虚引用的对象在被垃圾回收时会接到系统发送的通知! 而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后在继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。 2)避免析构问题 引用类型 用途 强引用 从来不会 对象的一般状态 JVM停止运行时终止 软引用 在内存不足时 对象缓存 内存不足时终止 弱引用 在垃圾回收时 gc运行后终止 虚引用 Unknown Java7之基础-强引用、弱引用、软引用、虚引用 译文:理解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)遍历GCRoots,对所有存活对象标记 2)把所有存活对象,集中复制到内存某一端(最前或者最后)连续区域A 3)清理除了A外所有区域 优点:连续空间 缺点:效率不高,标记所有存活对象,并记录所有对象引用地址 经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新创建的对象存放在称为新生代(younggeneration)中(一般来说,新生代的大小会比老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被提升(promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。 把Java堆分为年轻代(Eden:Survior1:Survivor2=8:1:1),老年代,再加上方法区的永久代;一共3个区域; 按照区域的特点,分别有适合垃圾回收算法! 对象存活率低,复制算法; 对象存活率高,标记清理,或者标记整理 GCRoots主要存在于常量池全局变量,线程栈本地变量表的引用;但现在应用方法区有上百兆,如何高效查找到所有GCRoot? GC停顿后,并非遍历所有线程栈和方法区查找GCRoots;而是在类加载的过程中虚拟机就把对象什么偏移量上是什么类型变量计算出来,放在OopMap数据结构中,JIT编译过程也会记录栈和寄存器中哪些位置是引用,这样JVM遍历OopMap就知道哪些是GCRoots了。 OopMap是记录对象索引在Java栈的哪些位置的数据结构,OopMap的首要作用是查找GCRoots;当对象从堆中删除时,其索引也被删除了,所以OopMap要在需要的时候更新reference纪律! 问题来了:什么时候更新OopMap 查找OopMap就知道GCRoots了,问题是程序一直在运行,可以导致OopMap变化的指令很多,什么时候记录(更新)OopMap? 你不知道什么时候应该GC,如果时刻记录OopMap,那么内存成本很高! 所以这个问题可以转化成-JVM应该什么时候GC? 因为GC的时候才要查找GCRoots,查找GCRoots才需要OopMap! 那什么时候才应该GC呢? 答案:safepoint的时候! safepoint的实现机制取决于JVM的实现机制! HotSpot中safepoint指的是:JVM在GC之前暂停程序来进行垃圾回收的机制! Safepoint期间,所有java线程停止,native如果不和JVM交互则不用停止,所有GCRoots是确定的,引用和对象的关系链也是确定的,在这个期间垃圾回收程序回收java堆上无引用的对象! 如果要触发一次GC,那么JVM中所有Java线程都必须到达GCSafepoint。 JVM只会在特定位置放置safepoint,比如: 1)内存分配的地方(allocation,即new一个新对象的时候) Safepoint的时刻就是GC的时刻!GC的时刻就是Stop-the-world时刻! 所以safepoint时刻就是STW时刻! 这样,JVM只在程序运行到1),2)情况时才记录/更新OopMap; 问题来了,如何让所有线程都到达自己最近Safepoint呢? 两种方式,抢占式和主动式是从线程的角度说的! 1)抢占式中断-理论方法没有JVM实现 GC发生时,首先把所有线程全部中断,遍历检查所有线程,如果发现有线程不在safepoint,则恢复该线程,知道它跑到安全点上!直到所有线程到safepoint上,进行垃圾回收。 2)主动式中断-大部分JVM实现 GC发生时,JVM不中断所有线程;所有线程检查自己是否在safepoint上,如果在则在自己线程上做一个标志,并且线程自动暂停;所有线程依自身情况先后主动暂停自己; 然后进行垃圾回收! Garbagecollectionpauses垃圾回收暂停 Codedeoptimization代码优化 Flushingcodecache刷新代码缓存 Classredefinition(e.g.hotswaporinstrumentation)类别重新定义(例如热插拔或仪器) Biasedlockrevocation有偏见的锁定撤销 Variousdebugoperation(e.g.deadlockcheckorstacktracedump) 各种debug操作,如死锁检查,堆栈dump 主动式中断线程的方式Safepoint被大部分JVM实现,主动式中断要求一点:线程是清醒的执行的状态,如果线程处于阻塞状态或者等待状态怎么办? JVM中程序不等人啊,程序要求一致运行!怎么办? 用SafeRegion安全区域解决! Saferegion指一段代码区域,这个区域中不会更改引用-对象的关系,所以在这个区域中任意位置都可以开始GC。 线程阻塞或者等待状态就不会更改引用对象的关系! 1)在GC前,所有线程标注自己是否进入safepoint状态,是否进入saferegion状态! 2)JVM不会去管进入saferegion状态的线程;只需等待其他线程进入safepoint状态即可开始GC回收; 3)如果进入saferegion状态线程要离开saferegion状态,JVM先查看是否完成GC任务,完成则可以离开,否则不能离开! 在垃圾回收中; 并行,指垃圾回收器并行多个线程同时执行;但垃圾回收器工作期间,程序STW; 并发,指用户程序与垃圾回收器并发执行;不一定是并行的,可能交替执行! 用户程序与垃圾回收器能并行同时执行吗? 不能全程同时执行;某些时刻t,需要用户程序STW, 虚拟机运行100分钟,垃圾回收1分钟,吞吐量就是99% 针对HotSpot的GC,分为两大类: 1)PartialGC:并不收集整个堆的模式 YoungGC 只收集young代的GC OldGC 只收集old代的GC,只有CMS是这个模式,其他说收集old的都会带着收集young MixedGC 收集整个young代+部分old代的GC,只有G1是这个模式 2)FullGC,收集整个堆,young代,old代,perm代(jdk8把perm移到了堆区) 一般MajorGC指FullGC,也有人认为MajorGC指OldGC,所以要问清楚他时候的到底是哪个! 清理年轻代(Eden,2Survivor)的YoungGC又叫做MinorGC 当Eden区满了,不能分配内存给new的对象时; 1.MinorGC频繁发生 Eden经常满,对象经常死亡,所以MinorGC频繁发生 2.MinorGC后没有内存碎片 年轻代MinorGC采用复制算法,经过标记后,Eden区的存活对象被复制到2个(0,1)Survivor区的1个; 对于Eden区,内存指针可以从0开始,没有内存碎片; 对于Survivor区,2个区域是没有先后顺序的,一个使用,另外一个就用来复制;所以Survivor的1区域内存指针也总是从0开始的,1个Survivor区没有内存碎片; 3.MinorGC清理年轻代,老年代也不会被清理。 MinorGC采用复制算法,标记-复制的标记阶段中,从年老代指向年轻代的引用被认为是有效的引用,而从年轻代指向年老代的引用则认为是无效的引用! MajorGC和FullGC是非官方的说法。 MajorGC说的就是FullGC FullGC是清理young+old+perm(如果属于java堆)的GC; 1)准备触发MinorGC时,发现Old区剩余的空间不够,如果不使用CMS,则触发FullGC。 因为只有CMS是单独收集Old区的!其他收集Old去的都会收集整个堆! 2)如果堆中有Perm代(jdk8),当Perm区不够时也会触发FullGC 3)System.gc()触发FullGC 4)Heapdump带的GC也是FullGC 把Heap中数据生成dump文件来分析JVM故障。 5)调节young,old区域size时也触发FullGC; 年轻代serial,parNew,ParallelScavenge使用复制算法; 老年代Serialold,Parallelold使用标记-整理算法,CMS使用标记-删除算法; 不同厂商,不同JVM,实现的垃圾收集器也不同! 用户也会根据应用特点组合各年代所使用的的垃圾收集器! 有连线,说明可以组合使用! 否则,不能组合!Tenuredgen老年代 注重吞吐量以及CPU资源敏感的场合,可以优先考虑ParallelScavenge+ParallelOld收集器 Serial是一个使用复制算法的单线程垃圾回收器,只使用单cpu,单线程执行GC,而且执行GC的过程必须暂停程序。 1)JVMclient模式下默认垃圾回收器!新生代收集器! 什么时候使用Serial垃圾回收期? 3)单核cpu或2核cpu;serial收集器没有线程交互的开销,专心做垃圾收集对于少核cpu来说效率较高! 4)与CMS配合使用 1)垃圾回收前STW,单线程对新生代Eden使用复制算法收集; 2)垃圾回收前STW,单线程对老年代Old使用标记整理算法收集; ParNew就是Serial收集器的多线程版本!暂停程序!也是复制算法 1)新生代收集器 什么时候使用ParNew垃圾回收器? JVMServer模式下默认垃圾回收器! 2)与CMS配合使用 CMS老年代收集器只能和Serial,ParNew收集器使用! 为什么使用ParNew回收器? 1)垃圾回收前STW,多线程对新生代Eden使用复制算法收集; 它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置 1)使用-XX:+UseConcMarkSweepGC选项设置使用CMS进行老年代收集后,新生代默认就使用ParNew回收器! 2)强制指定使用ParNew回收器 -XX:+UseParNewGC选项 Scavenge是清除的意思 Parallel是一个使用复制算法的新生代垃圾回收器;并行多线程收集; 吞吐量优先 ParallelScavenge收集器的目标是可控的吞吐量! 新生代收集器 什么时候使用ParallelScavenge垃圾回收器? 为什么使用ParallelScavenge垃圾回收器? ParallelScavenge垃圾回收期如何工作? ParallelScavenge提供了2个参数,用于控制吞吐量,分别是: -XX:MaxGCPauseMillis数值>0 2)吞吐量大小,直接设置吞吐量 -XX:GCTimeRatio数值大于0小于100,默认是99 3)ParallelScavenge有自适应调节策略GCErgonomics -XX:+UseAdaptiveSizePolicy是一个开关参数 设置后,虚拟机按照实际情况自动调节新生代Eden与Survivor的比例,自动调节今生老年代对象的年龄等参数! 动态调整这些参数来获得最大的吞吐量! 具有自适应调节策略! Serial收集器的老年代版本!使用复制-整理算法! 1)Client模式的老年代回收器! 2)Server模式下,作为CMS的备案。当CMS发生concurrentmodeFailure使用! ParallelScavenge收集器的老年代版本,使用标记-整理算法 注重吞吐量以及CPU资源敏感的场合,都可以优先考虑ParallelScavenge+ParallelOld收集器组合! 如何使用ParallelOld收集器 ConcurrentMarkSweep清扫,CMS使用并发的标记-清除算法 如何做到的?--这是寻找信息时给别人的回答 然后,要让程序运行,又要垃圾回收,想一个这种的办法就是让程序和垃圾回收同时运行! 但是,垃圾回收的可达性分析阶段,对不可到达对象的标记时必须要STW(因为对动态的reference-对象关系标记是没有用的); 问题来了:如何减少STW呢? CMS的做法: 第二,并发标记,让CMS垃圾回收程序和用户程序同时运行;所以没有STW;这次标记肯定是从第一级关联对象出发;搜索所有不能到达的对象; 第三,重新标记,因为第二阶段用户程序在运行,可能产生新的对象;这一阶段需要把这些新产生的对象找出来;所以需要STW; 第四,并发清理;直接删除第三阶段确定的所有不可到达对象;产生了内存碎片; 工作流程分为4步: 1)初始标记CMSinitialmark-STW 2)并发标记CMSconcurrentmark 从GCRoots的一级关联对象出发,递归标记所有能标记的对象; 3)重新并发标记CMSremark-STW 重新标记可到达对象; 为什么要重新标记?为什么还要STW重新标记? 因为2)中并发标记时程序运行,可能改变reference-对象的关系;所以如果想要一个确定的reference-对象的关系,必须STW。 4)并发清除CMSconcurrentsweep 并发清除所有垃圾对象 1)使用CMS收集Old -XX:+UseConcMarkSweepGC选项,则老年代使用CMS,然后默认年轻代使用ParNew收集器。 2)设置Old内存占满多少触发CMS垃圾回收 可以用-XX:CMSInitiatingOccupancyFraction值来设定老年代内存达到多少百分比来触发CMS垃圾回收! 但是,该值设置多少合适呢? 该值设置太低,CMS垃圾回收太频繁; 3)解决CMS碎片问题 4)设置执行多少次不压缩的FullGC后压缩 使用-XX:CMSFullFCsBeforeCompaction,默认值为0,每次进入FullGC都碎片整理。 CMS默认启动回收线程数tnum=(Cpu数量+3)/4 也就是说,即便不+3,CMS也要占用1/4的Cpu资源。 优点: 缺点: 是由于GC和程序同时运行导致。 1)CMS收集器对Cpu资源敏感,并发收集阶段会发生于用户程序抢Cpu资源情况。 并发收集垃圾过程一般占用1/4cpu资源! 2)CMS失败ConcurrentModeFailuer用户程序也暂停,可能导致另一次FullGC的产生! WhyCMS失败导致FullGC? 3)CMS使用并发标记-清除算法,产生大量内存碎片。 1)并发收集阶段,由于用户程序也要运行,所以,需要给程序预留足够内存空间; 2)因此,CMS收集器不能等老年代填满再收集,因为老年代也需要预留空间给程序。 JDK1.5默认设置下,当老年代使用68%空间则CMS就会被激活;68%的设置可以用 -XX:CMSInitiatingOccupancyFraction值来设定。 JDK1.6,默认92%老年代使用激活CMS回收;这时如果CMS运行期间程序所需老年代内存>8%,则会发生ConcurrentModeFailure失败, 这时,JVM启动备案,临时启用SerialOld收集器来重新进行老年代垃圾回收! CMS并发清理回收垃圾的阶段,程序是运行的;运行就可能产生新的reference-对象; 所以,浮动垃圾,就是CMS并发清理过程,由用户程序新生成的对象! CMS无法处理浮动垃圾FloatingGarbage,只能下次CMS时回收! 后果:整个GC过程会更长,但是对用户程序影响就显得少一些,速度下降没有那么快! 使用效果也不太好! 官方设置为deprecated,不再提倡用户使用! GarbageFirstCollector,简称G1Collector;是Hotspot1.7以后面向大内存(Heap区n~10G)、多核系统的收集器。 那到底什么是G1收集器? G1是一种可以操作全堆的,并发、并行、部分StopTheWorld、使用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到BRegion。 问题来了? Region是内存,JVM如何记录对象内存地址的变化的? 分代? G1中兼收Young代(Eden,Survior),Old代的概念;不过G1分割过的堆,代的地址空间不是连续的了!很可能一个region是Eden的,右边相邻的region是Old代的! 增量收集? GarbageFirst,First是啥意思? 这里的First是指回收价值最高; 那怎么叫回收价值最高呢? 肯定是垃圾对象占比越高的Region回收价值越高; 以什么为单位回收什么呢? Region; 合起来就是:对回收价值最高的Region进行回收! 为什么要分region呢? Hotspot之前已经携带了Serial,Paralel,CMS等收集器,为什么还需要研发一个新的G1呢?垃圾收集的三个性能指标:footprint,maxpausetime,throughput似乎像CAP一样不能同时满足。 标记:从某一时刻t的对象图快照开始标记; 然后标记程序和用户程序同时执行;需要记录t时刻用户程序新增了哪些对象,并发标记过程中这些对象都被认为是存活对象,不会对它们进行标记 G1收集器将堆内存划分为一系列大小相等的Region区域,Region大小在1MB到32MB在启动时确定,G1同样也使用分代收集策略,将堆分为Eden,Survivior,Old等,只不过是按照逻辑划分的,每个Region逻辑上属于一个分代区域,并且在物理上不连续,当一个Old的Region收集完成后会变成新可用Region并可能成为下一个EdenRegion。当申请的对象大于Region大小的一半时,会被认为是巨型对象,巨型对象默认会被放在Old区,但如果巨型对象知识短期存在,则会被放入一个HumongousRegion(巨型区域)中。当一个Region中是空的时,称为可用Region或新Region。 JVM指标: 1)JVM的吞吐量, 4)程序占用内存; G1需要在这4个JVM指标中取舍! 大部分GCRoots对象都是Old对象吗? 是 是否要扫描Old代的全部region? G1引进了RSet的概念。它的全称是RememberedSet,作用是跟踪指向某个区域内的对象引用。这样就可以用RS以Region为单位GC,而不用扫描整个堆GC。 一般情况下,这个RSet其实是一个HashTable,Key是别的Region的起始地址,Value是一个集合,里面的元素是CardTable的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,在进行YoungGC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。 其他的region中对象引用我自己region中的对象,自己region中对象属于哪个卡表,记录哪些卡表的索引 从region角度,有2种信息,分别是:我引用了谁?point-out,谁引用了我point-in? 从回收region的角度,肯定是谁引用了我更有价值!! 所以在G1中,并没有使用point-out,这是由于G1分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。 RSetforRefuib2中每个红格结构:region开始地址-该region的cardTable 需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1中又引入了另外一个概念,卡表(CardTable)。一个CardTable将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间,用来存放对象?应该是。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个HashTable,Key是别的Region的起始地址,Value是一个集合,里面的元素是CardTable的Index。 通过Index(0~某数字)应该不能直接找到Card的内存地址,应该还有其他映射关系。 GC和用户程序并发执行时,用户线程什么时候修改跨region的引用?? 维护rememberedset需要mutator(用户程序)线程在可能修改跨Region的引用的时候通知collector,这种方式通常叫做writebarrier(和GC中的MemoryBarrier不同),每个线程都会有自己的rememberedsetlog,相当于各自的修改的card的缓冲buffer,除此之外还有全局的buffer,mutator自己的remembersetbuffer满了之后会放入到全局buffer中,然后创建一个新的buffer。 Snaphot-At-The-Beginning简称SATB 对象的分配策略。它分为3个阶段: 1)TLAB(ThreadLocalAllocationBuffer)线程本地分配缓冲区 2)Eden区中分配 3)Humongous区分配 TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。 对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。 阶段目标 G1收集器的标记阶段负责标记处存活的对象、并且计算各个Region的活跃度等。 标记算法 G1使用了一种Snaphot-At-The-Beginning简称SATB的标记算法,记录标记开始时的对象图的快照,之后并发收集过程中的新申请的对象都认为是存活对象。 快照标记,和CMS一样无法解决浮动垃圾问题! 何时标记 当堆使用比例超过InitiatingHeapOccupancyPercent后开始marking阶段,使用SATB记录marking开始阶段的对象图快照。 如何标记 G1使用bitmap标记哪些位置已经完成标记了,一个bitmap的bit表示8bytes,我们使用两个markingbitmap,一个previous、一个next, previousmarkingbitmap表示已经完成标记的部分,标记完成后会交换previous和next 标记阶段分为一下几个步骤: 标记周期的最开始是清除nextmarkingbitmap,是并发执行的。然后开始initialmarkingphase,会暂停所有线程,标记出所有可以直接从GCroots可以直接到达的对象,这是在YoungGC的暂停收集阶段顺带进行的。 找出所有的GCRoots的Region,然后从这些Region开始标记可到达的对象,是一个并发阶段。 这个阶段G1通过tracing找出整个堆所有的可到达的对象。这个阶段是并发执行的; 用户程序如果修改对象引用关系,则记录修改到Remembersetlog中Rslog; Finalmark是一个STW阶段,G1将所有的SATBbuffer处理完成。就是处理Rslog,并到更新可到达对象关系RememberSet中; marking的最后一个阶段,G1统计各个Region的活跃性,完全没有存活对象的Region直接放入空闲可用Region列表中,然后会找出mixedGC的Region候选列表。 和一般的分代式收集不同,G1中除了普通的YoungGC,还有MixedGC。 它仍然属于分代收集器。 新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。 老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。 这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。 收集目标:younggen的Eden区 何时触发: 当Eden区域无法申请新的对象时(满了),就会进行YoungGC, 收集过程: YoungGC将Eden和Survivor区域的Region(称为CollectionSet,CSet)中的活对象Copy到一些新Region中(即新的Survivor),当对象的GC年龄达到阈值后会Copy到OldRegion中。由于采取的是Copying算法,所以就避免了内存碎片的问题,不再需要单独的压缩。 应用程序是否暂停 YoungGC过程中,应用程序暂停。 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。 YoungGC阶段: 阶段1:根扫描 静态和本地对象被扫描 阶段2:更新RS 处理dirtycard队列更新RS,point-in的引用更新 阶段3:处理RS 检测从年轻代指向年老代的对象 阶段4:对象拷贝 拷贝存活的对象到survivor/old区域 阶段5:处理引用队列 软引用,弱引用,虚引用处理 GC对象:young+old 何时触发 当old区Heap的对象占总Heap的比例超过InitiatingHeapOccupancyPercent之后,就会开始ConcurentMarking,完成了ConcurrentMarking后,G1会从YoungGC切换到MixedGC, GC步骤 全局并发标记(globalconcurrentmarking) 拷贝存活对象(evacuation) globalconcurrentmarking的执行过程 在G1GC中,它主要是为MixedGC提供标记服务的,并不是一次GC过程的一个必须环节。globalconcurrentmarking的执行过程分为五个步骤: 1)初始标记(initialmark,STW) 根区域扫描(rootregionscan) G1GC在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非STW)同时运行,并且只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。 2)并发标记(ConcurrentMarking) G1GC在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW年轻代垃圾回收中断 3)最终标记(Remark,STW) 该阶段是STW回收,帮助完成标记周期。G1GC清空SATB缓冲区,跟踪未被访问的存活对象,并执行引用处理。 4)清除垃圾(Cleanup,STW) 在这个最后阶段,G1GC执行统计和RSet净化的STW操作。在统计期间,G1GC会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。 和CMS一样,G1的一些收集过程是和应用程序并发执行的,所以可能还没有回收完成,是由于申请内存的速度比回收速度快,新的对象就占满了所有空间,在CMS中叫做ConcurrentModeFailure,在G1中称为AllocationFailure,也会降级为一个STW的fullgc。 G1使用一种Snapshot-At-The-Begining的方式记录活对象,也就是那一时刻(整个堆concurrentmarking开始的时候)的内存的Objectgraph,但是在之后这里面的对象可能会变成Garbage,叫做floatinggarbage只能等到下一次收集回收掉。 1.在JDK7及以后,大内存(n~10G),多核系统 个人认为更换GC或者进行调优只能算是系统的锦上添花,并不能作为主要解决系统性能问题的关键,出现内存问题时,应当以修改应用代码为主、编写清晰的GC友好的代码,选择与应用场景合适的收集器可以提高系统的性能。 现在推荐从CMS更换到G1的一些情况如下: 2.Java堆的50%以上都是活对象 3.对象的分配速率变化很大 2)分代收集,整体上看是标记-整理算法,局部(region)上看是复制算法;