jvm 在进行对象回收之前首先要进行搜索的,垃圾对象被搜索出后在合适的时候进行回收。因此垃圾的搜索对应的也有相应的算法
概念:给每个对象添加一个计数器,当有引用指向这个对象时计数器加一,当指向该对象的引用失效时计数器减一,当计数器的数值为零时代表对象没有被使用。
好处:
垃圾对象实时回收:程序运行时根据对象的引用计数器数值就可判断对象是否为垃圾。
垃圾回收无需挂起(无需为后台进程)。垃圾回收时内存不足可直接报OOM,
缺点:
1、当对象被引用时,要去更新下计数器的数值。这点是额外的时间开销。
2、很难解决对象之间的相互引用问题,这也是JVM放弃这个算法进行内存管理的原因。
/**
* 假设jvm 采用引用计数器来管理内存。
* */
void mockGc() {
Person personA = new Person();// 假设产生对象为a计数器为aCount=0,此时a被personA引用aCount=1
Person personB = new Person();//假设产生对象为b计数器为bCount=0,此时b被personB引用bCount=1
personA.instance = personB; //bCount=2(a对象的成员持有b的引用)
personB.instance = personA;//aCount =2(b对象的成员持有a的应用)
personA = null;//aCount =1
personB = null;//bCount =1
System.gc(); // 就算此处真的gc,对象a,b的计数器对象不为0,回收不了。
}
class Person {
Object instance;
}
personA想要垃圾回收:personB.instance 这个引用必须清除,而想要清除personB.instance,personB必须不被引用。然而personB又被personA.instance引用。同理personB想要被回收也是这样推理。至此造成循环。
概念:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链。当一个对象到GC Roots没有任何引用链,则证明此对象是不可用。
Person person = new Person();
....
....
person = null;
如上person 作为强引用存在虚拟机栈中,而new Person()作为对象存在于堆中,当person 的作用域结束,对应的虚拟机栈消失,person 引用也同时消失,但new Person()对象却仍然存在于堆中,“JVM必定不会回收这个对象” 。然而当person=null时,代表当前new Person()对象和GC Root切断关联。当jvm进行垃圾回收时就会回收这个对象了。
1、垃圾回收的主要区域是堆,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代(方法区)的垃圾收集效率远低于此。
2、Java虚拟机规范说过:“可以不要求虚拟机在方法区实现垃圾收集”
(1)方法区内主要回收:
1、废弃常量:当常量没有被任何引用引用时,当发生垃圾回收时,有必要进行回收时,这时常量就会被系统清理出常量池。
2、无用的类:无用的类需同时满足以下条件。
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
ps:满足以上3个条件时虚拟机可以对无用的类进行回收。这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制.
1、算法思想:算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
2、不足:
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
1、算法思想:为了解决效率问题,“复制”(Copying)算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
2、特点
- 优点:
a、实现简单、运行高效:内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。- 缺点:
a、代价高:将内存缩小为了原来的一半。
b、效率低:在对象存活率较高时就要进行较多的复制操作
c、需要额外的空间进行分配担保:如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况
现在的商业虚拟机都采用复制算法来回收新生代。新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间(from、to)每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
由于复制算法的缺点,所以老年代一般不采取这种算法,而是采取标记 - 整理算法。
1、算法思想:
首先标记出所有需要回收的对象。但后续步骤不是直接对可回收对象进行清除,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法。根据对象存活周期可将堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收
可达性分析对执行时间比较敏感的主要体现在GC停顿上,在整个可达性分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,不满足的话分析结果准确性就无法得到保证。这导致GC进行时必须停顿所有Java执行线程。Sun将这件事情称为“Stop TheWorld”
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
目前的主流Java虚拟机使用的都是准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构:在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
程序执行时并非在所有的地方都能停顿下来开始 GC,只有在到达安全点时才能暂停。
Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷,如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
那么如何让 GC 发生时所有的线程都跑到安全点上在停顿下来呢?有两种方案可供选择:
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是程序没有分配CPU时间时如线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
可以把Safe Region看做是被扩展了的Safepoint。
线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。