垃圾判断
垃圾介绍
垃圾:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾
垃圾判断的作用:
释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象
垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收
垃圾判断算法
在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:引用计数算法和可达性分析算法
引用计数法
引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用)
优点:
回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收
在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误
区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点:
每次对象被引用时,都需要去更新计数器,有一点时间开销
浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。
无法解决循环引用问题,会引发内存泄露(最大的缺点)
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();// a = 1
Test b = new Test();// b = 1
a.instance = b; // b = 2
b.instance = a; // a = 2
a = null; // a = 1
b = null; // b = 1
}
}
当p对象指向为null时,因为跟p的引用已经找不到他指向的三个对象了,理应被当作垃圾,当采用引用计数法,剩下三个对象的引用计数并不为零,并不会被当成垃圾,也就造成了内存泄漏
可达性分析
GC Roots
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
GC Roots 对象:
虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
本地方法栈中引用的对象
堆中类静态属性引用的对象
方法区中的常量引用的对象
字符串常量池(string Table)里的引用
同步锁 synchronized 持有的对象
GC Roots 是一组活跃的引用,不是对象,放在 GC Roots Set 集合
工作原理:
可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象
分析工作必须在一个保障一致性的快照中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因
可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
无用属性
无用类
方法区主要回收的是无用的类
判定一个类是否是无用的类,需要同时满足下面 3 个条件:
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
加载该类的
ClassLoader
已经被回收该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样不使用了就会必然被回收
废弃常量
在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话而且有必要的话(内存不够用),"abc" 就会被系统清理出常量池
静态变量
类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收
如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null
分代思想
分代介绍
Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代
新生代使用:复制算法
老年代使用:标记 - 清除 或者 标记 - 整理 算法
Minor GC 和 Full GC:
Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快
Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多
Eden 和 Survivor 大小比例默认为 8:1:1
分代分配
工作机制:
对象优先在 Eden 分配:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当 Eden 区要满了时候,触发 YoungGC
当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区和from,最后交换from和to的指针。
当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区
To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域指针进行互换,from存的上一次的幸存者,而to是这一次垃圾回收时的幸存者
From 区和 To 区 也可以叫做 S0 区和 S1 区
晋升到老年代:
长期存活的对象进入老年代:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中
-XX:MaxTenuringThreshold
:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15大对象直接进入老年代:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象
-XX:PretenureSizeThreshold
:大于此值的对象直接在老年代分配动态对象年龄判定:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
空间分配担保:
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC
回收算法
复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收
应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合
算法优点:
-
没有标记和清除过程,实现简单,运行速度快
-
复制过去以后保证空间的连续性,不会出现碎片问题
算法缺点:
-
主要不足是只使用了内存的一半
-
对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小
现在的商业虚拟机都采用这种收集算法回收新生代,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间
标记清除
标记清除算法,是将垃圾回收分为两个阶段,分别是标记和清除
标记:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,标记的是引用的对象,不是垃圾
清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到空闲列表的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块
分配阶段:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表
算法缺点:
标记和清除过程效率都不高
会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表
标记整理
标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法
标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题
优点:不会产生内存碎片
缺点:需要移动大量对象,处理效率比较低
三者对比
Mark-Sweep | Mark-Compact | Copying | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的 2 倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |