Java 进程的内存分配和垃圾收集由Jvm负责完成,前面有说过内存分配的原则,那垃圾收集是如何完成的呢。通过标记算法确定哪些对象可以回收,通过回收算法将可回收的对象回收掉。
标记算法
引用计数法(Java中没有采用这种算法)
- 原理:在对象中添加一个计数器,每一次被引用,计数器 +1 ,当引用失效后,技数器 -1 。当技数器为0时,对象即可被回收。
- 优点:实现简单,判断快,减少标记时间
- 缺点:无法准确的确定可回收的对象,例如循环引用(objA.instance = objB,objB.instance = objA),计数器永远不会归零,无法被回收,大量的循环引用出现,会导致最终出现OOM。
可达性分析法
- 原理:jvm通过一系列"GC ROOT"的对象为节点,开始搜索,走过的路径被称为引用链,当一个对象没有任何到达 “GC ROOT”的引用时,判定该对象为不可达对象
- GC ROOT 有以下几种
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区的静态属性引用的对象
- 方法区中常量应用的对象
- 本地方法栈JNI(native 方法)中的引用对象
垃圾收集算法
标记清除算法
- 原理:第一阶段,虚拟机重GC ROOT出发,向下搜索,然后将所有可达的对象进行标记。第二阶段,停止所有用户线程,垃圾收集器将未标记的对象回收掉
- 缺点:
- 效率低,需要遍历全堆(递归和全堆对象遍历);在清楚垃圾时,会停止所有用户线程,用户体验很差
- 会产生大量的内存碎片。对象在堆中不是均匀分配,在垃圾回收后,会产生很多不连续的内存空间即内存碎片。
- 标记清除算法是最基础的算法,其它算法都是在此基础上改进而来
复制算法 (用于新生代)
- 原理:将内存分成大小相等的两块区域,每次只使用其中一块区域,当这块区域用完了,将存活的对象复制到另一块区域中,然后将当前使用的这块区域全部清理掉。
- 优点:实现简单,运行高效。每次对其中一块进行清理,不会产出内存碎片
- 缺点:浪费较大的存在空间,每次只使用一半
标记整理算法(用于老年代)
- 原理:第一阶段,GC 从 GC ROOT 出发,向下搜索,将可达对象进行标记。第二阶段,将存活的对象移动到一侧,并按照内存地址依次排列,然后将末端内存地址以后的内存全部回收,这个过程为整理
- 优点:
- 避免了复制算法,只是用一般的内存空间,导致内存浪费
- 第二阶段,在回收垃圾前,对内存空间进行了整理,连续排列,避免产生内存碎片
- 缺点:效率比较低,第一阶段和标记清除算法一样,比较耗时。第二阶段在清除前,需要对内存空间进行整理,同样耗时。
分代收集算法
- 原理:分代收集算法并不是一个新的算法,是几种算法的混合使用。一个基本假设,绝大部分对象存活时间是短暂的。分代收集算法把堆分成两部分,新生代和老年代,一般新生待 : 老年代 = 1:2。
- 新生代:对象基本朝生夕死,例如方法里面的局部变量
- 老年代:存活时间较长,但最终也会死亡的对象(缓存对象、单例对象)
- 新生代使用复制算法进行回收,新生代中绝大部分对象存活时间较短,所以将新生代分为三块,一个较大的区域Eden占80%,两块较小的servivor区域,各占10%。Eden区域主要用来分配刚刚创建的对象。当发生垃圾收集时,先将Eden区和servivor from区的存活对象,移动到servivor to 区域,将Eden和servivor from 区全部清除,每次垃圾收集,重复此操作。
- 老年代使用标记清除算法或标记整理算法,老年代对象生命周期较长,一般很少发生垃圾收集,发生垃圾收集时,时间较长。
- 方法区在JDK8以前一般被称为永久代,JDK8以后,被元内存所取代,元内存使用物理机的本地内存。