先打好基础,阿里架构师一文带你深刻理解JVM内存回收机制

本文已收录GitHub,更有互联网大厂面试真题,面试攻略,高效学习资料等java

1、垃圾回收发生的区域

堆是java建立对象的区域(String对象在常量池中),也是垃圾回收最多的地方。可是除了堆空间还有方法区存在须要回收的垃圾git

回收方法区github

废弃的常量面试

在常量池中存在一个字面量A,若是系统中没有一个地方引用`A``,这时候发生垃圾回收,若是有必要这个字面量就会被清理出常量池。算法

注意是若是有必要。好比上一篇文章中引用的例子,就没有回收字符串。安全

无用的类数据结构

当知足如下条件时,这个类就能够被回收,而不是必定会回收。ide

  1. 全部的类实例都已经被回收也就是java堆里面不存在该类的任何实例
  2. 加载类的ClassLoader已经被回收
  3. 该类对应的Java.long.Class对象任何地方被引用,没法经过反射访问该类的方法。

2、如何判断对象是否能够被回收

java有一个很是大的好处就是会自动进行垃圾回收,而不用手动释放对象所占用的内存。当以一个对象再也不被引用的时候就能够进行垃圾回收,那么如何判断一个对象是否在被使用呢?学习

引用计数法线程

引用计数法很简单,只须要在对象建立之初给对象加一个引用计数器,每当有一个地方引用他就+1,引用失效就-1,当引用计数器为0,则对象再也不被引用。每次垃圾回收,只须要遍历一遍全部的引用计数器就能够。可是对于循环引用,引用计数法则没法释这两个对象。

可达性分析算法

经过一系列被称为GC Root的对象为起点,从这些节点往下搜索,搜索走过的路径称之为引用链,当一个对象到GC Root没有任何引用链的时候,则证实此对象不可达。

图1  可达性分析示例图

在JVM中,能够被用做GC Root的对象有:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

3、HotSpot实现

枚举根节点

对于根节点的枚举有以下的问题

  1. 能够做为根节点(GC Roots)的节点主要是全局性的引用(方法去中静态属性引用的对象和方法区中常量引用的对象)与执行上下文(栈中引用的对象)
  2. 在一次可达性分析过程当中,不能出现分析过程当中对象引用关系还在不断变化的状况,不然没法保证分析结果的准确性,为了达到这一目的,GC过程当中就必须停顿全部的java线程
  3. 垃圾收集时,手机线程会对栈上的内存进行扫描,看看哪些位置存储了Reference类型,若是发现某个位置确实存的是Reference类型,整个Reference所引用的对象就能够做为根节点,
    他所能到达的对象都不能被回收。
  4. 栈上的本地变量表中只有一部分是Reference类型,而那些非Reference类型的数据对于垃圾回收毫无用处,可是若是对于栈进行全栈扫描将会是一种对时间和资源的浪费,尤为是暂停了用户线程

解决方法

是否能够用额外的空间记录下每一个Reference的位置,这样的话GC的时候从这个结构中直接读取这个结构,而不用进行全栈扫描。事实上,大部分主流的虚拟机也确实是这样作的,以HotSpot为例,它使用一种OopMap的数据结构来保存这类信息。

一个栈意味着一个线程,而一个栈桢表明了一个方法,每一个被JIT编译事后的方法会在一些特定的位置记录下OopMap记录了执行到该方法的某条指令的时候,栈上和寄存器的哪些位置是引用,这样GC在扫描到这些栈的时候就会查询这些OopMap就知道哪里是引用。这些位置主要在:

  • 循环的末尾
  • 方法临返回前/调用方法的call指令以后
  • 可能抛出异常的位置

而这些位置就被称之为“安全点”,之因此要选择一些特定位置来记录OopMap,是由于若是对每条指令的位置都记录OopMap的话,这些记录就会比较大,那么空间开销就会显得不值得。

GC发生时,程序首先运行到最近的一个安全点停下来,而后更新本身的OopMap,枚举根节点时,递归遍历每一个栈桢的OopMap,经过栈中记录的被引用的对象的内存地址,便可找到这些对象。

安全点与安全区域

安全点

程序在执行时并非任什么时候间均可以进行GC,只有到达有OopMap记录的位置才能够执行GC,整个位置称之为安全点

安全点的选定基本是以程序“是否具备让程序长时间执行的特征”为标准选定的。程序通常不会由于指令流太长而长时间执行(每一个指令执行的时间都很短)。“长时间执行”的典型特征就是指令序列的服用,例如:循环、递归、方法调用。因此具备这些功能的指令才会产生安全点。

安全区域

安全区域指在这一段代码之中,引用关系不会发生变化,在这一段代码之中,任一点都是安全点。任何一个地方均可以中断线程开始GC。

当线程执行到安全区域后,首先标识本身已经进入安全区域,那么这段时间JVM要发起GC时就不用管标记本身进入安全区的线程。线程要离开安全区时,首先须要先检查系统是否已经完成了根节点的选举,若是完成则线程继续执行,不然要继续等待收到能够安全离开安全区的信号。

如何保证GC发生时,全部的线程都跑到了安全点上呢?

当要进行GC的时候,会让全部的线程都在安全点中断,就有两种方式:

  • 抢占式中断:不须要代码配合。当GC发生时,让全部的线程都终端,而后让不在安全点的线程继续执行到安全点上。不过通常不采用这种方式
  • 主动式中断:当GC须要中断线程时,不对线程进行操做,仅设置一个标识。各个线程轮询这个标识,当发现这个标识被设置时,使得程序运行到最进的安全点时,主动挂起。

标识的设置和安全点是重合的,标识的设置和安全点是重合的。除此以外还有一个建立对象须要分配内存的地方。

4、垃圾回收算法

假设存在以下的内存区域:

图2  原始状况内存中对象的分布

下文将以这块内存为例进行垃圾收集算法的分析

标记-清除算法

顾名思义,标记清除算法会为两个阶段,1-标记,2-清除。

标记:垃圾收集器从GC Roots出发,进行搜索,而后对全部能够访问的对象打上标识,标记其为可达的对象,标记通常保存在header中

图3  标记阶段

清除:垃圾收集器对堆内存进行线性遍历,若是发现某个对象没有被标记为可达,就会将其回收,回收后效果以下图

图4  标记清除算法进行垃圾回收

优势

  1. 实现简单
  2. 与保守式GC算法兼容

缺点

  1. 内存碎片化严重
  2. 分配速度缓慢,因为空闲块的维护是用链表实现的,分块可能不连续,每次分配都须要遍历链表,极端状况下要遍历震整个链表。
  3. 标记和清除的效率都不高,

复制算法

复制算法,就是将内存划分为相等的两块,每次只是用其中一块,当这块内存使用完了就将还存活的对象复制到另外一块,而后将这块空间清理掉,这样使得每次对内存的回收都是半区回收。

复制算法的示意图以下图:

图5  复制算法

优势

  1. 内存分配时不用考虑碎片的状况只须要移动栈顶指针分配内存便可
  2. 实现简单,高效

缺点

  1. 可用内存缩小为原来的一半

标记—整理算法

复制算法在对象存活较多的时候会进行较多的操做,若是对象所有存活复制将会进行100%,而且浪费50%的内存空间做为担保。

标记—整理算法和标记—清除算法前半部分同样,只是后续不是清理,而是让全部存活的对象都向一端移动,而后清理掉边界之外的内存。

图6  标记整理算法

5、JVM中使用的垃圾收集算法

在当前主流的垃圾收集器当中(g1除外),基本都采用一种分代收集算法。根据对象存活周期,将java堆分为新生堆和老年堆。对于新生堆,采用复制算法,对于老年堆采用标记-清除或者标记-整理算法。

研究人员发现大多数的对象都是“朝生夕灭”,对于这样的对象,生存周期很短,能够将其放入新生堆,由于其生存时间很短,因此新生堆采用复制算法的时候没有必要使用1:1的比例划份内存。

而是分为较大的Eden空间和两块较小的Suvivor空间;HotSpot的Eden和Suvivor的比例为8:1。回收时将Eden和一块Suvivor上还存活的对象,一次性copy到另外一块Suvivor上,而后清理掉之前的两块区域。这样每次新生代可用的内存空间占整个新生堆的90%,只有10%会被浪费。

咱们没有办法保证新生代回收的时候只剩下很少于10%的对象存活。当Suvivor空间不够用时,就须要依赖其余内存(老年堆)进行分配担保。对于存活过必定gc次数的对象放进老年堆。

老年堆对象存活率高,使用复制算法可能就须要1:1的空间,这样就会浪费内存,所以使用的是标记-清除或者标记-整理算法。

6、GC的分类

保守式GC

HotSpot虚拟机在栈上使用OopMap记录下了哪些位置是引用类型,根据记录的类型类型开始查找堆中存活的对象。

虚拟机最初的实现当中是没有记录每一个数据的类型的,JVM也没法区份内存里某个位置的数据到底应该解读为引用类型仍是其余数据类型,这种条件下,实现出来的GC就是“保守式GC”。在进行GC时,JVM开始从一些已知的位置(例如栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。

这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(一般分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就确定不是指针),之类的。而后递归的这么扫描出去。

优势

  1. 实现简单

缺点

  1. 会有部分对象原本应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集。会有一部分已经不须要的数据占用着GC堆空间,可是全部应该存活的对象都会活着,对程序语义来讲时安全的
  2. 因为是疑似指针,那么就不知道这个究竟是不是指针,因此这些值就都不能改写。移动对象就须要改写指针,也就是说对象不可移动,所以通常使用标记-清除的方式来进行垃圾回收。

有一种办法能够在使用保守式GC的同时支持对象的移动,那就是增长一个间接层,不直接经过指针来实现引用,而是添加一层“句柄”(handle)在中间,全部引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容便可。

半保守式GC

保守式GC没有在JVM中记录任何类型信息,半保守式GC会在对象上记录类型信息,这样的话,扫描栈的时候仍然和保守式GC同样,可是扫描到堆上的时候,对象上带了足够的类型信息,JVM就能判断出栈中这个位置是否是一个指向堆中对象的指针,以及这个对象内什么位置数据是引用类型,这种是“半保守式GC”,也称之为“根上保守”。

因为半保守式GC在堆内部的数据是准确的,因此它能够在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就能够移动了。

准确式GC

对于垃圾回收,JVM关心的就是扫描的根节点是否是一个指向堆内存的指针,那么就是在栈上记录下那个位置式引用类型,是指向堆上对象的指针,在HotSpot虚拟机中这个数据结构就是OopMap

总结

  1. 垃圾回收不止是发生在堆区,对于方法区中产生的垃圾有可能会被回收。在以前的从JDK源码理解java引用一文中举了不会被回收的例子
  2. 虚拟机通常采用引用可达性分析算法来寻找不被使用的对象,其实寻找到的是正在被使用的对象,剩下的就是再也不被使用的对象。
  3. 除了g1垃圾收集器。其余的垃圾收集器都有明显的区分老年代和新生代进行垃圾回收,因为老年代和新生代对象存货时间不同,采用不一样的垃圾回收算法
相关文章
相关标签/搜索