垃圾回收阶段
- 标记阶段
- 引用计数法:对每个对象保存一个整形的引用计数器属性 用于记录对象被引用的情况
- 对于一个对象A 只要有任何一个对象引用了A 则A 的引用计数器就加1; 当引用失效时,引用计数器就减1 只要对象A 的引用计数器的值为0 即 表示对象A 不可能再被 使用 可进行回收
- 优点:实现简单,垃圾对象便于辨识 判断效率搞,回收没有延迟性
- 缺点:
- 它需要单独的字段来存储计数器,这样的做法增加了 存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和假发操作增加了时间的开销
- 引用计数器有个严重的问题 即 无法处理循环引用的情况,这是一条致命的缺陷,导致在java 的垃圾回收中没有使用着类算法
- 可达性算法
- 相对于 引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点 更重要的是 该算法可用有效地解决在引用计数算法中 循环引用的问题,防止内存泄漏的发生
- 相较于引用计数算法 这里的可达性分析就是 java c# 选择的 这种类型的垃圾收集通常也叫做追踪性垃圾收集
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上到下的⽅式搜索被根对象集合所连接的⽬标对象是否可达;
- 使⽤可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所⾛过的路劲称为引⽤链(ReferenceChain);
- 如果⽬标对象没有任何引⽤链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象;
- 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象
- 在Java语⾔中,GC Roots包括以下⼏类元素:
- 虚拟机栈中引⽤的对象: ⽐如各个线程被调⽤的⽅法中使⽤到的参数、局部变量等;
- 本地⽅法栈内引⽤的对象
- ⽅法区中类静态属性所引⽤的对象: Java类的引⽤类型静态变量
- ⽅法区中常量引⽤的对象: 字符串常量池中的引⽤
- 所有被同步锁Synchronized持有的对象
- Java虚拟机内部的引⽤:基本数据类型对象的Class对象、⼀些常驻的异常对象(如,NullPointerException、OutOfMemoryError)、系统类加载器;
- 除了这些固定的GC Roots集合以外,根据⽤户所选⽤的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象"临时性"地加⼊,共同构成完整的GC Roots集合。⽐如:分代收集和局部回收。
- 注意:如果要使⽤可达性分析算法来判断内存是否可回收,那么分析⼯作必须在⼀个能保障⼀致性的快照中进⾏。这点也是导致GC进⾏时,必须"Stop The World"的⼀个重要原因。
- 即使是号称⼏乎不会发⽣停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
- 引用计数法:对每个对象保存一个整形的引用计数器属性 用于记录对象被引用的情况
清除阶段:
- 标记-清除算法
- 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执⾏垃圾回收,释放掉⽆⽤对象所占⽤的内存空间,以便有⾜够的可⽤内存空间为新对象分配内存。
- 执⾏过程: 当堆中的有效内存空间被耗尽的时候,就会停⽌整个程序(Stop The World),然后进⾏两项⼯作,第⼀项是标记,第⼆项是清除。
- 标记:Collector从引⽤根节点开始遍历,标记所有被引⽤的对象,⼀般是在对象的Header中记录为可达对象;
- 清除:Collectot对堆内存从头到尾进⾏线性的遍历,如果发现某个对象在其Header中没有标记可达对象,则将其回收。
- 缺点:
- 效率不算⾼;
- 在进⾏GC时候,需要停⽌整个应⽤程序;
- 这种⽅式清理出来的空闲内存是不连续的,产⽣内存碎⽚,需要维护⼀个空闲列表;
- 注意:这⾥所谓的清除并不是真的置空,⽽是把需要清除的对象地址保存在空闲的地址列表中。
- 复制算法
- 将活着的内存空间分为两块,每次只使⽤其中⼀块,在垃圾回收时将正在使⽤的内存中的存活对象复制到未被使⽤的内存块中,之后清除正
在使⽤的内存块中的所有对象,交换两个内存的⾓⾊,最后完成垃圾收集。 - 优点:
- 没有标记-清除的过程,实现简单,运⾏⾼效
- 复制过去以后保证空间的连续性,不会出现碎⽚问题;
- 缺点:
- 需要两倍的内存空间;
- 需要维护对象的引⽤关系;
- 将活着的内存空间分为两块,每次只使⽤其中⼀块,在垃圾回收时将正在使⽤的内存中的存活对象复制到未被使⽤的内存块中,之后清除正
- 标记-压缩算法
- 复制算法的⾼效性是建⽴在存活对象少、垃圾对象多的前提下。这种情况在新⽣代经常发⽣,但是在⽼年代,更常见的情况是⼤部分对象都是存活对象,如果依然使⽤复制算法,由于存活对象多,复制的成本也将很⾼。因此,基于⽼年代垃圾回收的特性,需要使⽤其他的算法。
- 标记-清除算法的确可以应⽤在⽼年代,但是该算法不仅执⾏效率低下,⽽且在执⾏完内存回收后,还会产⽣内存碎⽚,所以JVM的设计者需要在此基础上进程改进,标记-压缩算法由此诞⽣。
- 执⾏过程:
第⼀阶段和标记-清除算法⼀致,从根节点开始标记所有被引⽤对象。
第⼆阶段将所有的存活对象压缩到内存的另⼀端,按顺序排放。
之后,清理边界外所有空间。
⼆者的本质差异在于标记清除算法是⼀种⾮移动式的回收算法,标记压缩是移动式的。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,⽽未被标记的内存会被清理掉。如此⼀来,当我们需要给新对象分配内存
时,jvm只需要持有⼀个内存的起始地址即可,这⽐维护⼀个空闲列表显然少了许多开销
- 分代收集算法
⽬前⼏乎所有的GC都是采⽤分代收集算法执⾏垃圾回收的。
在HotSpot中,基于分代的概念,GC所使⽤的内存回收算法必须结合年轻代和⽼年代各⾃的特点- 年轻代:
特点:区域相对⽼年代较⼩,对象声明周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象的⼤⼩有关。因此很适合于年轻代的回收。⽽复制算法内
存利⽤率不⾼,通过HotSpot中的两个Survivor的设计得以缓解。 - ⽼年代:
特点:区域较⼤,对象⽣命周期长、存活率⾼,回收不及年轻代频繁。
这种情况存在⼤量存活率⾼的对象,复制算法明显变得不合适。⼀般是由标记-清除或者是标记-清除与标记-整理的混合实现。
- 年轻代:
- 增量收集算法
- 基本思想:
如果⼀次性将所有的垃圾进⾏处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应⽤程序线程交替执⾏。每次,垃圾收集线
程只收集⼀⼩⽚区域的内存空间,接着切换到应⽤程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法、增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶
段的⽅式完成标记、清理或复制⼯作。 - 缺点:
由于在垃圾回收过程中,间断性的还执⾏了应⽤程序代码,所以能减少系统的停顿时间,但是因为线程切换和上下⽂转换的消耗,会使得垃
圾回收的总体成本上升,造成系统吞吐量的下降。
- 基本思想:
- 分区算法