目录
标记阶段
- 注意:标记阶段标记的非垃圾对象!
引用计数算法
- JVM不采用这种算法
- 引用计数算法就是用一个整型属性存储被引用的情况,比如有3个引用,那就是3
- 引用计数算法无法处理循环引用
- 引用计数算法需要单独字段存储计数器 增加了存储空间的开销
- 引用计算算法每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 下面这种就是循环引用,如果采用引用计数算法,那当p置为null的时候,下面这三个对象就无法回收,因为始终有对象引用,导致内存泄漏
- 验证JVM是否使用的是引用计数算法,结果明显是不是的,因为发生了GC回收
- 小结
可达性分析算法(或根搜索算法、追踪行垃圾收集)
- 可达性分析算法就是用个GC Roots集合引用链,枚举这个集合,判断某个对象是否还能被链到,能链到就不能回收,不能链到就回收,关于什么能作为GC Root下面会说
- 什么能作为GC Roots
- 小技巧:一个指针指向了堆里面的对象,这个指针又不在堆中,那就是一个Root(静态变量可以算是一个特殊,即使在堆中,也是Root)。
- 注意:
对象的finalization
- 机制
- 虚拟机对象的三种状态 ,如果已经没有引用链了,调用finalize是有可能复活的,因为对象有三种状态;
- 没调用过finalize方法时,对象还会有一个缓刑阶段,能够调用finalize(每个对象只能调用一次)的状态,通常称为可复活状态;
- 调用过finalize方法后,如果对象又进入了缓刑阶段,就直接回收,因为finalize方法只能调用一次
- 具体过程
JProfiler的GC Roots溯源
- 用-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError 设置堆空间大小8m以及堆空间内存溢出后生成一个dump文件,在项目目录下
- 直接双击用JProfiler打开就能看到是哪个大对象溢出了
清除阶段(下面这些算法都会STW)
概述
标记-清除(Mark-Sweep)算法
- 标记阶段标记的是非垃圾对象,没被标记的才是垃圾对象;标记阶段是递归遍历GC Roots,时间复杂度也是近似O(n)了
- 清除就是把堆遍历一遍,发现没被标记就回收,回收也不是真的回收,就跟电脑磁盘一样,格式化并没有真的删除,只是把地址回收了,下次往里面存东西就把原有的数据覆盖这样。
- 综上:清除时时间复杂度O(n),效率并不是很高;这是缺点1,缺点2、3见下面图
- 清除出来的内存不连续,可以看到下面是散的,因此还需要维护一个空闲的地址列表,这是缺点2
- 缺点3就是需要停止整个程序,stop the world 体验差
复制(Copying)算法
- 复制算法就是把一块内存分成两个区域,之前的幸存者区就是使用的复制算法
- 如下图所示,复制算法从根节点开始递归遍历GC Roots的时候,把这些活着的对象全部复制到另外一个区域,复制完当前区域剩下的就全是垃圾了,直接全部回收,变成空闲区域,这样内存就是连续的,下次再递归遍历GC Roots的时候就把对象放到另外一个空区域
- 优点:可以说没有标记和清除过程,因为遍历过程就直接复制了,根本没有标记对象什么的;保证空间连续性,不会出现碎片问题
- 缺点:假设原本内存能放100个对象,用复制算法只能存50个对象,因为被一分为二了;第二个缺点:复制之后还需改变栈上对对象的引用指针,因为这是深拷贝,并不是移动。
- 特别注意:如果系统中存活的对象非常多,比如老年代中使用复制算法,那结果就是极有可能全部对象都还活着,那复制算法就得把全部对象复制一遍,再改变一下栈中的所有的引用,反而没有优点了,所以复制算法在新生代中使用,因为新生代对象都是朝生夕死的。
标记-压缩(或标记-整理、Mark-Compact)算法
- 也可以叫做标记-清除-压缩算法
- 其实跟标记-清除算法完全一样,只不过在标记-清除算法的基础上,多加了一个内存碎片整理阶段,把内存整理成连续的,方便对象的分配
- 优缺点如下
分代收集算法(前面算法结合)
- 年轻代的幸存者区就是用的复制算法
- 老年代是标记-清除、标记-整理算法相结合的
增量收集算法、分区算法
- 增量收集算法,就是专门有一个垃圾回收的线程,可以跟主线程交替执行,不会造成STW
- 分区算法
- 最后