顾名思义,每被引用一次就+1;
对于循环引用的情况,循环引用的对象就不会被回收,造成内存泄漏。
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象;扫描队中的对象,看能否沿着GC Root对象(根对象)为起点的引用链找到该对象,找不到表示可以回收。即先确定根对象,由根对象间接或直接引用的都不能回收。
哪些对象可以作为根对象呢
1.强引用
2.软引用(SoftReference)
3.弱引用(WeakReference)
4.虚引用(PhantomReference)
5.终结器引用(FinalReference)
软引用应用
public static void soft() {
//list-> SoftReference--> byte[]
List<SoftReference<byte[]>> list= new ArrayList<>();
for(int i=0;i<5;i++){
SoftReferencec<byte[]> ref=new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.outprintln(list.size())
}
System.out.println("活环结束:"+list.size());
for(SoftReference<byte[]> ref :list){
System.out.println(ref.get());
}
}
使用引用队列清除无用的软引用
private static final int_4MB=4* 1024* 1024;
public static void main(string[] args){
List<SoftReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();//引用队列
for(int i=0;i<5;i++){
//关联了引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去
SoftReference<byte[]> ref=newSoftReference<>(new byte[_4MB],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
//从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]>poll=queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("========");
for(SoftReference<byte[]> reference :list){
System.outprintln(reference.get());
}
}
弱引用应用
public static void soft() {
//list-> WeakReference--> byte[]
List<WeakReference<byte[]>> list= new ArrayList<>();
for(int i=0;i<5;i++){
WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
for(WeakReference<byte[]> w :list){
System.out.println(w.get());
}
}
}
标记出所有需要回收的对象,回收被标记对象
标记出所有需要回收的对象,回收被标记对象,将未被标记对象的内存 整理成 连续的内存空间,但这个过程会使得对象地址改变,因而
将内存划分为等大的两块,每次只使用其中的一块。当一快用完了触发垃圾回收:标记出所有需要回收的对象,将未被标记的对象复制到另一块内存空间,再一次性把现内存空间回收;下次触发垃圾回收时又将另一块存活的复制到这块,在一次性把另一块回收,循环往复。
将内存区域划分为新生代和老年代,针对不同区域采取不同算法:
注意:
若其他线程的运行不会影响主线程的数据情况,则其他线程的内存溢出,不会影响主线程的正常运行
总的来说垃圾回收器可分为四种:串行、并行、并发、G1
适用于单线程、堆内存较小
会暂停其他所有线程
Serial收集器:对新生代采取复制(Serial),对老年代采取标记整理(Serial Old)
对应的JVM参数:-XX:+UseSerialGC
ParNew收集器: 只针对新生代的回收,是Serial收集器新生代的并行多线程版本,同样对新生代采取复制。
对应的JVM参数:-XX:+UseParNewGC – ParNew + Serial Old的收集器组合
Parallel收集器:对新生代采取复制(Parallel Scavenge),对老年代采取标记整理(Parallel Old)。
Parallel Scavenge收集器:类似ParNew收集器也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。
吞吐量: 运行用户代码时间 占比 (运行用户代码时间 + 垃圾回收时间)
如程序运行10分钟, 垃圾回收5秒,吞吐量约为99%
Parallel Old收集器:是SerialOld收集器老年代代的并行多线程版本
对应JVM参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC – Parallel Scanvenge + Parallel Old的收集器组合
ParNew收集器 与 Parallel Scavenge收集器 区别 – Parallel Scavenge收集器的自适应策略
CMS收集器 :Concurrent Mark Sweep: 并发标记清除 ,是一种以获取最短回收停顿时间为目标的收集器。
对应的JVM参数: -XX:+UseConcMarkSweepGC (自动将-XX:+UseParNewGC打开)–
ParNew + CMS + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
注意:
G1适用于全堆,既可以在新生代使用和老年代使用
同时注重吞吐量和低延迟,默认的暂停是200ms
超大堆内存,会将堆划分为多个大小相等的region,可通过-XX:G1HeapRegionSize参数设置区域大小,必须是2的幂次方
整体上是标记+整理算法,两个region区域之间是复制算法
G1收集器: 一个有整理内存过程的垃圾收集器,不会产生很多内存碎片;STW更可控,在停顿时间上加了预测机制,用户可以指定期望停顿时间。
可分为以下四个步骤:
初始标记:标记根对象直接引用的对象,会STW即停止其他线程;
并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。
每个 Region 记录着两个 top-at-mark-start (TAMS) 指针,分别为 prevTAMS
和 nextTAMS
,在 TAMS 以上的内存空间对应的对象就是新分配的
并发标记:找出根对象简接引用的存活对象,其他线程并发执行
最终标记:修正并发标记期间因其他线程继续运行而导致标记产生变动的那一部分对象的标记记录,会STW
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,会STW
垃圾回收阶段
新生代回收:进行GC Root的初始标记,会STW;
新生代回收+并发标记:老年代占用堆空间比例达到阈值时,进行并发标记(不会STW);
混合回收:进行全面的回收,最终标记和拷贝存活,会STW。(在最大的停止时间内,为了垃圾回收后的释放的堆空间较多,会选择内存较大的空间
跨代引用
老年代引用新生代对象
垃圾回收时需要根据根对象进行可达性分析,而老年代可能也存有根对象,跨代引用可以不需要完全遍历老年代,而是通过遍历脏卡来得到根对象。
类卸载
所有对象都经过并发标记后,就能知道那些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
回收巨型对象
一个对象大于region的一半时,称之为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代时处理掉
调优领域
确定目标
低延迟还是高吞吐量,选择合适的回收器
因为如果频繁的发生GC那就说明STW的时长越长,那么对程序的运行也是不利的。
查看FullGC前后的内存占用,考虑以下问题
数据是不是太多
如数据库操作结果集,多加载了些不必要的数据,可通过limit限制
数据表示是否太臃肿
是否存在内存泄漏
static Map map 这种长度存在的对象
软引用/弱引用
第三方缓存实现 如中间件 redis
新生代内存越大越好嘛:
理想情况:
新生代能容纳所有【并发量*(请求-响应)】的数据
幸存区大到保留【当前活跃对象+需要晋升对象】
如果幸存区太小,会自动调整晋升阈值,就可能会提前将对象晋升到老年代,造成的影响:原先本来寿命很短的对象得等到Full GC时才能将它回收
晋升阈值配置得当,让长时间存活对象尽快晋升
Minor GC主要是标记和复制,主要消耗时间在复制上;如果寿命很长的对象不能尽快晋升,那就说明在幸存区会被复制来复制去,那么对性能而言反而是个负担。
以CMS为例
CMS垃圾收集器有两点缺陷,其一是无法处理浮动垃圾,即在并发清理的过程中,用户线程又产生了新的垃圾,这些垃圾无法标记只能等到下次FullGC进行清理。所以老年代需要预留一定的空间装下浮动垃圾。在CMS过程中就可能导致并发失败,即CMS运行时内存空间无法满足,这时虚拟机才将Serial Old拿出来并且STW,进行串行的垃圾清理。
CMS的老年代内存越大越好,避免浮动垃圾引起的并发失败
先尝试不做调优,如果没有Full GC那么说明老年代没有内存空间不足,即使有发生Full GC,先尝试调优新生代
观察发生Full GC时,老年代内存占用,将老年代内存预设调大1/4 ~1/3,减少Full GC的发生
-XX:CMSInitiatingOccupancyFraction=percent 调整启动CMS时老年代已用内存占比值
注明:图片来源字母站JVM系列视频截图