浅谈Java垃圾回收机制——GC

本文详细介绍了Java垃圾回收机制的工作原理和技术细节,包括可达性分析算法、对象的生命周期管理、不同垃圾回收算法的特点及应用场景,以及HotSpot虚拟机的具体实现。

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

Java有个东西叫垃圾收集器,它让创建的对象不需要像c/cpp那样delete、free。GC需要完成的三件事情:
  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

垃圾回收机制关注的内存是动态的。Java堆和方法去不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间时,才能知道会创建哪些对象。


对象已经死了吗?
1、引用计数算法:给对象中添加一个引用计数器,没当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是不可能再被使用的。(很难解决对象间相互循环引用的问题)。
2、可达性分析算法:利用可达性分析来判定对象是否存活。
  • 算法基本思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的化来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的
  • 在Java语言中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的native方法)引用的对象。

3、生存还是死亡
  • 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的。
  • 杀死一个对象,至少要经过两次标记过程:如果在可达性分析后没有与GC相连接的因用力按,将会第一次标记并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,则这两种情况都被视为“没有必要执行”。
  • 如果这个对象被判定为有必要执行finalize()方法,会进入一个队列之中,之后虚拟机会建立一个低优先级的线程去执行它(只会触发它,不会等待它结束)。如果此时它与其他引用链上的对象建立关联(将this赋值给某个变量或对象的成员变量),则就成功拯救了自己,在第二次标记时,就将它移除出“即将回收”的集合。
  • 注意:任何一个对象的finalize()方法都只能被执行一次,第二次被gc的时候就不会被执行了
  • 不建议拯救对象,应该避免使用它。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序

4、回收方法区
  • 在方法区中进行垃圾回收效率较低,在堆中,尤其是在新生代中,常规应用一次垃圾收集一般可以回收70%~95%,而永久代的垃圾收集效率远低于此。
  • 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
    • 判定一个产量是否为“废弃常量”:是否被其他地方引用了这个字面量。
    • 判定一个类是否“无用的类”,无用的类只是可以回收,并不一定一定被回收:
      • 该类所有的实例都已经被回收,Java中不存在该类的任何实例。
      • 加载该类的ClassLoader已经被回收。
      • 该类对应的java.lang.Class对象没有在任何地方呗引用,无法在任何地方通过反射访问该类的方法。


垃圾收集算法
1、标记——清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  • 缺点:
    • 效率问题,标记和清楚两个过程的效率都不高
    • 空间问题,标记清楚之后会产生大量的不连续的内存碎片。如果有较大对象,无法找到足够的连续内存,而不得不提取触发另外一次垃圾收集动作。

2、复制算法:为了解决效率问题。新生代使用复制算法(将Eden和一块S)
  • 将可用的内存分为两块,每次只使用其中的一块。当一块内存用完后,将存活的对象复制到另一块上面,将使用过的一块空间一次清理掉内存分配不需要考虑内存碎片等复杂问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价较高,将内存缩小到原来的一半

  • 不需要按1:1的比例来划分内存空间。一块较大的Eden空间,两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间上。最后清理掉Eden和Survivor空间。默认大小8:1,每次可以使用90%空间。当回收对象多余10%,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保

3、标记——整理算法

  • 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。在老年代不能直接选用这种算法。
  • 根据老年代的特点,提出了一种“标记——整理”算法,标记过程任然和“标记——清除”,并且让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。

4、分代收集算法:根据对象存活周期的不同将内存划分为几块。
  • 一般把Java堆分成新生代和老年代(可以根据不同年代的特点采取最舍当的收集算法)。
  • 新生代,每次收集都会有大量对象死去,少量存活,采用复制法。老年代因为对象存活率较高,所以采用“标记——清除”或“标记——整理”算法

总结:
效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记/整理算法>标记/清除算法。
内存利用率:标记/整理算法=标记/清除算法>复制算法。

