持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情
Java的垃圾回收机制是java语言的一大优势,相比于C和C++手动内存管理经常出现忘记回收内存造成内存泄露,Java的自动内存管理可以尽量避免内存泄漏,但是对于程序员来说也增加了必须学习Java虚拟机运行机制的负担,如果出现内存泄露,我们必须了解Java垃圾回收机制才能排查问题进行修复。
一、垃圾回收概述
关于垃圾回收有三个经典问题:
- 哪些内存需要回收?即需要判断什么是垃圾。
- 什么时候回收?
- 如何回收?
1.什么是垃圾?
简单来说就是程序中没有任何指针指向的对象。
如图所示,p1指向新的对象,原对象不存在其他指针指向,成为垃圾对象。
2.为什么需要GC(垃圾回收)
可以从三方面考虑:
- 内存不是无限的,程序会持续运行不断创建新的的对象,内存迟早会消耗完。
- 在分配内存时,会产生一些内存碎片,进行垃圾回收时可以合并调整内存,以便JVM能够有足够内存空间分配给新的对象。
- 随着应用程序的业务越来越庞大、复杂,没有GC就不能保证程序的正常运行,程序经常会因为内存不足而STW(Stop the world)。
3.Java回收哪些区域的内存
Java堆是垃圾回收的重点,堆中保存着程序运行时创建的各种对象。
二、垃圾回收算法
垃圾回收算法可以按阶段划分
- 标记阶段:引用计数法、可达性分析算法
- 清除阶段:标记-清除算法、复制算法、标记压缩算法
堆中存放着几乎所有的Java对象实例,在执行GC之前,首先需要区分内存中哪些对象是存活对象,哪些是已经死亡的对象,只有标记为已经死亡的对象才会被GC回收,标记对象的过程就是垃圾标记阶段。下面介绍标记阶段的两种算法。
1.引用计数法
引用计数法在实现时,设置了一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器+1;引用失效,引用计数器就-1。对象A的引用计数器的值为0,即表示对象A成为垃圾,不可能被使用。
引用计数法实现简单,垃圾对象便于识别,判定效率高,没有延迟。但引用计数法存在一个缺陷,即如上图所示,无法处理循环引用的情况。
2.可达性分析算法
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
在Java语言中,GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象
- 比如: Java类的引用类型静态变量
- 方法区中常量引用的对象
- 比如:字符串常量池(string Table)里的引用
- 所有被同步锁synchronized持有的对象人
- Java虚拟机内部的引用。
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法( Mark-Sweep )、复制算法( copying ) 、标记–压缩算法( Mark-Compact ) 。
3.标记-清除算法
执行过程: 当堆中的有效内存空间(avaifable memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除:collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
4.复制算法
复制算法的核心思想是将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
复制算法没有标记和清除过程,实现简单,运行高效,复制过去以后保证空间的连续性,不会出现“碎片”问题,但需要两倍内存空间。
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
5.标记-压缩算法
执行过程:
- 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
- 之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
在清除阶段,三种方法各有优缺点,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。