给对象设置一个引用计数属性,引用每新增1次计数加1,引用每释放1次计数减1。但是循环引用的对象没法回收。Java中没有采用这种方式。
// 循环引用
public class ReferenceCountingGc {
Object instance = null;
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 Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
JVM中的根对象分为这几类:
List list = new ArrayList<>();
这个list是个局部变量,存在栈中,实例化的那个对象放在堆中,这里的根对象指的是堆上的那个实例对象。也就是引用保存在Java栈中,而真正的引用的对象(ArrayList)保存在Java堆中。从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
软引用使用场景示例
先将虚拟机栈内存设置为20M:-Xmx20m
设置VM参数查看GC详情:-XX:+PrintGCDetails -verbose:gc
public class Main{
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<byte[]> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
}
如果执行上面这段程序,会触发栈内存溢出错误:
如果此时我们需要读取一堆图片到这个list集合中,而读取这些图片并不是核心业务,如果使用强引用就会导致内存溢出,像这种不是很重要的资源,在内存紧张的时候其实是可以释放掉的。下面改成软引用:
public class Main{
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
一次GC之后,发现内存不够,会再次触发GC回收软引用占用的内存,所以最后看到前4个变为了null。
最后结合引用队列,将值为null的软引用自身进行清理:
public class Main{
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i = 0; i < 5; i++) {
// 关联了引用队列,当软引用所引用的byte[]数组被回收时,
// 软引用自身会加入到queue中
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
// 从引用队列中取软引用自身,然后进行清理
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
list.remove(poll);
poll = queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
}
弱引用和软引用的区别在于:弱引用所引用的对象生命周期更短,无论内存是否足够,垃圾回收器都会回收弱引用所引用的对象。
static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
container.put(key,value);
}
public static void main(String[] args) {
//某个类中有这样一段代码
Object key = new Object();
Object value = new Object();
putToContainer(key,value);
//..........
/**
* 后来程序员发现这个key指向的对象没有用了,
* 为了节省内存打算把这个对象抛弃,然而下面这个方式真的能把对象回收掉吗?
* 由于container对象中包含了这个对象的引用,所以这个对象不能按照程序员的意向进行回收.
* 并且由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。
* 很可能会造成内存泄漏。
*/
key = null;
}
解决上述内存泄漏的办法之一是使用WeakHashMap
,WeakHashMap
是通过WeakReference和ReferenceQueue实现的,使用table保存键值对。WeakHashMap
的key是“弱键”,即WeakReference类型,ReferenceQueue是一个队列,保存被GC回收的“弱键”。
当某个“弱键”不再被其他对象引用并被GC回收时,这个“弱键”会被添加的ReferenceQueue中,当下次操作WeakHashMap
时,会根据ReferenceQueue中记录的“弱键”去删除table中被回收的键值对。
JVM的做法是协同上面三种做法,把整个堆内存分为新生代和老年代,新生代存放用完即可丢弃的对象,老年代存放需要长时间使用的对象。新生代又划分成了幸存区From和幸存区to。这样,就可以针对不同对象的生命周期特点,采用不同的回收机制。
分代回收步骤:
相关VM参数:
一个线程内的OutOfMemoryError不会导致整个进程的退出
串行
吞吐量优先
响应时间优先
举个例子,吞吐量优先垃圾回收器:单次STW时间是0.2s,单位时间内只进行了2次,总共使用了0.4s。 响应时间优先垃圾回收器:单位时间内触发了5次垃圾回收,让单次STW时间最短,是0.1s,单位时间内使用了0.5s。总体来看,吞吐量优先垃圾回收器在单位时间上优于响应时间优先垃圾回收器。
吞吐量可以理解为使用用户线程执行时间/(用于线程执行时间+垃圾收集器线程执行时间)。
开启串行垃圾回收器:-XX:+UseSerialGC = Serial + SerialOld
。Serial
工作在新生代,采用复制
算法。SerialOld
工作在老年代,采用标记+整理
算法。新生代和老年代的垃圾回收器是分别运行的。
下图是串行垃圾回收器的工作流程,新生代和老年代都是这个流程。只有一个垃圾回收器线程在运行,运行期间,其他线程都要阻塞。
复制
算法,老年代采用标记-整理
算法。- XX:+UseAdaptiveSizePolicy
参数打开之后,就不需要手动指定新生代的大小,Eden和Survivor区的比例,晋升老年代对象等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应的调节策略ConcMarkSweep
简称CMS,C
代表concurrent,并发,指某些时刻垃圾回收器线程和用户工作线程并发运行,都要去抢占CPU,这种方式减少了stop the world时间。UseConcMarkSweepGC
运行在老年代,采用标记+清除
算法;UseParNewGC
运行在新生代,采用复制
算法;SerialOld运行在老年代,采用标记+整理
算法。标记+整理
算法。一旦退化到SerialOld收集器,CMS的响应时间就会达到很高,这就是CMS最大的一个问题。由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。特点是并发收集、低停顿。
CMS详细内容可以参考:
https://www.jianshu.com/p/86e358afdf17
http://blog.sina.com.cn/s/blog_df25c55f0102wxh3.html
Carbage First,JDK9默认的垃圾收集器。G1兼顾了吞吐量和响应时间。并发执行。
适用场景:
标记+整理
算法,两个Region之间采用复制
算法使用-XX:+UseG1GC
开启G1
1. G1垃圾收集阶段
工作一段时间后,Eden内存紧张,就会触发新生代垃圾回收(短暂的STW)。具体步骤是:
注:下面的图少了一个巨型对象区域,G1中的巨型对象是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。
3. Young Collection + CM(并发标记)
这个阶段会对Eden(E)、Survival(S)、Old(O)进行全面垃圾回收
具体过程是:
老年代的垃圾收集和CMS类似,G1老年代占用达到阈值时,会进行并发收集,当并发收集速度赶不上垃圾产生速度时,就会退化为串行收集,需要更长时间的STW,导致响应时间变慢
老年代优先选择回收的机制就是Garbage First的名字由来
5. 新生代垃圾回收时的跨代引用问题
对新生代进行垃圾收集进行可达性分析的时候,新生代对象的根对象可能来自老年代,老年代的对象很多,如果去遍历整个老年代查找新生代的根对象,效率很低。
G1的做法是采用卡表技术,将老年代区域再进行细分成一个个的Card,每个Card大约是512k,如果老年代中的某个对象引用了新生代对象,那么对应的Card称为脏卡,这样就不用去遍历整个老年代对象,而是遍历脏卡区域对象即可,提高效率。
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块(每一个小块称为卡),标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记忆集数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
维护记忆集数据的正确性采用的方法是写入屏障,当对象的引用关系发生变更时,都要去更新脏卡,这是个异步操作,更新脏卡的任务是放在一个队列中,由后台线程去完成。
下图中粉色区域就是脏卡区域:
6. Remark(重新标记)
并发标记阶段之后要进行重新标记,修正并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,以及并发标记期间用户线程可能产生的新对象,重新标记就是对这些对象进行标记。
并发标记采用三色标记法:
此时C还是白色,但是引用它的A已经是黑色了,不会再次进行并发标记了。解决这个问题就用到写入屏障机制,当对象间的引用关系发生了变化(比如A -> B -> C 改成了 A -> C),写入屏障机制就会把C加入到一个队列中,并把C变成灰色,表示C还没处理完成。等到整个并发标记过程结束进入重新标记阶段后,重新标记检测这个队列,发现有对象被引用,就会处理这些对象,最终变成黑色。
案例分析:
(1)Full GC 和 Minor GC频繁
分析:新生代内存很快就满了,会导致频繁的minor gc,也会导致对象提前晋升到老年代,这样的话,老年代就会有很多生命周期短的对象,导致老年代空间很快被占满,然后触发full gc。
解决:用监测工具查看新生代空间大小、对象晋升情况等,适当增大新生代内存、幸存区大小、阈值。
(2)请求高峰期发生Full GC,单次暂停时间特别长(业务需要低延迟,选择的是CMS)
分析:CMS四个阶段中,最慢的是重新标记,因为重写标记要扫描新生代和老年代。在业务高峰期的时候,新生代对象产生的速度比较快,扫描的时间就会增长。
解决:通过查看GC日志,可以看到每个阶段耗费的时间,如果真是重新标记耗费时间长,可以通过下面这个参数设置在重新标记发生之前先对新生代做一次垃圾回收。
(3)老年代充裕的情况下,发生Full GC(CMS JDK7)
JDK7中采用永久代作为方法区实现,永久代内存不足也会触发Full GC。而JDK8中,采用元空间作为方法区实行,垃圾回收不由GC这边控制和管理,而且元空间一般比较充裕。可以增大元空间的初始值和最大值
JDK8中用的是Parallel Scavenge+Parallel Old GC;G1是JDK9中默认的GC,直到目前JDK14默认也是用的G1;最强大的ZGC目前还在预使用期。