文章目录
1.如何判断对象可以回收
Java将垃圾对象的回收交给了JVM自动处理不需要,程序员手动的去回收释放内存了,而判断一个对象是否能被回收就要看这个对象是否“已死”;下面就来介绍一些垃圾回收算法用于判断对象是否”已死“。
1.1引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
致命的缺陷(循环引用问题):
这样的两个对象虽然没有其他的对象来引用他们,但是他们各自的引用计数器都为1,所以采用引用计数法的垃圾回收算法会有很大的安全隐患,容易导致内存泄漏。
2.可达性分析算法
Java虚拟机扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,则表示该对象可以回收,否则就不能回收。
2.1哪些对象可以作为GC Root对象呢?
- 程序启动所需的核心类;(System.Class)
- 调用本地方法时引用的Java对象(Native Stack)
- 正在加锁的对象;(Busy Monitor)
- 活动线程所引用对象;
2.2五种引用
- 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。 - 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身。 - 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身 - 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。 - 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象。
图示如下:
代码示例:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*
* 当一些浏览网上图片或者某些不需要长时间保留的资源时,可以用软引用或者弱引用来创建资源对象
*
*/
public class Test1004 {
private final static int _5mb=5*1024*1024;
public static void main(String[] args) {
// List<byte[]> list = new ArrayList<>();
// for (int i = 0; i < 4; i++) {
// list.add(new byte[_5mb]);
// }
soft();
// weakYingYong();
}
//软引用
public static void soft(){
List<SoftReference<byte[]>> list=new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for(int i=0;i<4;i++){
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> sf=new SoftReference<>(new byte[_5mb],queue);
System.out.println(sf.get());
list.add(sf);
System.out.println(list.size());
}
System.gc(); //显示调用GC
Reference<? extends byte[]> poll = queue.poll(); //引用队列里面放的是可以被回收的软引用
while (poll!=null){
list.remove(poll);
poll = queue.poll();
}
System.out.println("=================");
for(SoftReference<byte[]> sf:list){
System.out.println(sf.get());
}
}
//弱引用
public static void weakYingYong(){
List<WeakReference<byte[]>> list=new ArrayList<>();
for(int i=0;i<4;i++){
//创建弱应用对象
WeakReference<byte[]> weak=new WeakReference<>(new byte[_5mb]);
System.out.println(weak.get());
list.add(weak);
System.out.println(list.size());
}
System.gc(); //显示调用GC
for(WeakReference<byte[]> we:list){
System.out.println(we.get());
}
}
}
运行结果:
3.标记清除算法(老年代回收算法)
标记清除算法是最早也是最基础的垃圾回收算法,分为"标记"和"清除"两个阶段,其"标记"过程就是判断对象是否是垃圾,"清除阶段"就是将垃圾的占用内存的起始地址放到"空闲地址列表"种即可。
其回收过程示意图:
特性:
- 缺点:回收后可能会造成大量的内存碎片(如果要分配连续大内存的对象,则可能造成无法分配的结果);
- 效率不高,如果Java堆中存在着大量的垃圾对象,就需要进行大量的标记和清除操作,从而效率会降低;
4.标记整理算法(老年代回收算法)
标记整理算法与标记清除算法是相似的,本质区别在于整理,将存活的对象移动到一个连续的内存中,这样就解决了”内存碎片“问题。
其回收过程示意图:
特性:
- 优点:没有内存碎片问题;
- 缺点:整理的时候,可能要花费更多的时间(效率不高),适用于存活率较高的老年代使用;
5.复制算法(新生代回收算法)
为了完善标记清除算法效率低的情况,复制算法就被提出来了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存 用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过 的内存空间一次清理掉。
其回收过程示意图:
特性:
- 优点:没有内存碎片问题; 对于新生代中的对象,大部分都是那种朝生夕死,复制算法效率很高;
- 缺点:内存利用率不高,50%;
6.分代收集算法(一种算法思想)
分代收集是一种理论,将Java堆分成了几个区域,然后根据实际应用场景来设计垃圾收集器。
6.1内存划分示意图:
6.2创建对象的内存分配策略
1:对象优先在Eden区分配内存;
2:新生代空间不足时,触发minor GC,伊甸区和from区 的存活对象使用复制算法到To区,存活的对象年龄就要加1,并且交换from和To区,幸存区的对象的年龄超过阈值会进入老年代;
3:大对象直接进入老年代;
4:空间分配担保机制:
- 当新生代GC后,存活的对象在To(10%)的空间能够放下,则对象就正常创建;
- 当新生代GC后,存活的对象在To(10%)的空间不能放下,就由老年代进行担保,将一部分幸存区的对象放到老年代中,若老年代的空间不足放不下,就会先尝试minnor gc,看能否放下,若不能放下就会触发Major gc;
5:动态年龄判断:
新生代GC时,存活的对象中,某个年龄的对象的总和大于幸存区空间/2,则该年龄的对象全部进入老年代;
6.3内存分配示意图
6.4相关 VM 参数
7.垃圾回收器
7.1Serial收集器(新生代收集器,串行GC)
Serial收集器是一个历史悠久的垃圾收集器,其主要收集对象为新生代垃圾对象,使用的是复制算法。
特性:
- 单线程
- 复制算法
- 会暂停用户线程(STW)
垃圾收集示意图:
7.2ParNew收集器(新生代收集器,并行GC)
ParNew收集器实质上是Serial收集器的多线程并行版本,是在特定的场景下对Serial收集器的一种改进。
特性:
- 多线程
- 复制算法
- 会暂停用户线程(STW)
垃圾收集示意图:
7.3Parallel Scavenge收集器(新生代收集器,并行GC)
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标 记-复制算法实现的收集器。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
特性:
多线程
复制算法
可控制的吞吐量:
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别
是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接
设置吞吐量大小的-XX:GCTimeRatio参数。
自适应的调节策略:
Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
7.4Serial Old收集器(老年代收集器,串行GC)
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集 器,使用标记-整理算法。它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS收集器发生失败时的后备预案。
特性:
- 单线程
- 标记整理算法
垃圾收集示意图:
7.5Parallel Old收集器(老年代收集器,并行GC)
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并 发收集,基于标记-整理算法实现。
“吞吐量优先”收集器终于有了比较,名副其实的搭配组合==》Parallel Scavenge加Parallel Old(-XX:+UseParallelGC ~ -XX:+UseParallelOldGC)
特性:
- 多线程
- 标记整理算法
垃圾收集示意图:
7.6CMS收集器(老年代收集器,并发GC)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿
时间为目标的收集器。
特性:
- 用户体验优先,则可能导致吞吐量下降,性能下降;
- 浮动垃圾问题:
CMS第四个阶段中,用户线程会产生新的对象,其中包含垃圾对象(浮动垃圾),所以要预留空间给"浮动垃圾",不能等老年代空间满了在进行回收; - 要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”:此时就会触发一次老年代GC(Serial Old收集器);此时用户线程的等待时间就会大大增加。
- 内存碎片问题: 由于使用的是标记清除算法,会产生大量的内存碎片,当内存不够时会提前触发一次Full GC(Serial Old收集器)
7.7G1收集器(全区域垃圾收集器)
G1收集器是垃圾收集器开创了收集器面向局部收集的设计思路和基于Region 的内存布局形式。:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、 Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段 时间、熬过多次收集的旧对象都能获取很好的收集效果。
G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记:和CMS类似,但是可以和minor gc并发执行;
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象;
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。(其实和CMS的重新标记是差不多的)
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
特性:
- G1回收器回收region时基本是不会STW的,从整体来看是基于标记整理算法,而局部两个region之间是使用复制算法进行垃圾回收的;
- 用户体验优先;