垃圾回收和GC算法

参考《周志明.深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)(华章原创精品)(Kindle位置1870).北京华章图文信息有限公司.Kindle版本.》

java运行时数据区域的垃圾回收

java运行时数据区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域和线程的生命周期一致,栈中的栈帧随着方法的进入和退出有序地执行进栈和出栈。每个栈帧中分配的内存基本是在类结构确定下来就已知了,所以这些区域的内存分配和回收是确定的,不用过多考虑垃圾回收问题,当方法或者线程结束的时候,内存就随着回收了。
java堆和方法区里对象和接口的分配和回收是动态,只有在运行期间才会知道有那些接口和对象,这两块的垃圾回收是需要重点关注的。

判断对象存活的方法

引用计数法

在对象中添加一个引用计数器,每有一个地方引用就加一,每有一个引用失效就减一,减到0时认为对象已死。
优点:判别效率高,原理简单
缺点:占用额外内存空间,需要考虑大量的例外情况,例如相互循环引用的问题。

可达性分析算法

当前主流的商用程序语言的内存管理子系统都是通过可达性分析算法判定对象是否存活的,通过一系列称为“GC roots”的跟对象作为其实节点集,从这些节点开始,根据应用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说从GC roots到这个对象不可达时,则证明这个对象时不可能再被使用的。

java的GC Roots

在Java技术体系里面,固定可作为GCRoots的对象包括以下几种:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
·在本地方法栈中JNI(即通常所说的Native方法)方法)引用的对象。
·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
·所有被同步锁(synchronized关键字)持有的对象。
·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GCRoots集合。

引用的分类

  • 强引用
    普通赋值就是强引用
  • 软引用
    用WeakReference类实现,被软引用关联的对象,当系统要发生OOM异常时,会将软引用关联的对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够资源才抛出OOM异常
  • 弱引用
    用WeakReference类实现,被弱引用关联的对象只能生存到下次垃圾回收发生为止,不管内存是否够用,只要发生垃圾回收就会收回只被弱引用关联的对象。
  • 虚引用
    用PhantomReference类实现,无法通过虚引用来获取一个对象实例,虚引用也不会对对象的生存实践构成影响。唯一作用是能在对象被收集器回收的时候收到一个系统通知。

java如果判定对象死亡

1)首先通过可达性分析判定对象已经不被引用链关联;
2)在对象可能执行的finalize方法(如果没有重写该方法或者已经被调用过将被视为“没有必要执行”)中,该对象没有逃脱(finalize中对象可能重新被引用链关联)。
以上两个阶段都符合后,对象将判定死亡并被回收。但是不建议大家使用finalize方法,这个方法十分不可靠。

方法区回收

方法区的回收较为苛刻,性价比低,但是在某些特定的场景(如大量使用反射、动态代理、CGlib等字节码框架)收集方法区是有必要的。
如何判定一个类是否属于“不再被使用的类”?
·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

垃圾回收算法

只讨论追踪式垃圾收集算法。

分代收集理论

1)弱分代假说(WeakGenerationalHypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(StrongGenerationalHypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说(IntergenerationalReferenceHypothesis):跨代引用相对于同代引用来说仅占极少数。

不用为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,RememberedSet),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

标记-清除算法

先标记(可能标记存活的对象,也可能标记死亡的对象),标记完成后在清除。
优点:标记清除速度快;
缺点:标记清除操作效率随着对象的增长而降低,内存碎片化导致大对象无法分配导致再次触发GC;

标记-复制算法

  • 半区复制
    将内存分为大小相等的两块,一块内存用完,将存活的少量对象移动到另一块内存上,保障了新的可用内存区域的连续性,然后集中清理原来那块内存。
    优点:速度快,内存空间连续
    缺点:可用内存减半
  • Appel式回收
    IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。
    AndrewAppel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。
    Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次MinorGC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(HandlePromotion)。

标记-整理算法

标记-复制算法的使用前提是对象存活率低且存在担保空间,但是这两点对于老年代都不适用,所以提出了标记-整理算法,先标记,再整理,最后清理。

  • 标记整理VS标记清除
    标记清除不用移动活对象,故内存回收步骤效率高,但是内存分散,需要借助例如“分区空间分配链表”等额外开销使用分散的内存块,内存分配效率低;
    标记整理需要移动活对象,内存回收效率低,但是可用内存连续,内存分配和访问时效率高。
    整体看来,标记整理算法的吞吐量最高,因为其内存访问环节开销最低,而内存访问是程序最频繁的操作,但是由于其移动活对象需要较长的时间,故有时出现较大的暂停(延迟)。相反的,标记清除的延迟低,响应速度快,但是整体吞吐量低。
    此语境中,吞吐量的实质是赋值器(Mutator,可以理解为使用垃圾收集的用户程序,本书为便于理解,多数地方用“用户程序”或“用户线程”代替)与收集器的效率总和。
  • 综合使用
    CMS收集器平时采用标记清除算法,当内存的碎片化程度较高影响对象分配时,采用标记整理算法整理一次内存空间。
原文地址:https://www.cnblogs.com/lllliuxiaoxia/p/15792409.html