JVM的垃圾收集器策略

说明:本篇属于读书笔记,大量参考《深入理解Java虚拟机》

判断对象不可用的几种方式

引用计数量法

  • 通过计算对象被引用的次数来判断该对象是否还有被引用,这种方式的判断效率较高,因为判断逻辑比较简单嘛,但是无法解决对象之间的循环引用问题,加入A对象引用了B对象,而B对象也引用了A对象,而A对象和B对象不再被其他对象所引用,那么A和B对象都是不可达的,但是引用计数法会导致A和B的引用计数都不为0,所以不会被垃圾收集器回收,因此JVM主流的虚拟机都不会用这种方式来判断对象是否可达

可达性分析算法

  • 可达性分析算法就是所谓的Gc Roots,在MAT的内存泄漏分析中就是根据对象是否存在Gc Roots来判断对象是否还存在引用,从而确定对象是否泄漏;当一个对象到Gc Roots没有任何引用链的时候就认为该对象不可达,不可达的对象就是可回收对象
  • 在Java中,可作为Gc Roots的对象包括以下几种:
  1. 虚拟机栈中的引用对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法区中的JNI引用的对象
    为什么以上四种可以作为Gc Roots?因为以上四种在Java程序运行起来之后就基本不会被回收

Java中的对象引用

  • 强引用,new出来的对象就是属于强引用,只要对象存在强引用,垃圾回收器就不会回收该对象
  • 软引用,如果对象存在软引用,垃圾回收器在发生Gc操作的时候并不会被回收,而是在程序内存紧张的时候才会被回收,例如抛出OOM之前会被回收,SoftReference类来实现软引用
  • 弱引用,如果对象存在弱引用,那么在每次垃圾回收器发生Gc操作的时候都会被回收,WeakReference来实现弱引用
  • 虚引用,虚引用是最弱的引用关系,虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中,虚引用由PhantomReference类来实现,如下:
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

对象是否可以被回收的判断条件

  • JVM发现对象到Gc Roots已经没有引用链了,这时候对象会被第一次标记,并判断该对象是否有必要执行finalize()方法,如果对象没有实现finalize()方法或者finalize()方法已经被JVM调用过那JVM认为没有必要执行finalize()方法,如果对象没必要执行finalize()方法的话就满足被回收的条件,如果对象还需要被执行finalize()方法,那对象会被放到F-Queue队列中并由低优先级的Finalizer线程区执行finalize()方法,但是并不会等待finalize()方法执行结束,因为如果finalize()方法中的实现存在问题,例如卡死,那么会导致F-Queue队列都处于等待状态,这将导致整个JVM内存回收都会卡住,这绝对是不允许的,所以在对象被回收的时候并不保证finalize()方法一定会被执行到,只能说对象被回收之前通常会执行finalize()方法,如果对象有重载该方法

方法区的回收

  • 方法区通常存储类的信息,例如常量,方法等,因此这些区域一般不会发生垃圾回收,也就是通常所说的永久代,但是其实永久代也是有垃圾回收的,只不过回收率很低,永久代的垃圾回收主要是回收废弃常量和无用类
  • 废弃常量,所谓的废弃常量是指程序中是否还存在引用该常量的String对象,如果没有,那么该常量就是废弃常量,垃圾回收器发生Gc的时候就会回收
  • 无用类,无用类指的是该类的所以实例都已经被回收,加载该类的ClassLoader也已经被回收,该类对应的Class在程序中没有被引用,基于以上三点,通常情况下是不会存在无用类的,因为加载该类的ClassLoader通常是不会被回收,但是一些动态代理,动态生成的Class,如CGLib,javassist等字节码框架,android中的插件化自定义的ClassLoader加载的外部dex,都需要具备类的卸载功能来避免方法区不内存溢出

垃圾回收算法

标记-清除算法(Mark-Sweep)

  • 标记需要被回收的对象,然后再执行对象的回收操作(清除),但是回收效率地下,而且这种回收算法会留下大量不连续的内存碎片,如果内存中有大量的碎片,可能会导致当程序申请一块比较大的连续内存的时候由于内存中的连续内存不够会提前触发一次Gc来获取到足够的连续内存

复制算法

  • 该算法是将内存分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还活着的对象复制到另一块内存中,然后把已经使用过的内存空间进行一次清理,这种回收算法不会产生大量的内存碎片,而且每次只是针对一半的内存块进行操作,效率较高,但是不足的是内存的利用率较低,相当于只有一半的内存得到了有效的利用
  • 现在很多虚拟机采用的是:将内存分为一块较大的Eden区域和两块较小的Survivor区域,每次使用Eden区域和其中一个Survivor,在进行垃圾回收的时候把Eden区和在使用的Survivor区中活着的对象都复制到另一块Survivor当中,然后再清理Eden和Survivor区域,当另一块Survivor区也不够存放存活下来的对象的时候,这时候会把这些对象变成老年代

标记-整理算法

  • 标记整理算法和标记清除算法类似,都是先标记需要被回收的对象,然后标记整理算法会再对此时已经被标记好的内存进行整理,把存活的对象都向一端移动,然后清理调端边界以外的内存

分代收集算法

  • 新生代对象的有经常被回收只有少量存活,而老年代对象的存活率较高,所以对新生代对象可以采用复制算法来回收,对于老年代对象可以采用标记-清理活着标记-整理算法来回收

Stop The World

  • Java虚拟机在发生Gc的时候,由于是用Gc Roots的方式来判断对象是否要被回收,所以当发生Gc的时候需要暂停虚拟机的所有线程,等Gc结束后才继续执行,因此Gc的时候会造成程序卡顿,之所以要暂停所有线程的原因是Gc的发生是基于当前的Gc Roots引用链,如果线程还在继续执行,那么引用链很可能发生改变,这样Gc可能把还有引用链的对象给回收了,那就乱套了

对象的内存分配于回收

  • 通常对象在新生代会被分配在Eden区因为大部分对象都会发生频繁的常见并很快就会被回收,当Eden区的内存不够了,这时候会触发一次Monitor Gc,当Gc之后Eden区的内存还是不够用,那么此时Edne区的存活对象就会被移入老年代
  • 大对象直接进入老年代,因为老年代对象存活的几率大,不会经常发生Gc,所以针对大对象的创建和回收需要耗费较大的资源的这种就直接放入老年代来避免大对象频繁的创建与Gc
  • 当对象在Eden区域存在足够久的时候(长期存活的对象),这时候就把该对象放入老年代,因为该对象不经常被回收,长期存活,所以不应该放在经常发生Gc的Eden区,以此来减少Eden区的对象,从而加快Eden的Gc效率

你可能感兴趣的:(JVM的垃圾收集器策略)