什么是Java里的垃圾回收?如何触发垃圾回收?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:
- 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
- 手动请求:虽然垃圾回收是自动的,开发者可以通过调用
System.gc()
或Runtime.getRuntime().gc()
建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。 - JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:
-Xmx
(最大堆大小)、-Xms
(初始堆大小)等。 - 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。
#判断垃圾的方法有哪些?
在Java中,判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数法和可达性分析算法。
引用计数法(Reference Counting)
- 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
- 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。
可达性分析算法(Reachability Analysis)
Java虚拟机主要采用此算法来判断对象是否为垃圾。
- 原理:从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI(Java Native Interface)引用的对象、活跃线程的引用等。
#垃圾回收算法是什么,是为了解决了什么问题?
JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。
垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。
通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。
#垃圾回收算法有哪些?
- 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
- 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
- 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
- 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。
#垃圾回收器有哪些?
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代
#标记清除算法的缺点是什么?
主要缺点有两个:
- 一个是效率问题,标记和清除过程的效率都不高;
- 另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
#垃圾回收算法哪些阶段会stop the world?
标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中。标记-复制算法可以分为三个阶段:
- 标记阶段,即从GC Roots集合开始,标记活跃对象;
- 转移阶段,即把活跃对象复制到新的内存地址上;
- 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。
下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:
G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。
标记阶段停顿分析
- 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
- 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
- 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
清理阶段停顿分析
- 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
复制阶段停顿分析
- 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。
因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。
#minorGC、majorGC、fullGC的区别,什么场景触发full GC
在Java中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将GC分为三种类型:Minor GC(也称为Young GC)、Major GC(有时也称为Old GC)、以及Full GC。以下是这三种GC的区别和触发场景:
Minor GC (Young GC)
- 作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。
- 触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。
- 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
Major GC
- 作用范围:主要针对老年代进行回收,但不一定只回收老年代。
- 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。
- 特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
Full GC
作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
触发条件:
直接调用
System.gc()
或Runtime.getRuntime().gc()
方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。
特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。
#垃圾回收器 CMS 和 G1的区别?
区别一:使用的范围不一样:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
- G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二:STW的时间:
- CMS收集器以最小的停顿时间为目标的收集器。
- G1收集器可预测垃圾回收 (opens new window)的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-整理”算法,进行了空间整合,没有内存空间碎片。
区别四: 垃圾回收的过程不一样
注意这两个收集器第四阶段得不同
区别五: CMS会产生浮动垃圾
- CMS产生浮动垃圾过多时会退化为serial old,效率低,因为在上图的第四阶段,CMS清除垃圾时是并发清除的,这个时候,垃圾回收线程和用户线程同时工作会产生浮动垃圾,也就意味着CMS垃圾回收器必须预留一部分内存空间用于存放浮动垃圾
- 而G1没有浮动垃圾,G1的筛选回收是多个垃圾回收线程并行gc的,没有浮动垃圾的回收,在执行‘并发清理’步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理是才会被回收。如果在清理过程中预留给用户线程的内存不足就会出现‘Concurrent Mode Failure’,一旦出现此错误时便会切换到SerialOld收集方式。
#什么情况下使用CMS,什么情况使用G1?
CMS适用场景:
- 低延迟需求:适用于对停顿时间要求敏感的应用程序。
- 老生代收集:主要针对老年代的垃圾回收。
- 碎片化管理:容易出现内存碎片,可能需要定期进行Full GC来压缩内存空间。
G1适用场景:
- 大堆内存:适用于需要管理大内存堆的场景,能够有效处理数GB以上的堆内存。
- 对内存碎片敏感:G1通过紧凑整理来减少内存碎片,降低了碎片化对性能的影响。
- 比较平衡的性能:G1在提供较低停顿时间的同时,也保持了相对较高的吞吐量。
#G1回收器的特色是什么?
G1 的特点:
- G1最大的特点是引入分区的思路,弱化了分代的概念。
- 合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷
G1 相比较 CMS 的改进:
- 算法: G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。
- 停顿时间可控: G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
- 并行与并发:G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。
#GC只会对堆进行GC吗?
JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。
- 堆(Heap): 堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上,因为大多数对象都会被分配在堆上,而垃圾回收的重点通常也是回收堆中不再被引用的对象,以释放内存空间。
- 方法区(Method Area): 方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法区中的垃圾回收与堆有所不同,但是同样存在对不再需要的常量、无用的类信息等进行清理的过程。