1.GC简介:
JVM中的Garbage Collection,简称GC,它会不定时去堆内存中清理不可达对象。
2.GC分类:
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),
新生代GC(minor GC):只针对新生代区域的GC。
老年代GC(major GC or Full GC):针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC。
Minor GC触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor满不会引发GC。
Full GC触发机制:当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
Minor GC是对新生代进行的垃圾回收,它通常发生在Eden区满了时。在Minor GC中,虚拟机会回收年轻代的内存,清理掉不再被引用的对象,并将存活的对象移动到Survivor区或者老年代。
而Full GC是对整个堆内存(包括新生代和老年代)的垃圾回收。它会检查和清理整个堆内存中的无效对象,并进行内存整理,以便提供更大的连续可用空间。Full GC的触发条件比较复杂,可能包括老年代空间不足、永久代空间不足(在JDK8及以前的版本中)、元空间不足(在JDK8及以后的版本中),或者手动调用System.gc()方法等。
Full GC相比Minor GC通常需要更长的时间,因为它需要扫描和整理整个堆内存。执行Full GC时,应用程序一般会暂停,可能会导致一定的性能问题和延迟。
在实际开发中,我们需要根据应用的特点和性能需求来选择合适的垃圾回收策略。合理配置垃圾回收参数,如堆大小、新生代与老年代的比例、内存分配速率等,可以优化应用的性能和内存利用率。
3.GC的工作特点:
在GC工作中,通过某种算法来对JVM中的内存区域进行检测,对检测到的不可达对象,进行垃圾回收。
理论上GC过程中会频繁收集Young区,很少收集Old区,基本不动Perm区(元空间/方法区)。
4.标记不可达到对象:
引用计数法:
引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到循环指向的存在。
首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。
什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。
主流的java虚拟机中没有使用引用计数法的最主要的原因是它很难解决对象之间相互循环引用的问题。
public class MyObject { public Object ref; public String name; public static void main(String[] args) { MyObject myObject1 = new MyObject(); MyObject myObject2 = new MyObject(); myObject1.ref=myObject2; myObject2.ref=myObject1; myObject1=null; myObject2=null; } }
将myObject1和myObject2赋值为null后,虚拟机依然无法回收,因为他们还相互指向和依赖。
可达性分析(GC ROOTS算法):
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
简单理解,可以理解为堆外指向堆内的引用。
以下对象可以选取GC ROOTS节点:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
5.垃圾回收的三种方式:
当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分为三种。
清除:
第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。
清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。
压缩:
第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。
复制:
第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。
总结:
回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。