JAVA垃圾回收算法及原理

本文详细介绍了JAVA垃圾回收的原理,包括引用计数法和可达性分析算法,以及四种引用类型:强引用、软引用、弱引用和虚引用。讨论了垃圾回收算法,如标记清除、标记整理、标记复制和堆区垃圾回收流程。还涵盖了不同的垃圾回收器,如串行、吞吐量优先、响应时间和G1垃圾回收器的工作方式及其特点。最后,文章提及了巨型对象的处理和TLAB策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何判断垃圾是否可以回收?

1、引用计数法(早期的Python)

原理:

只要该对象被其他的对象所引用,对象就计数加一,若被引用两次,就计数加二,当引用该对象的变量不再引用该对象的时候,计数减一。当计数为零的时候,代表没有变量对该对象进行引用,最后就会作为垃圾回收。

缺点:

当发生循环引用的时候,当A,B都不再使用的时候,计数仍为一,不会被垃圾回收。

public void buidDog(){
         Dog newDog = new Dog()
         Tail newTail = new Tail();
         newDog.tail = newTail;
         newTail.dog = newDog;
        }

在代码中,newTail中拿着对newDog的引用,newDog中拿着对newTail的引用。若使用引用计数法,则其对象基数都为一,而运行buidDog后,无法进行垃圾回收(引用计数不为零)

 2、可达性分析算法(java)

原理:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

注:GC根本身不是对象,而是对对象的引用。GC根引用的任何对象都将在下一个垃圾收集中自动生存

可以作为GC 根对象的对象

JAVA中有四种主要的根:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象当前正在运行的方法中的局部变量被视为GC根。这些变量引用的对象始终可以通过在声明的方法中立即访问,因此必须保留它们。这些根的生命周期取决于程序的构建方式。在调试生成中,只要方法在堆栈上,局部变量就一直存在。
  • 方法区中的类静态属性引用的对象:它们引用的对象可以由声明它们的类随时访问(如果它们是公共的,则可以由程序的其他部分访问)
  • 方法区中的常量引用的对象
  • 本地方法栈JNI(即一般说的Native方法)中的引用的对象:包括系统的核心进程,同步锁机制(正在加锁的对象)等等

 

 

java的四种引用

1、强引用

沿着根节点GC链可以直接找到。都是强引用。

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

 Object o = new Object(); // 强引用 

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

 o = null; // 帮助垃圾收集器回收此对象 

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法。

2、软引用

没有被直接的强引用所引用,在垃圾回收后,内存依然不足,则软引用会被当做垃圾全部回收。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

应用:软引用可用来实现内存敏感的高速缓存。比如说图片,视频缓存,浏览器的前进按钮等的

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示软引用, 配合引用队列
 */
public class Demo2_4 {
    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 = new SoftReference<>(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.out.println(reference.get());
        }

    }
}

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,可以实现对内存的监控与软引用的删除。

3、弱引用

也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

Cache cache = new Cache();
    ReferenceQueue<Cache> referenceQueue = new ReferenceQueue();
    WeakReference wr = new WeakReference(cache, referenceQueue);
    //cache = null;//显示调用的时候,必须设置为null,否则为强引用,不能回收
    System.gc();

/**
ReferenceQueue< Cache > rq=new ReferenceQueue();
WakeReference <Cache>wr=new WakeReference(new Cache (),rq);//隐式调用
**/

弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。

软引用与弱引用的区别:

只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

应用场景:

两者都可以实现缓存功能,但软引用实现的缓存通常用在服务端,而在移动设备中的内存更为紧缺,对垃圾回收更为敏感,因此android中的缓存通常是用弱引用来实现(比如LruCache)

4、虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。

//虚引用
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null,无法获取实例
pf.isEnQueued();//返回是否从内存中已经删除

//弱引用
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
System.gc();  //通知JVM的gc进行垃圾回收,但JVM不一定会立刻执行
wf.get();//此时会返回null

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,RefferenceHandler每隔一段时间进入到虚引用队列中寻找有没有虚引用,如果有,释放)

