咱们都知道对象死亡的时候须要进行垃圾回收来回收这些对象从而释放空间,那么什么样的对象算是死亡呢,有哪些方法能够找出内存中的死亡对象呢?通常来讲,咱们能够这样认为:若是内存中不存在对当前对象的引用,则此对象必定是死亡状态;可是死亡状态的对象并不必定没有其余对象进行引用(可能存在死亡对象循环引用的状况)。这里须要说明一下,死亡的对象并不必定会被回收释放占用的空间,这种状况就是常称的"内存泄漏"。断定对象存活的算法通常是如下两种。算法
引用计数法,即在对象内放置一个变量来表示这个对象被引用的次数,若是其余对象引用了当前对象则变量值+1,若是失去引用则-1,当变量值为0的时候表示没有引用,应当回收。此算法并无被Java采用,由于其存在着一个致命的问题——循环引用。安全
如上图中,栈中没有任何堆中两个对象的引用,而堆中的两个对象则互相持有对方的引用,若是使用引用计数法的话引用变量值永远不会为0,从而形成内存泄漏,两个互相引用的对象没法释放空间。数据结构
public class TestForGc { TestForGc testInstance; // 模拟上图的现象 public static void main(String[] args) { TestForGc testA = new TestForGc(); TestForGc testB = new TestForGc(); testA.testInstance = testB; testB.testInstance = testA; testA = null; testB = null; // 建议垃圾回收器进行回收操做 System.gc(); } }
而后设置-XX:+PrintGCDetails打印GC日志:
最终新生代的对象所有被回收,说明JVM使用的并非使用引用计数法来实现垃圾回收。并发
GCRoots,大意为选中一些特定的对象做为根节点,而后从这些根节点出发寻找能够引用到的全部对象,造成一条引用链(引用网),不在这条链中的对象则标记为死亡,进行回收。根节点的特定对象从下列四种产生:spa
一、虚拟机栈中引用的对象。线程
二、本地方法栈中引用的对象。3d
三、方法区中静态变量引用的对象。日志
四、方法区中常量引用的对象。code
使用GCRoots便不会出现循环引用的问题,如图,虽然A、B相互引用,可是因为不在根节点的引用链中,因此会被标记为可回收对象。对象
在Hotspot虚拟机对GCRoots算法的实现中,大体能够分为三个部分理解。
如上所说,根节点的选取对象有四处,若是虚拟机对这些位置进行全盘扫描的话,效率天然要影响很多,因此Hotspot采用一种数据结构——OopMap来解决这个问题。在类加载完成的时候,虚拟机将对象的什么偏移量有什么对象计算出来,在JIT编译过程当中在特定的位置记录下栈和寄存器中哪些位置是引用。这样一来GC在扫描的时候就能够直接获得这些引用的信息,从而减小GC的停顿时间。顺便一提,在枚举根节点的时候,为了保持“一致性”,不能再扫描的时候还出现对象引用变化的状况,因此须要暂停全部Java执行线程(被称为"STOP-THE-WORLD"),即使在具备划时代意义、能够并发执行的CMS收集器中在枚举根节点的时候也须要STW。
OopMap数据结构能够说为GC的扫描减小了很多的时间,可是随之而来的还有一个问题,若是每条指令都生成对应的OopMap,那么想必须要大量的额外空间,GC的空间成本将十分巨大,就是什么时候生成对应OopMap成为当前面临的问题。以前说过在特定的位置会记录下引用的位置,这个特定的位置就是OopMap的生成时机,也就是“安全点(SafePoint)”,在Sop-The-World的时候线程要先跑到安全点才能够进行线程的停顿。那该如何判断这个特定的位置呢?若是设置的太少可能会致使GC时间变长,设置的太多会增大运行时的负荷。Hotspot给出的答案是以程序“是否具备让程序长时间执行的特征”为标准进行选定。"长时间执行"的明显特征就是指令复用,例如方法调用、循环跳转、异常处理等,只有这些指令才能产生安全点。
对于安全点来讲,另一个问题就是采用什么样的方式让全部的线程跑到最近的安全点停顿。有两种实现的方式:
一、抢先式中断:在GC发生的时候首先暂停全部线程,若是发现有线程没在安全点的话,则恢复线程,让其跑到最近的安全点再进行暂停。如今已经不多有使用抢先式的了。
二、主动式中断:GC发生的时候不强制暂停线程,而是设置一个标识变量,线程会去轮询这个标志,若是为true则将本身中断挂起。这个轮询的位置和安全点是重合的,还有建立对象时须要分配内存的地方。
上面安全点的设置几乎已经解决了问题,可是还少了一点,就是创建在线程都是执行状态的时候,那线程不执行的时候呢,例如进入休眠状态的线程,这时候本身不能跑到安全点也不能等待JVM分配时间。此时就须要安全区域来解决这一点。
安全区域指的是在一段代码块中,引用关系不会发生变化。当程序走到安全区域的时候,则标识当前线程进入了安全区域。这时候发生GC的时候则能够不用管有安全区域标识的线程,而这些线程在快离开安全区域的时候必需要检查是否完成了根节点的枚举(或者整个GC的过程),若是完成了才能够离开安全区域,不然必须待到完成为止。
如今咱们知道哪些对象是死亡的,哪些对象应该回收,而这个回收有许多种实现的方式(算法),有的算法对死亡对象进行标记最后一并清除、有的算法将内存分块而后将存活对象从一头搬到另外一头,还有算法在清除完死亡对象贴心的将存活的对象整放在一起,这些都是咱们接下来要说的。
正如这个算法的名称通常,其总共有两个阶段——"标记"和"清除":首先其会对全部的死亡对象进行标记,最后再一块儿将这些对象回收。
这个算法是基础的算法,后续的算法都是对其缺点的一些改进。此算法有两个不足的地方,其一从上图也能够看得出来,垃圾回收后的内存空间不连续,形成许多的内存碎片。其二就是其效率问题,标记和清除的效率并非过高,因此后续出现的算法都对两个缺点的调整和改进。
为了解决效率和内存碎片的问题,一种称做"copy"的算法出现,这个算法将内存空间分红两份或以上,一份存放对象,一份空白,当进行垃圾回收的时候将全部的存活对象复制到空白的一份中,而后清空以前存放对象的空间。
上图也能够看得出,对此算法最重要的是内存空间的切分,若是切分不当可能会浪费大量的空间。固然使用得当也有十分值得的优势:必定范围内的高效率和没有内存碎片。
缺点:
一、适用于存活对象相较死亡对象少的状况,例如新生代,若是存活的对象较多的话可能获得相反的效果。因此才说是必定范围的高效率。
二、须要划份内存空间。若是自己的内存空间比较小还去划分的话那可能会致使频繁的GC,停顿时间增多,影响用户体验。
另:此算法通常用在新生代作垃圾回收算法,而且将新生代分红三个部分——两个Survivor和一个Eden区,其比例默认为1:1:8(能够经过虚拟机参数改变)。当咱们生成一个对象(经过关键字new或者反射)的时候,对象首先会分配在Eden区,等到Eden区放不下的时候则触发一次MinorGC,将Eden和其中一个Survivor中的存活对象一块儿移到另外一个Survivor中,而后清空。顺带一提,有存活对象的Survivor老是称做From区,空白的Suvivor老是称做To区,通常新生代存活对象占5%左右。
复制算法是一个很是优秀的算法,但其只在存活对象想多较少的状况下表现良好,而对于其余例如年老代中这些存活对象较多的区域则多是一种糟糕的选择。因此,须要一个更加合适的算法——标记-整理法。这个算法原理和步骤基本跟标记-清除同样,可是多出一个整理的步骤,也就是说整个过程为标记-清除-整理,结束以后不会产生内存碎片。
严格来讲这不能算是一种算法,应该是一种理念。其把整个内存空间分为两个区域——新生代和年老代(1.8以前还有一个永久代,也就是方法区,可是在1.8以后已经删除)。而且虚拟机对对象定义了年龄的概念,表示该对象熬过了多少次GC,以此来做为对象放在新生代仍是年老代的标准之一,默认新生代的对象15岁以后就能够进入年老代了。对于两个区域采用的回收算法也是不一样的,新生代通常采用复制算法,年老代通常采用标记-整理法,固然具体仍是得看使用的垃圾回收器,若是年老代使用的是CMS的话那么就是标记-清除了。
It is an honor if I could get some advices or corrections from you guys.