标签:强引用 obj 判断 搜索 一半 开始 c++ 先来 queue
首先来张祖传的思维导图:
内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的垃圾收集方式才能获得最好的性能。
GC发生在那里:
JVM虚拟机运行时内存区域主要分为(如下图):虚拟机栈、本地方法栈、程序计数器、Java堆、方法区。其中虚拟机栈、本地方法栈、程序计数器为线程私有区域,在这几个区域中就不需要过多的考虑垃圾回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,这两个区域为线程共享区域,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建那些对象,这两部分内存的分配和回收都是动态的,垃圾回收主要发生在这两部分区域。JVM内存区域解析可参考:xxxxxx
如何给对象分配内存:
详细讲解见:xxx
如何判断那些对象需要被回收:
现在主流的虚拟机主要使用可达性分析算法来判断对象是否可以被回收,而不是使用引用计数算法来判断,并行进行二次标记筛选来最终回收对象。
1、引用计数算法:
基本思想:给对象中加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0时对象就是不能被使用,可回收了。
例子:在发生gc时t1、t2对象是否会被回收?如果按照引用计数算法对象t1和t2之间相互引用,对象不会被回收,但是结果是要被回收的。说明虚拟机并不是使用引用计数算法来判断对象是否可以被回收。主要原因是引用计数算法很难解决对象之间相互引用的问题。
1 public class TestGC { 2 public Object instance = null; 3 private static final int oneMb = 1024 * 1024; 4 private byte[] bigSize = new byte[2 * oneMb]; 5 6 public static void test(){ 7 TestGC t1 = new TestGC(); 8 TestGC t2 = new TestGC(); 9 t1.instance = t2; 10 t2.instance = t1; 11 12 t1 = null; 13 t2 = null; 14 //在这里发生GC, t1和t2是否能被回收? 15 System.gc(); 16 } 17 }
2、可达性分析算法:
基本思想:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则说明该对象不可用,可以被回收。
可以作为GC Roots的对象包括:
(1)虚拟机栈中引用的对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中JNI引用的对象
可以理解为:
(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
无论是通过引用计数算法还是可达性分析算法判断对象,判断对象是否存活都与引用有关,java中的引用包括4种(由强到弱):强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
3、最终判定(二次标记筛选):
即使对象进行了可达性分析算法判定后,发现没有与GC Roots可达的引用链,此对象也不会立刻进行回收,还需要经历二次标记筛选过程,才能回收对象。
(1)第一次标记:如果某个对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机执行过,则这两种情况下,虚拟机会视为这对象“没有必要执行”,对象会被回收。
(2)第二次标记:以第一次标记情况相反,如果这个对象被判断为有必要执行finalize()方法,则将这个对象放入F-Queue队列中,此后虚拟机会自动建立一个低优先级的Finalizer线程对F-Queue中的对象进行二次标记,如果对象在finalize()方法中成功的重新与引用链上的任何一个对象建立关联,那么将把该对象移出“即将回收”的集合,该对象不会被回收,否则将被回收。每个对象都有finalize()方法(Object类里定义了该方法),finalize()方法是对象逃脱被回收的最后一次机会。
如何回收内存垃圾:
当然是使用各种垃圾垃圾收集算法来对内存进行清理,包括有:标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-整理算法(Mark-Compact)、分代收集算法(Generational Collection)。
1、标记-清除算法(Mark-Sweep):
基本思想:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程是根据可达性分析算法和二次标记过程来判断和标记。
缺点:(1)标记和清除两个过程效率不高;(2)标记清除后会产生大量不连续的内存碎片;内存碎片太多会导致以后在程序运行需要分配较大对象时,无法找到足够连续的内存空间而不得不提前触发另一次的垃圾回收动作。
2、复制算法(Copying):
基本思想:它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
复制算法主要应用于回收新生代,新生代内存划分为:Eden区、From Servivor区、To Servivor区,如图:
缺点:(1)内存缩小为原来的一半。(2)如果对象存活率较高时需要进行较多次的复制操作,效率会降低。(3)需要有额外的空间进行分配担保。
3、标记-整理算法(Mark-Compact):
基本思想:标记过程与标记-整理算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法(Generational Collection):
基本思想:根据对象的存活时间不同,将java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时都发现有大量的对象死去,只有少数存活,可以采用复制算法,只需要复制出少量存活的对象就可以完成收集。在老年代中对象的存活率高,没有额外空间进行分配担保,可以采用“标记-清除”或者“标记-整理”算法。当前商业虚拟机都是采用此算法。
垃圾收集器
如下图所示,java垃圾收集器主要有一下7种,作用于新生代:Serial收集器、ParNew收集器、Parallel Scavenge收集器。作用于老年代:Serial Old收集器、Parallel Old收集器、CMS(Concurrent Mark Sweep)收集器。G1(Garbage First)收集器主要面向服务端应用的垃圾收集器。两个收集器之间存在连线,说明它们可以搭配一起使用。
未完,待续...
本文参考:
《深入理解java虚拟机》周志明
注:本文主要是学习《深入理解java虚拟机》一书的读书笔记,以此来总结和学习。本文图片大部分来源于网络,侵删。
标签:强引用 obj 判断 搜索 一半 开始 c++ 先来 queue
原文地址:https://www.cnblogs.com/oush/p/11686671.html