Finallize

finallize对象重写了finallize()方法并且对象本身无强引用,会被回收。当回收的时候,现将终结器引用放入引用队列,再有一个优先级很低的线程finallize每隔一段时间扫描引用队列,若遇到终结器引用,会调用finallize()方法,调用完成时候,等到下一次垃圾回收的时候,会被回收掉。工作效率很低,不推荐finallize释放资源。

垃圾回收算法

1、标记清除算法

先对无用的内存进行标记,再释放(记录无用的内存的开始地址与结束地址。以后的内容直接进行覆盖)

速度快(只记录每个内存开始结束地址放于链表中),但是标记清除之后会产生大量的碎片,可能会导致,当程序在以后的运行过程中需要分配较大的对象时无法找到足够的连续内存而不得不提前触发另一次收集动作。

2、标记整理算法(老年代使用)

先对无用内存标记 ,然后整理(将可用的对象向前移动,使内存紧凑)

标记整理算法没有内存碎片。但是整理牵扯到对象的移动,效率较低。而且在整理的过程中,还需改变引用地址。

3、标记复制算法(新生代使用)

首先划分了两个区from区,to区(始终为空)。首先做标记,丛from区存活的对象复制到to取中。再清空from,最后交换from与to区。

标记复制算法不会产生碎片,但会占用双倍的内存空间

4、堆区垃圾回收流程

在堆内存中,内存化为两块区域,为新生代[伊甸园{大部分},幸存区from,幸存区to],和老年代两块区域。长时间使用的对象放于老年代。用完就可以丢弃的对象放于新生代中。在新生代垃圾回收较为频繁,老年代频率较低。

变量(静态变量,常量除外)对象首先诞生在新生代伊甸园当中,当伊甸园的空间占满的时候,

会触发MinorGC,采用标记复制算法,将存活的对象复制到幸存区to中,并使存活的对象的寿命+1。伊甸园的内容被回收掉,并且交换幸存区from与幸存区to的位置。

当伊甸园又满了之后,(MinorGC)垃圾回收(除了伊甸园,还要回收from,判断对象是否有用)至幸存区to中。

当其寿命超过了一个阈值(默认为15),将相应的对象晋升到老年代中。当新生代和老年代中都满了,就会触发一次FullGC(老年代的内存不足的时候)会做一个整个的清理。若在Full GC之后仍内存不够,则报异常OutOfMemoryException

注:

  • 当新生代内存紧张,有可能生命值很小的时候,晋升到老年代中。
  • 老年代与新生代的算法不同,老年代不是经常回收,所以不是很注重时间。
  •  

垃圾回收器

串行垃圾回收器

串行垃圾回收器分为两部分:Serial(复制算法,新生代)SerialOld(标记整理算法,老年代)。二者的GC分别运行,分别叫作MinorGCàSerial与FullGCàSerialOld。当发生GC时,其余的线程都阻塞暂停(堵塞的时间称为STW)。当垃圾回收之后,其余线程运行。

串行垃圾回收是单线程。所以适合堆内存较小的个人电脑(cpu个数小)

吞吐量优先垃圾回收器

吞吐量优先垃圾回收器是并行的,多线程的。适合对内存较大的场景,需多核CPU支持。(单核的话,就和单线程差不多了)

吞吐量优先垃圾回收器注重在单位时间内,STW的时间最短。1.8默认使用。。

当垃圾回收的时候,GC会开启多个线程进行垃圾回收,线程个数与cpu核数相关,CPU的占用率会短时间100%。

响应时间垃圾回收器

响应时间垃圾回收器为并发(可以与用户线程并发执行)多线程。新生代为复制算法。老年代采用标记清除的算法,当发生并发失败的时候,会退化成为标记整理的单线程GC。适合对内存较大的场景,需多核CPU支持。

响应时间垃圾回收器注重垃圾回收的时候,单次的暂停时间(STW)尽可能的短。

