JVM——GC理论
对象的创建及内存分配后,接下来就是对象的回收了——垃圾收集器GC
一、GC回收的内存区域
线程私有:程序计数器、虚拟机栈、本地方法栈,都是线程私用的,基本可以在编译期固定大小,在线程或方法执行结束后回收,具备了确定性。
线程共享:Java堆、方法区,由于是线程共享的,无法根据线程来判断内存何时回收,具备不确定性。
所以GC主要回收的区域是Java堆、方法区。
二、GC相关的算法
1、判断对象是否需要被回收
① 引用计数算法
- 在对象中添加一个引用计数器,
- 每当有一个地方引用它时,计数器值+1;
- 当引用失效时,计数器-1;
- 任何时刻计数器值为0的对象就是不可能再被使用的,需要被GC回收
引用计数器原理简单,判定效率也很高,在大多数情况下都是一个不错的算法,微软COM、FlashPlayer、Python都是使用引用计数算法进行内存管理的。
但主流的Java虚拟机并没有选择引用计数算法来管理内存。原因是引用计数算法还存在一些额外场景需要特殊处理,例如循环引用,会产生内存泄漏,需要额外逻辑处理。
/** * VM Args:-XX:+PrintGCDetails -Xms20m -Xmx20m */ public class ReferenceCountingGc { public Object instance = null; private static final int _1MB = 1024*1024; /** * 占用内存,以便GC日志中能看清是否有回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args) { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; //这里发生GC,objA和objB能被回收,所以JVM不是通过引用计数算法判断对象是否需要被回收 System.gc(); //[Full GC (System.gc()) [PSYoungGen: 512K->0K(6144K)] [ParOldGen: 2344K->698K(13824K)] 2856K->698K(19968K), [Metaspace: 3355K->3355K(1056768K)], 0.0057982 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] } }
② 可达性分析算法
当前主流JVM都是通过可达性分析算法来判定对象是否存活。基本思路:
- 通过一系列的被称为“GCRoots”的跟对象作为起始节点集;
- 从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”
- 如果某个对象与“GCRoots”间没有任何引用链相连,即不可达,证明对象不会被使用,需要被GC
可达性分析算法,避免了引用计数器算法的部分特殊场景,如上面提到的循环引用。
可作为“GCRoots”的对象:
- 虚拟机栈中引用的对象,
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 在本地方法栈中native方法引用的对象
- JVM内部的引用
- 所有被同步锁持有的对象
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
③引用
- 强引用:最传统的引用定义,“Object obj = new Object()”这种引用关系。在任何情况下,只要强引用关系存在,GC就永远不会回收掉被引用的对象。
- 软引用:描述一些有用的,但非必须的对象。只被软引用关联着的对象,在系统即将要发生OOM前,会将这些对象列入回收范围之内,进行二次回收。JDK1.2后提供SoftReference类来实现软引用
- 弱引用:描述非必须的对象,比软引用更弱一点,被弱引用引用的对象,只能生存到下一次GC发生为止。JDK1.2后提供WeakReference来实现弱引用
- 虚引用:“幽灵引用”“欢迎引用”,最弱的引用关系,这类引用关系,完全不会对其生成时间构成影响,也无法通过虚引用来取得一个对象实例。JDK1.2后提供PhantomReference类来实现虚引用。
④finalize方法
通过可达性引用分析,如果对象不存在“GCRoots”的引用链关联后,即对象不可达,需要被回收,但是对象不会直接被回收,还需要判断对象是否必要执行finalize()方法,
finalize方法可实现对象GC之前的自救,但是finalize方法仅能被执行一次。
2、GC对象回收算法
① 分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕死的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说:跨代引用相对于同代引用老说仅占极少数。所以新生代GC时,若存在老年代引用时,直接判定对象,若干年龄后进去老年代,存活避免新生代与老年代有GC引用链,导致新生代GC的时候,需要进行老年代GC
这两个分代假说奠定了多款常用的垃圾收集器的一致的设计原则。
- 垃圾收集器应该将Java堆划分出不同的区域。
- 将回收对象依据其年龄分配到不同的区域之中存储。“新生代”、“老年代”
将Java堆划分出不同区域之后,垃圾回收器回收其中某一个区域或者某些部分的区域,因而就有了“Minor GC”、“Major GC”、“Full GC”,
部分收集:Partial GC,指垃圾收集的目标不是整个Java堆,其中又分为
- 新生代收集:Minor GC/Young GC,垃圾收集的目标是新生代
- 老年代收集:Major GC/Old GC,垃圾收集的目标是老年代。目前只有CMS收集器才会单独收集老年代的行为。另“Major GC”有资料也指的是整堆GC
- 混合收集:Mixed GC,垃圾收集的目标是整个新生代+部分老年代。目前只有G1收集器会有中行为
整堆收集:Full GC,指垃圾收集的目标是整个Java堆,方法区。Full GC会导致“Stop the World” ,所以JVM的GC调优:主要减少Full GC的次数。
针对不同区域对象存亡年龄特征,因而就有了“标记-清除算法”、“标记-复制算法”、“标记-整理算法”
② 标记-清除算法
标记清除算法是最基础的垃圾手机算法,其他算法都是在此算法上改良出来的。
标记-清除算法过程:标记所有需要回收的对象(JVM是根据上面的可达性分析算法),标记完成后,统一回收掉所有被标记的对象。也可以是标记所有存活的对象,标记完成后,同一回收掉所有没有被标记的对象,并且会清除标记。
缺点:
- 执行效率不稳定。如果Java堆中包含大量对象,而且大部分是需要被回收的,就需要进行大量的标记和清除动作,导致标记清除随对象数量增长而效率降低。
- 会产生很多空间碎片。
③ 标记-复制算法
标记-复制算法也直接简称为复制算法,主要是解决标记清除算法的两个缺点。
标记-复制算法过程:将内存按容量划分为大小相等的两块,每次只是用其中一块;当这一块内存用完后,将还活着的对象复制到另一块上,然后将这块内存一次清理掉,然后循环执行。
缺点:
- 不存在空间碎片了,但内存中多数对象存活时,复制开销会很大。
- 由于可用内存缩小为原来的一半,会产生极大的空间浪费。
④ 标记-整理算法
标记整理算法:标记所有需要回收的对象(JVM是根据上面的可达性分析算法),标记完成后,让所有被标记的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。
标记清除算法与标记整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式。
缺点:
解决了空间碎片,但是标记对象的移动会导致用户应用程序的停顿,“Stop the World”,移动开销大。