Java垃圾回收

主要有下面几个问题,怎样判断哪些对象可以被回收?怎样回收?方法区的常量和类怎样回收?,基本是深入理解Java虚拟机书中的内容。

怎样判断哪些是可回收的垃圾呢?

两种方法:

  1. 引用计数法:每次引用对象都会在该对象的引用计数器上加一,引用失效时就减一,计数为0时就可被回收,但是这种方法可能出现循环引用,就是ab两个对象相互引用

  2. 可达性分析:通过GC roots对象为起点向下搜索,和被引用的对象间存在引用链,如果一个对象到GCroots没有任何引用链,就可被回收。GCroots对象一般包括:

    1. 虚拟机栈中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中JNI引用的对象

但是也不是说只要有引用存在就一定不会被回收,这种说法是针对强引用而言的。如果是软引用、弱引用、虚引用的对象,即使该对象还被引用着,也可能会被回收。

而且在垃圾回收的最后阶段会给这些对象第二次机会,在分析阶段会把可被回收的对象放入一个队列中,分析完成后会再次查看队列中的每个对象是否复活了(重新被引用),如果复活了就不再对其回收。

怎样回收呢?

  1. 回收算法:
    1. 复制:将内存分为两块,每次只使用其中一块,当这块内存用完时就将还存活的对象复制到另一块内存上,再清理本块内存。很高效,没有内存碎片,但是缩小了内存使用空间。新生代回收器一般都采用这种算法,不过并不是平均分为两半,而是8:1分
    2. 标记-清除:先标记出所有要被回收的对象,再统一清除标记的所有对象。不高效且有内存碎片
    3. 标记-整理:先标记所有要被回收的对象,将所有存活的对象移到一端,再将端边界外的内存全部清理掉
    4. 分代收集:就是将内存分为新生代和老年代

基本上是分代然后使用不同的垃圾回收器进行回收,垃圾回收器会使用不同的回收算法,有7种垃圾回收器:

  • Serial:新,可与Serial Old和CMS一起用,单线程,复制算法
  • ParNew:新,同上,多线程,复制算法
  • Parallel Scavenge:新,能与Serial Old和Parallel Old搭配,多线程,复制算法。但是和ParNew的区别是它关注的主要是收集器的可控制的吞吐量,可以设置最大垃圾收集停顿时间或者设置吞吐量大小
  • Serial Old:老,单线程,标记-整理算法
  • Parallel Old:老,多线程,标记-整理算法,注重吞吐量
  • CMS:老,多线程,标记-清理算法,优点是暂停时间短,但会有碎片问题和浮动垃圾以及降低吞吐量。分四个阶段:
    1. 初始标记:会短暂暂停工作线程,找到GCroots初始对象
    2. 并发标记:从GCroots进行可达性分析
    3. 重新标记:会短暂暂停工作线程,为了修正上一步并发标记阶段工作线程因为继续运行而导致的标记的变动
    4. 并发清除:
  • G1:将堆内存分为很多大小相等的独立区域,有的区域属于新生代,有的属于老年代,G1会跟踪每个区域的回收价值(回收后获得的空间越大,耗时越小,价值就越大),通过一个优先队列先回收在允许的回收时间内价值最大的区域。优势是可预测的停顿时间,注重吞吐量和低延时,并且没有内存碎片。回收流程:(前三步和CMS的类似)
    1. 初始标记
    2. 并发标记
    3. 最终标记
    4. 筛选回收:对各个region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。在不同区域间通过复制算法进行回收

上面的是对堆中的垃圾进行回收,那么别的区域怎样回收呢?

方法区的常量和类

  1. 常量回收和堆中的对象回收类似,看有无某个常量的引用,没有的话就从常量池中清除。

  2. Class类的卸载,判断条件比较苛刻:

    1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    2. 加载该类的 ClassLoader 已经被回收。
    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为Java虚拟机自带的类加载器包括启动类加载器扩展类加载器和应用程序类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

    由用户自定义的类加载器加载的类是可以被卸载的