垃圾收集器,俗称GC(Garbage Collection),是对内存中垃圾的自动回收,它主要完成三件事情:
- 怎么哪些内存应该是垃圾,需要回收?
- 什么时候回收?
- 怎么回收?
程序计数器、虚拟机栈、本地方法栈这三个地方是随线程的创建而创建,线程销毁时也跟着销毁,他们在类结构确定下来时分配多少内存就已知了,所以这几个区域的内存分配和回收都具备了确定性。不是垃圾收集的重点区域。垃圾收集重点作用的地方应该是java堆和方法区。
哪些内存是垃圾?
在垃圾收集器中,判断对象是否为垃圾通常有以下的两种策略算法:
引用计数法
引用计数器原理很简单:
- 给对象添加引用计数器
- 当有人引用他时,计数器值+1,引用失效时,计数器值-1
注意:这个策略在java中并不流行。
因为引用计数有很多额外情况,需要配合很多额外的处理才能保证正确工作,最简单的例子就是对象之间的相互循环引用, A->B,而B->A,相互引用,都是垃圾。。都删不掉
可达性分析算法
这个就是所谓的GC Roots,其思想就是:
- 以若干
GC Roots为起始节点集 - 从
GC Roots集中根据引用关系向下搜索,搜索路径也成为"引用链" - 如果对象到
GC Roots没有和任何引用链相连,则对象不可达,可以被删除了(不是一定删除,后面有解释)
不难发现,GC Roots是标记可达对象,而没有被标记的就会被删除。
在java中,可以作为GC Roots的对象包括但不限于:
- 虚拟机栈中引用的对象,如线程中被调用方法堆栈中使用的参数、局部变量、临时变量
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,如String
- 在本地方法中的JNI引用对象
- 虚拟机内部引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointException)
- 被同步锁持有的对象
关于引用
java中对引用概念分为强引用、软引用、弱引用、虚引用
- 强引用:
程序代码之中普遍存在的引用赋值, 即类似Object obj=new Object()。
只要强引用关系还存在,垃圾会收集永远不会回收这个对象
- 软引用
软引用是一些还有用,但非必须的对象。就是可以有,但没必要。
发生OOM之前,若存在软引用,则会把软引用作为第二次回收,如果回收后还是没有足够内存,才会抛出OOM。
- 弱引用
弱引用是一些可有可无的对象。
在下一次垃圾收集发生时,他们都会被回收。
- 虚引用
最弱的引用,不管有没有虚引用,都不会对对象的生存造成影响。
设置虚引用的唯一目的就是在对象被回收时能收到一个系统通知。
方法区的回收
方法区的垃圾收集主要回收两部分内容: 废弃的常量和不再使用的类型。
回收废弃常量与回收Java堆中的对象非常类似。
假如一个字符串“java”曾经进入常量池中, 但是当前系统又没有任何一个字符串对象的值是“java”,换句话说, 已经没有任何字符串对象引用常量池中的“java”常量, 且虚拟机中也没有其他地方引用这个字面量。 如果在这时发生内存回收, 而且垃圾收集器判断确有必要的话, 这个“java”常量就将会被系统清理 出常量池。
--------书中原话
不再使用的类型 回收的条件很苛刻。
- 该类所有的实例都已经被回收, 也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景如OSGi、 JSP的重加载等, 否则通常是很难达成的
- 该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法。
垃圾收集算法
分代收集理论
看过jvm的都知道,有新生代和老年代。这就是分代收集理论的一种。
分代收集理论建立在两个分代假说上:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次回收过程的对象越有价值,难以消亡
----原话---
两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:
收集器应该将Java堆划分出不同的区域, 然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区域之中存储。 显而易见, 如果一个区域中大多数对象都是朝生夕灭, 难以熬过垃圾收集过程的话, 那么把它们集中放在一起, 每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象, 就能以较低代价回收到大量的空间; 如果剩下的都是难以消亡的对象, 那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域, 这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用 。
简单来说就是,通过标记存活对象来做删除,而不是标记需要删除对象。
基于这两个假说,并划分不同区域后,就有了Minor GC、Major GC、Full GC这样的回收类型。他们对不同的区域根据特征进行垃圾收集。也就有了标记-复制算法、标记-清除算法、标记-整理算法等。
- 跨代引用假说: 跨代引用相对于同代引用来说仅占极少数。
这个假说的引入是原因之一,是在进行新生代收集(MinorGC)时,完全可能存在新生代被老年代引用,那存活与否就不能只根据新生代区域的GCRoots做可达性分析。
但是为了少量的跨代引用去扫描整个老年代太过浪费。而通过在新生代建立一个全局的数据结构(记忆集),把老年代划分成若干小块, 标识出老年代的哪一块内存会 存在跨代引用。 此后当发生
Minor GC时, 只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。 虽然这种方法需要在对象改变引用关系( 如将自己或者某个属性赋值) 时维护记录数据的正确性, 会增加一些运行时的开销, 但比起收集时扫描整个老年代来说仍然是划算的。
名词解释
-
部分收集(
Partial GC):指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:- 新生代收集(
Minor GC/Young GC) : 指目标只是新生代的垃圾收集。 - 老年代收集( Major GC/Old GC) : 指目标只是老年代的垃圾收集。 目前只有CMS收集器会有单独收集老年代的行为。
- 混合收集( Mixed GC) : 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。
- 新生代收集(
-
整堆收集(
Full GC):收集整个Java堆和方法区的垃圾收集。
标记-清除算法
这个算法分为:标记和清除 两个阶段。标记过程就是对对象是否为垃圾的判定过程
它的主要缺点:
- 执行效率不稳定:当堆中存在大量对象需要标记清除时,效率会很低,对象数量越多效率越差
- 内存空间碎片多:清除后留下的内存通常是不连续的,下次分配可能不存在足够大小的连续内存,导致另一次GC发生
标记-复制算法
思想:将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。
这个算法的缺点很明显:
- 浪费空间:内存空间缩小为原来的一半
由于对象大部分都是朝生夕死的,存活对象都是小小的部分,所以针对空间的浪费,现在的java虚拟机都做出了优化。
Appel式回收就是一种优化的方式:
- 把新生代分为较大的
Eden区和两块小的Survivor区 - 每次GC使用
Eden和其中一块Survivor(也叫From Survivor),将其中存活的对象直接复制到另一块Survivor(To Survivor)中。
Eden、From Survivor、To Survivor的大小比例默认是8:1:1所以新生代中默认情况下,可用空间是90%(
Eden加上一块Survivor)
标记-整理算法
这个算法中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存。
缺点就更明显了:
- 移动是一种极其重的操作,操作过程中还要暂停用户应用程序(STW现象)
STW(Stop The World)是指一个操作完成前,整个程序都不能提供使用
其实如果学过了操作系统,会发现前面这些和操作系统中的内存管理在思想上是差不多的。
经典垃圾收集器
垃圾收集器太多了。。。反正也记不住全部,就看最常用的CMS和G1吧。
CMS 收集器
CMS(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。 对于要求服务器响应速度的应用上,这种垃圾收集器非常适合。启动 JVM 的参数加上XX:+UseConcMarkSweepGC来指定使用 CMS 垃圾回收器。
CMS是基于标记-清除算法实现的,整个过程分为四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
注意:初始标记和重新标记仍然需要STW。
初始标记只是标记GC能关联的对象,速度很快。
并发标记可以和垃圾收集线程一起并发允许,不用STW。
重新标记是为了修正并发标记过程中,应用继续运行导致标记发生改变的那一部分对象的标记。
并发清除则清除前面被判断死亡的对象,没有对象的移动、复制操作,可以和用户线程同时并发运行。
G1 收集器
G1(Garbage First),是一款面向服务端应用的垃圾收集器。它被设计出来取代Parallel Scavenge+Parallel Old组合,作为服务端下的默认收集器。如今CMS已经沦落到不推荐使用的收集器的情况了。
从JDK9开始继续用XX:+UseConcMarkSweepGC开启CMS收集器的话,将会收到一个警告信息。
TODO:
针对一些面试题:
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是标记-复制算法
老年代回收器一般采用的是标记-整理算法进行垃圾回收。
实战:内存分配与回收策略
MinorGC测试
/**
* 年轻代GC测试
* VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public class MinorGCTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
}
大对象直接进入老年代
/**
* VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public class LargeObjTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
short[] allocation;
allocation = new short[4 * _1MB]; //直接分配在老年代中
}
}
长期存活对象进入老年代
/**
* VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public class LongTimeObjTest {
private static final int _1MB = 1024 * 1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
}