Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙, 墙外面的人想进去, 墙里面的人想出来。
哪些内存需要回收
Java运行时内存区域中,程序计数器, 虚拟机栈, 本地方法栈为线程私有, 大小在分配栈帧时基本已经确定,因此这几个区域不需要过多考虑回收问题。
堆和方法区不同于上述区域, 因为只有在运行时才能确定要创建哪些对象, 这部分内存的分配和回收都是动态的。 因此垃圾回收重点关注这部分内存。
什么时候回收
堆中存放着几乎所有对象的实例, 因此对堆内存进行垃圾回收主要关注堆中哪些对象还活着, 哪些已经死亡。
对象死亡即不能被任何途径进行使用,例如没有任何的引用指向该对象, 此时该对象可以被回收。
引用计数法
实现简单效率高算法。
实现: 给每个对象添加一个引用计数器, 每当有一个地方引用该对象时, 计数器的值加一, 当该引用失效时计数器的值减一,当计数器的值为0时表示没有引用指向该对象, 则该对象不可能被访问, 可以进行回收。
主流的Java虚拟机没有使用该算法,主要是因为该算法很难解决循环引用的问题。
例如一个A对象中存放着对象B的引用 A.object = B 并且B对象中存放着A的引用 B.object = A。 但目前却没有引用指向A或者B,所以A和B都访问不到, 此时就会产生循环引用的问题, 因为A对象和B对象都不能被访问到但其引用计数器的值却不为0,导致内存泄漏。
/**
* VM Args: -XX:+PrintGCDetails
* @author: shuo
* @date: 2019/08/05
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[4 * _1MB];
public static void testGC()
{
ReferenceCountingGC A = new ReferenceCountingGC();
ReferenceCountingGC B = new ReferenceCountingGC();
A.instance = B;
A.instance = A;
A = null;
B = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
[GC (System.gc()) [PSYoungGen: 10854K->728K(38400K)] 10854K->736K(125952K), 0.0016684 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 728K->0K(38400K)] [ParOldGen: 8K->652K(87552K)] 736K->652K(125952K), [Metaspace: 3441K->3441K(1056768K)], 0.0068400 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
10854k->728k GC没有因为循环引用而没有清理A和B对象, 因为目前的虚拟机并没有使用引用计数法.
可达性分析法
目前的Java使用的是可达性分析法来判断对象是否存活.
这个算法主要通过一系列的成为"GC Roots" 的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径成为引用链, 如果一个多小到GC Roots没有任何引用链时则证明这个对象不可达, 因此他们会被判定视为可回收的对象.
Object5 Object6和Object7虽然都有关联, 但他们都堆GC Roots 不可达, 因此他们都是可回收的对象.
GC Roots对象
- 虚拟机栈的栈帧中的本地变量表中引用的对象
- 方法区中静态属性引用的对象
- 方法去中常量引用的对象
- 本地方发展中Native方法引用的对象
四种引用
前面所说的引用仅指强引用, 在JDK1.2后Java对引用的概念进行了扩充, 将引用又分为强引用, 软引用, 弱引用, 虚引用四种.
-
强引用(Strong Reference)
强引用在代码中普遍存在, 例如Object o = new Object();这类的引用, 只要强引用还在, 对象就不会被回收. -
软引用(Soft Reference)
JDK1.2后提供了 SoftReference来实现, 用来描述一些有用但非必须的对象, 在系统将要发生内存溢出异常之前会将软引用指向的对象再进行回收一遍, 如果还是没有足够内存才会发生溢出异常. -
弱引用(Weak Reference)
JDK1.2后提供WeakReference来实现, 比软引用更若一点, 用来描述非必须的对象, 被该引用引用的对象只能生存到下一次垃圾回收发生之前. 无论内存是否够用都会回收该对象. -
虚引用(Phantom Reference)
JDK1.2后提供PhantomReference来实现虚引用, 最弱的一种引用, 通过该引用无法获得对象, 该引用仅有的作用是在对象被回收时收到一个系统通知.
finalize()
当对象不可达, 将要被垃圾收集器回收时, 还有一次挽救的机会.
当一个对象进行可达性分析后发现没有与GC Roots相连接的引用链, 那么它会被标记一次, 并调用其finalize()方法进行筛选, 如果对象的类没有重写finalize()方法或是finalize()方法之前被调用过, 虚拟机则会放弃执行finalize()方法.
执行finalize()方法, 这个对象会被放置在F-Queue队列中, 并在一个优先级较低的线程Finalizer线程中去执行finalize()方法. 如果对象在自己的finalize()方法中成功将自己重新与引用链连接起来, 那么在第二次标记时他会被移除即将回收的集合. 否则基本上该对象就要即将被回收了.
如何回收
标记 - 清除算法
最基础的收集算法, 算法分为"标记"和"清除"两个阶段, 首先标记出要回收的对象, 标记完成后统一回收被标记的对象.
主要的不足: 效率低, 标记和清除过程效率不高. 产生大量的内存碎片.
复制算法
将内存分为两块, 每次只使用其中一块, 当这一块用完了, 就将还存活的对象复制到另一块内存上面, 然后把已用过的内存一次性清理掉. 解决了碎片问题, 且运行高效.
目前商业虚拟机都采用这种算法来回收新生代. 新生代的对象98%是朝生夕死的, 因此需要将内存分为一块较大的Eden区和两块较小的Survivor区, 回收时将Eden和一块Survivor区中存活的对象一次性复制到另外一块Survivor区上, 然后再清理其空间. HotSpot虚拟机默认Eden和Survivor的大小比例是8:1.可以使用参数设置其比值 -XX:SurvivorRatio=n 值为8表示Eden : Survivor from : Survivor to = 8 : 1 : 1
当Survivor区空间不够时, 需要依赖老年代进行分配担保.
标记整理算法
复制算法在收集存活率高的内存区域时效率会变低, 一般采用标记整理算法代替复制算法.
首先标记出需要被回收的内存, 之后将存活的对象都向一端移动, 然后直接清理掉端边界以外的内存.
分代收集算法
根据对象的存活周期不同将内存划分为几块, 一般把Java堆分为新生代和年老代, 根据各个年代的特点采用适当的收集算法
新生代只有少量对象存活, 一般采用复制算法.
年老代存活率较高, 一般采用标记整理或者标记清除算法.
参考文献: 《深入理解Java虚拟机》