本文已收录GitHub,更有互联网大厂面试真题,面试攻略,高效学习资料等
堆是java创建对象的区域(String对象在常量池中),也是垃圾回收最多的地方。但是除了堆空间还有方法区存在需要回收的垃圾
回收方法区
废弃的常量
在常量池中存在一个字面量A,如果系统中没有一个地方引用`A``,这时候发生垃圾回收,如果有必要这个字面量就会被清理出常量池。
注意是如果有必要。比如上一篇文章中引用的例子,就没有回收字符串。
无用的类
当满足以下条件时,这个类就可以被回收,而不是一定会回收。
java有一个非常大的好处就是会自动进行垃圾回收,而不用手动释放对象所占用的内存。当以一个对象不再被引用的时候就可以进行垃圾回收,那么如何判断一个对象是否在被使用呢?
引用计数法
引用计数法很简单,只需要在对象创建之初给对象加一个引用计数器,每当有一个地方引用他就+1,引用失效就-1,当引用计数器为0,则对象不再被引用。每次垃圾回收,只需要遍历一遍所有的引用计数器就可以。但是对于循环引用,引用计数法则无法释这两个对象。
可达性分析算法
通过一系列被称为GC Root的对象为起点,从这些节点往下搜索,搜索走过的路径称之为引用链,当一个对象到GC Root没有任何引用链的时候,则证明此对象不可达。
在JVM中,可以被用作GC Root的对象有:
枚举根节点
对于根节点的枚举有如下的问题:
解决方法
是否可以用额外的空间记录下每个Reference的位置,这样的话GC的时候从这个结构中直接读取这个结构,而不用进行全栈扫描。事实上,大部分主流的虚拟机也确实是这样做的,以HotSpot为例,它使用一种OopMap的数据结构来保存这类信息。
一个栈意味着一个线程,而一个栈桢代表了一个方法,每个被JIT编译过后的方法会在一些特定的位置记录下OopMap记录了执行到该方法的某条指令的时候,栈上和寄存器的哪些位置是引用,这样GC在扫描到这些栈的时候就会查询这些OopMap就知道哪里是引用。这些位置主要在:
而这些位置就被称之为“安全点”,之所以要选择一些特定位置来记录OopMap,是因为如果对每条指令的位置都记录OopMap的话,这些记录就会比较大,那么空间开销就会显得不值得。
GC发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的OopMap,枚举根节点时,递归遍历每个栈桢的OopMap,通过栈中记录的被引用的对象的内存地址,即可找到这些对象。
安全点与安全区域
安全点
程序在执行时并不是任何时间都可以进行GC,只有到达有OopMap记录的位置才可以执行GC,整个位置称之为安全点
安全点的选定基本是以程序“是否具有让程序长时间执行的特征”为标准选定的。程序一般不会因为指令流太长而长时间执行(每个指令执行的时间都很短)。“长时间执行”的典型特征就是指令序列的服用,例如:循环、递归、方法调用。所以具有这些功能的指令才会产生安全点。
安全区域
安全区域指在这一段代码之中,引用关系不会发生变化,在这一段代码之中,任一点都是安全点。任何一个地方都可以中断线程开始GC。
当线程执行到安全区域后,首先标识自己已经进入安全区域,那么这段时间JVM要发起GC时就不用管标记自己进入安全区的线程。线程要离开安全区时,首先需要先检查系统是否已经完成了根节点的选举,如果完成则线程继续执行,否则要继续等待收到可以安全离开安全区的信号。
如何保证GC发生时,所有的线程都跑到了安全点上呢?
当要进行GC的时候,会让所有的线程都在安全点中断,就有两种方式:
标识的设置和安全点是重合的,标识的设置和安全点是重合的。除此之外还有一个创建对象需要分配内存的地方。
假设存在如下的内存区域:
下文将以这块内存为例进行垃圾收集算法的分析
标记-清除算法
顾名思义,标记清除算法会为两个阶段,1-标记,2-清除。
标记:垃圾收集器从GC Roots出发,进行搜索,然后对所有可以访问的对象打上标识,标记其为可达的对象,标记一般保存在header中
清除:垃圾收集器对堆内存进行线性遍历,如果发现某个对象没有被标记为可达,就会将其回收,回收后效果如下图
优点
缺点
复制算法
复制算法,就是将内存划分为相等的两块,每次只是用其中一块,当这块内存使用完了就将还存活的对象复制到另一块,然后将这块空间清理掉,这样使得每次对内存的回收都是半区回收。
复制算法的示意图如下图:
优点
缺点
标记—整理算法
复制算法在对象存活较多的时候会进行较多的操作,如果对象全部存活复制将会进行100%,并且浪费50%的内存空间作为担保。
标记—整理算法和标记—清除算法前半部分一样,只是后续不是清理,而是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
在当前主流的垃圾收集器当中(g1除外),基本都采用一种分代收集算法。根据对象存活周期,将java堆分为新生堆和老年堆。对于新生堆,采用复制算法,对于老年堆采用标记-清除或者标记-整理算法。
研究人员发现大多数的对象都是“朝生夕灭”,对于这样的对象,生存周期很短,可以将其放入新生堆,因为其生存时间很短,所以新生堆采用复制算法的时候没有必要使用1:1的比例划分内存。
而是分为较大的Eden空间和两块较小的Suvivor空间;HotSpot的Eden和Suvivor的比例为8:1。回收时将Eden和一块Suvivor上还存活的对象,一次性copy到另一块Suvivor上,然后清理掉以前的两块区域。这样每次新生代可用的内存空间占整个新生堆的90%,只有10%会被浪费。
我们没有办法保证新生代回收的时候只剩下不多于10%的对象存活。当Suvivor空间不够用时,就需要依赖其他内存(老年堆)进行分配担保。对于存活过一定gc次数的对象放进老年堆。
老年堆对象存活率高,使用复制算法可能就需要1:1的空间,这样就会浪费内存,因此使用的是标记-清除或者标记-整理算法。
保守式GC
HotSpot虚拟机在栈上使用OopMap记录下了哪些位置是引用类型,根据记录的类型类型开始查找堆中存活的对象。
虚拟机最初的实现当中是没有记录每个数据的类型的,JVM也无法区分内存里某个位置的数据到底应该解读为引用类型还是其他数据类型,这种条件下,实现出来的GC就是“保守式GC”。在进行GC时,JVM开始从一些已知的位置(例如栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。
这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。然后递归的这么扫描出去。
优点
缺点
有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。
半保守式GC
保守式GC没有在JVM中记录任何类型信息,半保守式GC会在对象上记录类型信息,这样的话,扫描栈的时候仍然和保守式GC一样,但是扫描到堆上的时候,对象上带了足够的类型信息,JVM就能判断出栈中这个位置是不是一个指向堆中对象的指针,以及这个对象内什么位置数据是引用类型,这种是“半保守式GC”,也称之为“根上保守”。
由于半保守式GC在堆内部的数据是准确的,所以它可以在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就可以移动了。
准确式GC
对于垃圾回收,JVM关心的就是扫描的根节点是不是一个指向堆内存的指针,那么就是在栈上记录下那个位置式引用类型,是指向堆上对象的指针,在HotSpot虚拟机中这个数据结构就是OopMap