深入理解 Java垃圾收集器(GC)

时间:2022-11-09 00:04:14
 
 

小常识:

提起HotSpot VM,相信所有java程序员都知道,它是SUN JDK和open JDK中所带的虚拟机,也是目前使用范围最广的java虚拟机。其余比较出名的还有JRockit和J9


储备知识点:

线程在进行时往往会出现多次停顿来进行gc,线程停顿的这段时间被称为回收周期,而停顿开始的那个时间点叫做安全点,但是每个线程的安全点分布是不同的,所以需要考虑的问题是如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来?jvm用的是主动式中断的思想,当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

深入理解 Java垃圾收集器(GC)
但是上面说的全都是正常进行的线程,万一有线程正处于睡眠状态,睡眠状态也算暂停,但是随时可能苏醒,万一在gc的时候苏醒容易出现意外情况,但如果我们把这个线程强制唤醒,再让它运行到下一个安全点又有点不太现实,针对这种情况,就出现了安全区域这个机制。

安全区域是指在我们写的代码中,有某些代码片段的引用关系不会发生变化,那么在该片段的任何地方发生gc都是安全的,我们就称这个片段为安全区域,当线程运行到这个片段会标识出自己在安全区域。

那么我们再来看刚才那个问题,如果线程一直处于睡眠状态,那jvm就会忽略这个线程,反正这个线程也不会影响gc;如果判断这个线程会在gc中苏醒,那就分两种情况:①如果线程处于安全区域,jvm同样会忽略它,因为线程苏醒后会自动检测jvm是否在进行gc,从而决定是否要执行到安全区域外②如果线程没有处于安全区域,那么jvm会等它苏醒并执行到下一个安全点之后再开始gc。       
  蓝字内容都是本人猜想,不一定对,以后会慢慢修改




注意:回收和收集一个意思!!

一、JVM为什么要进行垃圾回收?

如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。

二、垃圾收集的定义

(1) GC是垃圾收集的意思(Gabage Collection),Java提供的GC功能可以自动也只能自动地回收堆内存中不再使用的对象,释放资源(目的)Java语言没有提供释放已分配内存的显式操作方法(gc方法只是通知,不是立即执行)。
 
(2) 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
 
(3) 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,当一个对象不再被引用的时候,按照特定的垃圾收集算法来实现资源自动回收的功能。

    (4)   那什么时候进行垃圾回收呢?

(1)当jvm空闲时,即没有线程在运行时,GC会被调用。
(2)Java堆内存不足时,GC会被调用。(3)当然,也由垃圾收集器的算法所决定。

三、我们可以主动垃圾回收吗?

(1) 每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。
 
(2) 唯一的区别就是System.gc()写起来比Runtime.getRuntime().gc()简单点. 其实基本没什么机会用得到这个命令, 因为这个命令只是建议JVM安排GC运行, 还有可能完全被拒绝。 GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化。


四、哪些垃圾需要回收?

哪些垃圾需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何找到这些对象?

     可达性分析算法(JVM就用这个)

这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

(4). 本地方法栈中JNI(Native方法)引用的对象。

这些比较难理解,我也不懂,以后再详细补充,可以忽略黄色部分

下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。

深入理解 Java垃圾收集器(GC)

由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收(不要问为什么,这是可达性分析算法所决定的)。

注意:不光是堆中,方法区也需要垃圾回收:1. 废弃常量。2. 无用的类。

五、垃圾回收的步骤有哪些呢?

对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡(彻底释放占用的内存),至少需要经历两次标记阶段。(对应问题:如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象在下个回收周期将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的,低优先级的Finalizer线程去执行。

2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。但是,一个对象只能拯救自己一次,第二次就被回收了。

简单点来说,一旦垃圾收集器准备好释放对象占用的存储空间(进入第一个回收周期),首先会去调用finalize()方法进行一些必要的清理工作,只有到下一次再进行垃圾回收动作(下一个回收周期)的时候,才会真正释放这个对象所占用的内存空间。

补充:

finalize()方法也叫收尾方法。
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法①进行一些必要的清理工作(对垃圾回收器不能处理的特殊情况进行处理)(例子在下边)②也有可能使该对象重新被引用,我习惯叫这种作用为复活注意!!每个对象的finalize()方法只能被执行一次,第二次就会直接跳过finalize()方法,这就是为了防止出现对象无限复活,内存空间只增不减。

一般忽略第二种情况,概念就变成了:一旦垃圾收集器准备好释放对象占用的存储空间(进入第一个回收周期),首先会去调用finalize()方法进行一些必要的清理工作,只有到下一次再进行垃圾回收动作(下一个回收周期)的时候,才会真正释放这个对象所占用的内存空间。

例子:1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。

System.runFinalization()和System.gc()是做什么的呢? 我个人的理解,这两个函数分别是应用层向JVM发出一个信号,告诉JVM,希望你能尽快的回收内存和调用对象的finaliztion方法,但是只是一个请求,而JVM只保证会尽最大的努力执行,但是具体什么时候执行以及会不会执行都是未知的。

六、垃圾收集算法

1、标记-清除(Mark-Sweep)算法

     这是最基础的算法,标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。标记-清除算法执行过程如图:

深入理解 Java垃圾收集器(GC)

2、复制(Copying)算法

      复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。复制算法的执行过程如图:

深入理解 Java垃圾收集器(GC)

     不过这种算法有个缺点,内存缩小为了原来的一半,这样代价太高了。现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

3、标记-整理(Mark-Compact)算法

    复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。标记-整理算法的工作过程如图:

深入理解 Java垃圾收集器(GC)

4、分代收集算法

根据上面的内容,用一张图概括一下堆内存的布局

深入理解 Java垃圾收集器(GC)

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

所有新生成的对象被划分到年轻代,在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中,而那些需要持久化的对象被划分到持久代


如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个代所使用的收集器。

七、垃圾收集器

jvm是一个进程,垃圾收集器就是一个线程,垃圾收集线程是一个守护线程,优先级低,其在当前系统空闲或堆中老年代占用率较大时触发。

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

深入理解 Java垃圾收集器(GC)

Serial收集器(复制算法)

  新生代单线程收集器,标记和清理都是单线程,优点是简单高效。但需要stw,停顿时间长。

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。

Serial Old收集器(标记-整理算法)

  老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(停止-复制算法) 

  新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(停止-复制算法)

  并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

Parallel Old收集器(停止-复制算法)

  Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

  高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择


借鉴和引用:

https://www.cnblogs.com/xiaoxi/p/6486852.html

https://blog.csdn.net/hudashi/article/details/52058355