- 哪些内存需要GC
- 判断对象是否还存活
- 引用计数法
- 给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器的值就减1,任何时候计数器为0的对象就是不可能再被使用的。
- 微软公司的COM技术,使用ActionScript3的FlashPlayer等都使用了引用计数算法来进行内存管理。
- 但是Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因时它很难解决对象之间相互循环引用的问题
- 可达性分析算法:
- 在主流的商用程序语言(Java, C#, Lisp等)都是通过可达性分析算法来判断对象是否存活的。
- 通过一些“GC Roots” 的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。
- Java中可作为GC Roots 的对象包括:
- 虚拟机栈(栈中的本地变量表) 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般说的Native方法)引用的对象
- 在可达性分析算法中不可达的对象,仍有机会“自救”。一个对象真正被判断死亡,至少要经历两次标记过程。
- 在可达性分析算法后发现没有与GC Roots 相连接的引用链,将会第一次被标记并且进行一次筛选,条件为是否有必要执行finalize()方法。
- 如果对象没有覆盖这个方法,或者已经被虚拟机调用过了,则视为没有必要执行
- 自救:只要在finalize()方法中重新与引用链上的任何一个对象建立关联即可
- 引用计数法
- 判断对象是否还存活
2.什么时候进行GC
- 可达性分析过程中必须保证一致性,也就是指在分析过程中,对象的引用关系不能发生改变,导致GC进行时,必须停顿所有Java执行线程。
- 而当执行系统停顿下来之后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置。使用一组称为OopMap的数据结构来记录哪些地方存放着对象引用。
- 在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
- 并不是什么地方都能停顿下来进行GC,只有到达安全点时才能暂停,那么如何让所有线程都到了最近的安全点再停顿下来呢?
- 抢先式中断:不需要线程的执行代码主动配合,GC发生时,首先中断全部线程,再恢复不在安全点上的线程,让它跑到安全点上。
- 主动式中断:当GC需要中断线程时,不直接对线程操作,而是仅仅简单设置一个标志,各个线程执行时,主动去轮询这个标志,发现标志为真时就主动中断挂起,而轮询标志的地方是与安全点重合的。
- 安全点机制
- 但是如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
- 因此设置一个安全点机制,只有在达到安全点时才能暂停。既不能太少以致于让GC等待时间太长,也不能太过于频繁以至于过分增大运行时的负荷.
- 安全区域机制
- 使用安全点机制似乎已经完美解决了如何进入GC的问题,但是这个机制能保证程序执行时,在不太长的时间内就会遇到可进入GC的安全点,但是程序不执行的时候呢
- 这个时候,就需要安全区域来解决了。安全区域是指在一段代码片段中,引用关系不会发生改变。在这个区域中的任意地方开始GC都是安全的。
- 安全点机制
3.如何进行GC
- 标记-清除算法
- 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 不足:
- 效率问题,标记和清除两个过程的效率都不高,
- 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作
- 复制算法
- 将可用内存容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完后,就将还存活者的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。
- 这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针。按顺序分配内存即可,实现简单,运行高效。
- 问题:代价是将内存缩小为了原来的一半
- 改进:
- 目前商业虚拟机都采用这种收集算法来回收新生代,而研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要1:1的比例来划分。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。
- 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间
- 这样只有10%的内存会被“浪费”
- 当然当出现Survivor空间不够用时,需要依赖其他内存(这里只老年代)进行分配担保
- 标记-整理算法
- 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法
- 根据对象存活周期的不同将内存划分为几块,一般是将java堆分为新生代和老年代,这样就可以根据每个年代的特点采用最适当的收集算法。
- 在新生代中,每次垃圾收集都会有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 而老年代因为对象的存活率较高,且没有额外空间对它进行担保,就必须使用标记-清除或者标记-整理算法来进行回收
- 标记-清除算法