Java 垃圾回收机制
- 一:哪些内存需要回收
- 二:怎么定义垃圾
- 1、引用计数算法
- 2、可达性分析算法
- 3、方法区的回收
- 三:引用类型
- 1、强引用
- 2、软引用
- 3、弱引用
- 4、虚引用
- 四:怎么回收垃圾
- 1、垃圾回收算法
- 标记 - 清除算法
- 标记 - 整理算法
- 标记 - 复制算法
- 2、分代收集理论
- 五:一次完整的GC流程
一:哪些内存需要回收
在Java内存运行时区域的各个部分中,堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们平时所说的内存分配与回收也仅仅特指这一部分内存。
二:怎么定义垃圾
1、引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
2、可达性分析算法
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何“引用链”相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中(Native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3、方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。主要是对常量池的回收和对类的卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
三:引用类型
1、强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
2、软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3、弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4、虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
四:怎么回收垃圾
1、垃圾回收算法
标记 - 清除算法
将存活的对象进行标记,然后清理掉未被标记的对象。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
标记 - 整理算法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记 - 复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
2、分代收集理论
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
新生代使用: 标记 - 复制算法
老年代使用: 标记 - 清除 或者 标记 - 整理 算法
依据分代假说理论,垃圾回收可以分为如下几类:
1、新生代收集(Minor GC/Young GC):目标为新生代的垃圾收集。
2、老年代收集(Major GC/Old GC):目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。
3、混合收集(Mixed GC):目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
4、整堆收集(Full GC):目标为整个堆和方法区的垃圾收集。
五:一次完整的GC流程
当Eden区的空间满了,java虚拟机会触发一次Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到Survior区。
大对象(需要大量连续内存空间的java对象,如那种很长的字符串)直接进入老年代。
如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),
则被晋升到老年态,则长期存活的对象进入老年代。
老年代满了,无法容纳更多的对象,Minor GC之后经常会进行Full GC,Full GC清理整个内存堆- 包括年轻代和老年代。
Major GC发生在老年代,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
Full GC会导致什么?
Full GC会 “Stop The World” ,即在GC期间全程暂停用户的应用程序。
JVM什么时候触发GC,如何减少FullGC的次数?
1、当新生代的Eden区满的时候触发 Minor GC。
2、serial GC 中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行 Full GC。而在 CMS 等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行 Full GC 回收。
可以采用以下措施来减少Full GC的次数:
(1)增加方法区的空间;
(2)增加老年代的空间;
(3)减少新生代的空间;
(4)禁止使用System.gc()方法;
(5)使用标记-整理算法,尽量保持较大的连续内存空间;
(6)排查代码中无用的大对象。