HotSpot的算法实现
1、枚举根节点
  • 可达性分析,从全局引用和执行上下文开始,逐个检查,会消耗大量的时间。可达性分析对执行时间的敏感还体现在GC停顿上,要确保整个执行系统在一个时间点上,不能不断变化。枚举根节点是必须要停顿的
  • 当前主流Java虚拟机使用准确式GC,当执行系统停顿下来后,虚拟机有办法直接得知哪些地方存放着对象引用,
2、安全点:只是在“特定的 位置”记录了这些信息,这些位置称为安全点
  • 程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点的时候才能暂停
  • 对于安全点,需要考虑,如何在GC发生时让所有线程都“跑”最近的安全点上停下来。两种方法:抢先式中断和主动式中断。
    • 抢先式:GC发生时,中断所有线程,发现某个线程不再安全点上,就恢复线程,让它跑到安全点
    • 主动式:需要中断时,不直接对线程操作,仅仅设置一个标志,各个线程主动轮询,发现中断标志为真时,就自己中断挂起。
3、安全区域
  • 对于sleep和blocked的线程,无法响应JVM中断请求,无法走到安全点中断挂起,使用安全区域进行解决。
  • 安全区域是在一段代码片段中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的。进入首先要表示自己进入,离开要检查是否完成根节点枚举或GC过程。

内存分配与回收策略
对象的内存分配,就是在堆上分配,对象的主要分配在新生代的Eden去上,如果启动了本地线程分配缓存,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。
1、对象优先在Eden分配
  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  • 新生代GC(Minor GC):指的是发生在新生代的垃圾回收动作,Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Full GC/Major GC):值得是发生在老年代的GC,出现了Full GC,经常会伴随有至少一次的Minor GC(不绝对,在Parallel Scavenge收集器的收集策略里面就有直接进行Full GC的策略选择过程)。Full GC的速度一般会比Minor GC慢10倍以上
2、大对象直接进入老年代
  • 大对象:需要大量连续内存空间的Java对象,比如很长的字符串以及数组。大对象对于虚拟机的内存分配来说是一个坏消息(应当避免短命大对象,写程序的时候应当避免)。
  • 虚拟机提供了一个参数,令大于这个设置值的对象直接在老年代中分配,目的是为了避免在Eden区一级两个Survivor区之间发生大量的内存复制。(新生采用内存复制算法)。
3、长期存活的对象将进入老年代
  • 为了能够识别哪些对象应该放在新生代,哪些对象应该放在老年代。虚拟机为每个对象定义了一个对象年龄(Age)计数器。对于在Survivor去中,没熬过一次MinorGC,计数器加1,当计数器的值达到一定程度,就会被晋升到老年代中。
4、动态对象年龄判定
  • 为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
5、空间分配担保
  • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果可以承担风险,则检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是由风险的;如果小于,或者不允许冒险,则需要进行一次Full GC。
  • JDK 6Update 24之后的规则变为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。


关于GC的题目:

1、 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
   对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
2、 垃圾回收的优点和原理。
   垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
3、 java中会存在内存泄漏吗,请简单描述。
   会,所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象编程了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉 。由于Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。
   java中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
   内存泄露的另外一种情况:当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
4、 GC是在什么时候,对什么东西,做了什么事情?
(1)、GC在新生代Eden区域连续空间不够时,进行Minor GC。在每次Minor GC前,查看老年代的连续内存是否足够,如果老年代剩余的连续空间大于新生代所有对象的总和则是安全的。如果不大于,查看HandlePromotionFailure是否允许承担风险,若可以则查看连续可分配空间是否大于历次晋升至老年代的对象大小的平均值,若果是,则进行Minor GC,否则需要进行一次Full GC。通过MaxTenuringThreshold控制进入老年前生存次数等。
(2)、经过可达性分析(GC Root)搜索,不再引用链中,并且经过第一次标记、清理后,仍然没有复活的对象。
(3)、 能说出诸如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值