纸上得来终觉浅 绝知此事要躬行
前言:本文参考自 周志明先生的《深入理解Java虚拟机》作学习记录作用,想详细学习java虚拟机的朋友建议买一本书仔细研读。
JVM的自动内存管理机制的内容就是 内存自动分配和内存自动回收两个部分。
本篇要整理的是内存自动回收这个部分。说到内存回收自然就想到了垃圾收集器了。
关于垃圾收集器,我们需要弄清楚三点。
- 哪些内存需要回收?
- 什么时候回收?
- 怎么回收?
哪些内存需要回收?
我们学过运行时数据区可以分成五个部分,程序计数器、方法区、堆、虚拟机栈、本法方法栈。其中程序计数器、虚拟机栈、本地方法栈都是随线程生而生依线程死而死,方法和线程结束内存自然回收了,这三个区域的回收具有确定性,可以不必过多考虑。主要考虑堆内存以及方法区内存。
可达性分析算法
垃圾回收中,什么对象是垃圾,什么对象又是有用的呢?确定什么内存是垃圾,是GC过程中做的第一件事情,这里采用了可达性分析算法。这个算法的主要思路就是通过一系列称为"GC Roots"的对象作为起始点,从这些起始点开始往下搜索,搜索走过的路径称为引用链,当一个对象到没有任何引用链到达GC Roots的时候,那么我们就称这个对象是"垃圾"。
JAVA中的GC Roots对象包括以下几种
- 虚拟机栈中的引用对象
- 方法区中类敬他器属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
但是并不是对象被标记上不可达的标记之后就立马被清楚,一个对象要被清除至少需要被标记两次,第一次是可达性分析之后算出不可达,第二次是判断这个对象是否有必要执行finalize()方法。当判定有必要finalize()之后,这个对象就会被放到F-Queue中去执行,在队列中等待的过程也是对象最后个逃逸的机会。
回收方法区
有一种说法是方法区是永久代,不存在垃圾回收,JVM规范中确实也不要求虚拟机在方法区实现垃圾回收。因为这个区域的回收效率比较低下,在方法区中的回收的对象主要是废弃常量和无用的类。
废弃常量:常量池中的某个常量,没有任何一个对象引用到这个常量
无用的类:同时满足下述三个条件可以判定为无用的类
- 该类的所有实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
在大量使用反射、动态代理、GCLib等框架以及频繁定义ClassLoader的场景需要实现方法区GC,保证永久代不会溢出。
总结:我们要回收的垃圾主要分布在两个区域
第一:堆区域中我们要回收的垃圾是对象,在可达性分析算法计算之后不可达的对象且判定有必要执行finalize()方法的对象
第二:方法区中的废弃常量以及无用的类
什么时候回收?
GC可以分成三种类型:Minor GC、Major GC、Full GC对应的不同的回收区域和策略。
-
Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快。
-
Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。
-
Full GC:指发生在老年代和新生代的GC,速度很慢,需要Stop The World。
首先我们要了解一下堆内存的分代划分。
堆内存分成了三块,分别是年轻代、年老代、永久代。其中年轻代又可以分成三块,一块是eden(伊甸园)以及两块幸存区S0、S1。
Minor GC
假定目前整个堆内存尚未存储任何数据,新的对象会被分配到新生代中的eden区中,当eden区放不下的时候,立即执行Minor GC将幸存下来的对象移存到S0,不再引用的对象被删除,对此幸存的对象年龄标记为1,eden区和S0如下图所示。
再次触发Minor GC的时候,执行的步骤一直,区别在于被引用的对象会存储到S1区,在S1存在并且在S0也存活的对象并在原有的计数器上继续累加年龄变成了2。
当再次触发Minor GC之后,步骤一直数据又移存到S0,如此反复。
Major GC
当幸存区S0/S1中的幸存对象年龄到5(我们假设这个阈值是5,这个值是可以设置的。)的时候这个对象就会被提升到年老区。
当老年代空间被对象堆满之后,将会执行一次Major GC,将会清除老年代不再引用的对象并对该空间执行压缩
永久代:在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
几个特例:
动态对象年龄判定
一般来说经过几轮Minor GC还存活的对象会进入老年代,除此之外还有另一条进入老年代的判断规则。如果幸存区空间中相同年龄所有对象大之和大于幸存区空间的一半,年龄大于或者等于该年龄的对象直接进入老年代。
空间分配担保
在执行一次Minor GC 的时候,虚拟机会先先检测老年代中可以使用的连续空间是否大于新生代的所有对象总空间,如果是,那么Minor GC可以确定是安全的。如果不是,那么检查HandlePromotionFailure设置值是否是允许担保失败,如果允许,那么虚拟机会继续查询历次晋升老年代的对象的平均大小与老年代剩余的连续空间进行比较,决定是否进行一次Full GC ,这种做法可能出现担保失败的情况。