在图中,初始标记和并发标记仍然需要Stop the World、初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用

若老年代发生内存不足,执行初始标记(STW)[列举根对象],用户线程阻塞。初始标记结束后。用户线程恢复运行,垃圾回收线程并发运行,并发标记。再重新标记(STW),[在并发标记的时候,会产生垃圾],接着垃圾清理,与其他线程并行。当在并发清理的时候,会产生新的垃圾,成为浮动垃圾,带下一次GC清理。所以得预留空间来保留浮动垃圾。当老年代的碎片过多时,会造成将来分配对象失败,造成并发失败。垃圾回收器不能正常执行。退化为单线程的标记整理,响应时间会突然变长。

响应时间垃圾回收器对吞吐量是有降低(垃圾回收占用了CPU内核)

Garbage First垃圾回收器

同时注重吞吐量和低延迟。适合超大的堆内存,会将堆划分为多个大小相等的Region(区域),每个区域都化为老年代,伊甸园,幸存区。

Garbage First垃圾回收器整体上是标记整理算法,两个区域之间是复制算法。

G1中也有分代的概念,不过使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

 

G1引入了Region(分区)的概念,每个region中有个关联的Remembered Set(RS),RS数据结构是Hash,里面的数据是卡表(Card table),一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RS也将这个数组下标记录下来。

RS里面的存的是region中存活对象引用本region对象的关联指针。RS作用是跟踪某个堆区内的对象引用。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。在将来遍历的时候,无需遍历老年代,只需关注脏卡的区域,在每次对象引用发生变更的时候,标记脏卡。

当region中数据发生变化时,首先会反映到card table中的一个或多个card上,RS通过扫描card table知道region中内存使用情况。在region使用过程中如果region被填满,分配内存的线程会重新选择一个一个新的region,空闲的region被组织到一个基于链表的数据结构里面,这样可以快速找到新的region。

Young GC 阶段:

  • 阶段1:根扫描,静态和本地对象被扫描
  • 阶段2:更新RS,处理dirty card队列更新RS
  • 阶段3:处理RS,检测从年轻代指向年老代的对象
  • 阶段4:对象拷贝,拷贝存活的对象到survivor/old区域
  • 阶段5:处理引用队列,软引用,弱引用,虚引用处理

Mix GC阶段:

  • 初始标记(initial mark,STW)在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
  • 根区域扫描(root region scan)G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
  • 并发标记(Concurrent Marking)G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
  • 最终标记(Remark,STW)该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
  • 清除垃圾(Cleanup,STW)这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

G1垃圾回收有以下优势:

  • 并行与并发:有些收集器需要停顿的过程G1仍然可以通过并发的方式让用户程序继续执行;
  • 分代收集:可以不使用其他收集器配合管理整个Java堆;
  • 空间整合:使用标记-整理算法,不产生内存碎片;
  • 可预测的停顿:G1除了降低停顿外,还能建立可预测的停顿时间模型;

重标记(G1,CMS):

当对象引用发生改变时,JVM会给对象加入写屏障,只要对象应用改变,写屏障的代码就会执行。

  • 在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。
  • 在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:1:在开始标记的时候生成一个快照图标记存活对象。2:在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)。3:可能存在游离的垃圾,将在下次被收集

回收巨型对象

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:

  • TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  • Eden区中分配
  • Humongous区分配

TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在Humongous区中进行分配空间。巨型对象若在年轻带的时候没有引用也可以删除

 

参考链接:

https://www.cnblogs.com/study-everyday/p/7018977.html

https://www.jianshu.com/p/8bfa2a874f0c

https://www.cnblogs.com/superping/p/4185121.html

https://blog.youkuaiyun.com/qq_34173549/article/details/83065652

https://blog.youkuaiyun.com/sunjin9418/article/details/79603651

https://cloud.tencent.com/developer/article/1476764

 https://blog.youkuaiyun.com/swl1993831/article/details/91507991

 

若有不足,请多多指教。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值