Loading

垃圾回收机制

一般说来,我们要解决三个问题:
1、回收哪些内存?
2、什么时候回收?
3、如何回收? 
 
首先我们来看Java的四种引用类型:
 
强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
 
软引用:SoftReference,软引用是介于强引用和弱引用之间的引用类型。当系统内存不足时,垃圾回收器会尝试回收只有软引用指向的对象。与弱引用不同的是,软引用通常会在内存不足时才被回收,因此适合用于实现内存敏感的缓存。
 
弱引用:WeakReference,当一个对象只有弱引用指向时,垃圾回收器在下一次垃圾回收时可能会回收该对象。弱引用通常用于实现缓存或者容器类,允许对象在没有强引用时被自动回收。
 
虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知。
 
 
不同的引用类型,在做 GC 时会区别对待,我们平时生成的 Java 对象,默认都是强引用,也就是说只要强引用还在,GC 就不会回收,那么如何判断强引用是否存在呢?
 
一个简单的思路就是:引用计数法,有对这个对象的引用就+1,不再引用就-1,但是这种方式看起来简单美好,但它却不能解决循环引用计数的问题。因此可达性分析算法登上历史舞台,用它来判断对象的引用是否存在。
 
可达性分析算法:
通过一系列称为 GCRoots 的对象作为起始点,从这些节点从上向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与 GCRoots连接时就说明此对象不可用,也就是对象不可达。
 
GC Roots 对象通常包括:
 
1、虚拟机栈中引用的对象(栈帧中的本地变量表)
 
2、方法区:类的静态属性对象
 
3、方法区:常量引用的对象
 
4、本地方法栈JNI(Native 方法)中的对象

 

 

 

 例子一:

 

 GCRoot1由于是静态变量,所以它属于GCRoot对象。在方法还没执行完之前,这五个变量都是属于GCRoot组的,此时GC无法回收。

但是如果方法执行完成后,本地变量表中的对象:object1,object2,object3,object4,都会从GCRoot组里面移除出来。

 

 

 

 

 变为:

  

由于引用链的存在,所以在GCRoot1没有被回收之前,下面四个局部变量(本应该被回收的)都不会被回收。

 

例子二:

 

 

 

在方法还没完成前,object5,object6,object7都属于本地变量表,所以都在GCRoot组里面。

方法完成后:

 

 

object5,object6,object7都会被移除GCRoot,这时候GC扫描的时候,由于引用链向上找不到GCRoot对象,所以object5,object6,object7都会被释放,o的话根据自己的生命周期决定是否释放。

 

 如何减少OOM的概率:

1、尽可能少的发生内存泄漏。

2、尽可能不在循环中申请内存。

3、尽可能不在调用次数多的函数中申请内存。

 

 

可达性分析算法整个流程如下所示:
第一次标记:对象在经过可达性分析后发现没有与 GC Roots 有引用链,则进行第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行 finalize()方法。没有覆盖 finalize()方法或者 finalize()方法已经被执行过都会被认为没有必要执行。 如果有必要执行:则该对象会被放在一个 F-Queue 队列,并稍后在由虚拟机建立的低优先级 Finalizer 线程中触发该对象的 finalize()方法,但不保证一定等待它执行结束,因为如果这个对象的 finalize()方法发生了死循环或者执行时间较长的情况,会阻塞 F-Queue 队列里的其他对象,影响 GC。
 
第二次标记:GC 对 F-Queue 队列里的对象进行第二次标记,如果在第二次标记时该对象又成功被引用,则会被移除即将回收的集合,否则会被回收。
总之,JVM 在做垃圾回收的时候,会检查堆中的所有对象否会被这些根集对象引用,不能够被引用的对象就会被圾收集器回收。
 
一般回收算法也有如下几种:
1).标记-清除(Mark-sweep)
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
 
2).标记-整理(Mark-Compact)
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)。
 
3).复制(Copying)
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
 
4).分代收集算法
不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块

 

posted @ 2023-12-25 19:36  妖久  阅读(22)  评论(0编辑  收藏  举报