JVM系列-JVM内存区域与GC机制

JVM内存区域

了解Java GC之前,必须先搞清楚JVM中内存区域的划分。


JVM内存区域图

JVM中内存区域大致可分为如上图所示几大区域。其中:

堆区

JVM只有一个堆,堆区是JVM内存管理中最大的一块,也是GC主要工作区域,是线程共享的。堆区的主要作用是存储对象实例,一般来说,所有的对象都在堆上分配内存。

方法区

JVM方法区又称静态区,存放所有的class和静态变量、final常量。在HotSpot虚拟机中也被称为永生代,线程共享。

虚拟机栈

线程的每个方法在执行的同时都会创建一个栈桢,栈桢中存储的有局部变量表,操作站,动态链接,方法出口灯,当方法被调用时,栈桢在JVM栈中入栈,当方法执行结束的时候,栈桢出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回的地址等,在局部变量表中在编译是就已经确定好的,方法在运行时所需要分配的空间在栈桢中完全确定的,在方法生命周期内都不会改变。
虚拟机栈中会出现2种异常:StatckOverFlowError 和 OutOfMemoryError;当线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError,如无限递归,就容易出现这个问题。线程不断的申请内存空间,直至内存不足,会抛出OutOfMemoryError异常,如无限循环申请内存空间就会导致OOM异常。

本地方法栈

本地方法栈的作用和虚拟机栈很多方面都相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是执行native方法的。线程私有。
Java对一些底层操作系统或者某些硬件交换信息时,如何用Java来实现在效率上非常低,对于这类方法通常是使用其他语言实现,如C或者C++,我们可以使用System.loadLibrary()来调用Dll。

程序计数器

程序计数器记录着正在执行的虚拟机字节码指令地址,如果执行的本地方法,则程序计数器为Undefined,因为只保存当前指令的地址,所以不会存在内存溢出的问题,也是唯一一个没有定义OOM的区域。

JAVA GC 算法

Java GC(垃圾回收)机制是一种和垃圾回收的自动内存管理机制。GC机制对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄露和溢出问题。

GC的评价标准
  • 吞吐量
  • 最大暂停时间(stop the world)
  • 堆使用效率
  • 访问的局部性
GC算法

在JAVA中GC算法主要分为3种,GC标记-清除算法、引用计数算法、GC复制算法,其他的GC算法多数都是以上GC算法的组合和应用。

GC标记-清除算法

标记阶段:递归标记堆中指针数组能够访问的对象,将每个活动对象打上mark。
清除阶段:遍历堆将没有标记的对象,将其回收(将其连接到空闲链表上)。
再分配阶段:遍历空闲链表,寻找大于或等于待分配的size的分块。大小相等直接返回,大于则分割,剩余返回空闲链表
合并阶段:将连续空闲分块合并。

优点:实现简单,对象不移动,可与保守式GC算法兼容;缺点:碎片化过于严重,分配的时候需要遍历空闲链表,效率低下,与写时复制不兼容。

算法改进:

  • 引入多个空闲链表,每个链表拥有固定分块,一般来说设定2、3、4、5..100字,以及大于等于101字,拱100个空闲链表,如果出现需要分配大于100字的size,将再第100个链表中查找匹配返回。分配时不再需要遍历链表,缓解分配效率低下的问题。
  • BIBOP法:将大小相近的对象整理成固定大小的块进行管理。将堆分割成固定大小的块,让每个块只能配置同样大小的对象。优点减少碎片化,缺点堆使用效率不够高。
  • 位图标记,引入二进制位图表格进行标记对象,解决写时复制的不兼容的问题。清除阶段不再需要遍历堆。

引用计数法
引入计数器,用增减计数器的值来进行内存管理。在分配内存空间时,将对象的计数器+1. 在更新指针的过程中,增加新应用对象的ref_cnt,减少指针原先对象的ref_cnt计数。如果ref_cnt为0,则为垃圾,将被立即回收。

优点:引用计数,可以及时回收垃圾,最大暂停时间短,减少指针查找的次数。缺点:增减操作频繁开销大;计数器保证位宽,需要占用空间,堆的使用效率低;循环引用无法回收。

算法改进:

  • 延迟引用计数法,使用ZCT(zero count Table)记录计数为0的对象,然后进行统一回收。通过延迟引用计数法,延迟了根引用的计数,减轻了根引用发生变化带来计数器频繁增减的开销,但丧失了垃圾可以立即回收的优点。

  • sticky引用计数法,减少计数器的位宽,会引入新的问题,计数器溢出爆表。对于计数器爆表可以选择啥都不做,也可以使用标记-清除算法清除垃圾,这样可以回收循环引用,但是吞吐量会减少。

  • 1位引用计数法,计数器只有1位,0表示被引用数为1,1表示被引用数大于等于2。通过指针复制的方式更新指针。优点:不需要再更新计数器的时候去读取对象,效率高;无需为计数器留足空间,节省内存消耗。缺点是还会存在计数器爆表。

  • 部分标记-清除算法,将对象涂成4中颜色来管理内存,黑(绝对不是垃圾),灰(搜索完毕的对象),白(绝对是垃圾),阴影(可能是循环垃圾)。mutator删除根到对象的引用,会将对象的计数减1,并将其涂成阴影并将指针追加至阴影队列中。在new_obj的过程中,如果发生无法分配区块,将调用scan_hatch_queue() 找出被涂成阴影的对象,依次执行paint_gray、scan_gray、collect_white。缺点:需要多次查找对象,效率低下。优点:解决了循环对象不可回收的问题。

    • paint_gray:将对象涂成灰色,对子对象进行减量操作。
    • scan_gray: 对于ref_cnt大于0的对象涂黑,等于0 的对象及其子对象涂白。
    • collect_white: 回收白色对象.

GC复制算法
将堆空间等分成大小相同的两份,执行GC时候,将from空间的活动对象复制至to空间,复制完成后,回收from空间。复制的时候先将原有对象打上copy标签,指向新空间的对象存放在obj.forwarding中。

优点:吞吐量高,可实现高速分配,不会发生碎片化,与缓存兼容。
缺点:堆使用效率低下;对象移动了,不兼容保守式GC算法,递归调用复制子对象,每次复制都要调用函数。

算法改进:

  • Cheney的GC复制算法,采用迭代进行复制的算法,广度优先搜索。优点:抑制递归调用函数额外的开销和栈的消耗;缺点:无法兼容缓存。
  • 近似深度优先的搜索算法,原理是将有引用关系的对象集中在一页中,再每个页中执行广度优先搜索。优点:可以兼容缓存。
  • 多空间复制,2个空间执行GC复制,其他空间执行GC标记-清除算法。优点:堆空间使用效率得到提高;缺点:引入GC标记-清除算法,带来的问题:分配耗时,碎片化。

分代GC算法
原理:分代GC中将对象分成几代。针对不同的代是用不同的GC算法。刚生成的对象叫新生代对象,经过一定次数GC的对象成为老年代GC。新生代分为eden区,from区,to区,多采用复制算法。老生代多采用标记清除、标记压缩/整理算法。

优点:吞吐量得到优化。

增量式GC算法

增量GC名称的由来跟全量GC相对,就是每次只处理一小部分的对象。把GC堆切分为很多小块(叫做chunk或者region),然后每次GC增量式对一部分小块做回收(而不必对整个GC堆做回收),这样就把一次大收集拆分成了多次小的增量式收集,减小了每次收集的工作量——也就减小了GC暂停时间。hotspot JVM 用 train GC作为增量式GC的实现,但后面被废弃了。目前G1垃圾收集器,采用的是增量式GC的算法。

HotSpot JVM GC 机制

HotSpot JVM 大致可分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。我们创建的对象在其生命周期基本上活动在这几块区域中。

年轻代(Young Generation)

年轻代又可以细分为:Eden区、Survivor 区(From区、To区)默认比例:Eden:from:To = 8:1:1。当我们创建一个对象时,JVM首先将对象分配在eden区,其中大部分对象在该区域死亡,成为垃圾对象。当Eden区内存空间不足,年轻代将执行Young GC回收垃圾对象,在此阶段,年轻代利用Survivor 区GC复制算法来收集垃圾。经过几次Young GC 仍然存活的对象,将根据相关策略晋升至年老代。

年老代(Old Generation)

对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行 Full GC。如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。

显然HotSpot JVM 的GC机制的基本算法是:分代收集算法。在年轻代中的Young GC使用的GC复制算法;在年老代中,将使用标记-清除算法、标记-压缩算法;对于方法区(永生代)中,常量池中的常量,无用的类信息,没有引用了就可以被回收,但回收不是必须的。

垃圾收集器

在年轻代执行GC的时候,所有的线程都将会暂停工作(stop the world),尽管各种GC收集器都在不断优化减少暂停时间,但目前为止GC的暂停时间还是存在的。

*** Serial收集器***:新生代收集器,串行收集器。使用复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)。GC触发机制:之前Young GC晋级到年老代的平均大小 < 年老代的剩余空间 < eden+from Survivor的使用空间。当HandlePromotionFailure为true,则仅触发Young gc;如为false,则触发Full GC。

ParNew收集器:新生代收集器,并行收集器。使用复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

Parallel Scavenge 收集器:新生代收集器。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),可以利用这2个参数精确控制吞吐量。用开关参数-XX:+UseAdaptiveSizePolicy可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等。GC触发机制:在回收前PS GC会先检测之前每次PS GC时,晋升到老生代的平均大小是否大于老生代的剩余空间,如大于则直接触发full GC;

Serial Old收集器:老年代收集器,串行收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。触发机制:old gen空间不足;perm gen空间不足;Young GC时的悲观策略;Young GC后在eden上分配内存仍然失败;

Parallel Old收集器:老年代收集器,并行收集器,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

CMS(Concurrent Mark Sweep)收集器:老年代收集器,并发收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集。触发机制: 当老生代空间的使用到达一定比率时触发;当perm gen采用CMS收集且空间使用到一定比率时触发;Hotspot根据成本计算决定是否需要执行CMS GC;可通过-XX:+UseCMSInitiatingOccupancyOnly来去掉这个动态执行的策略。

Yong GC 悲观策略:
1、在YGC执行前,min(目前新生代已使用的大小,之前平均晋升到old的大小中的较小值) > 旧生代剩余空间大小 ? 不执行YGC,直接执行Full GC : 执行YGC;
2、在YGC执行后,平均晋升到old的大小 > 旧生代剩余空间大小 ? 触发Full GC : 什么都不做。

GC监控

命令行工具:jmap,jstat,jstack。
GUI工具:jconsole,jvisualvm

*** 如果有纰漏,请留言指正。***

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 13,237评论 3 82
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 68,166评论 17 295
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,212评论 0 7
  • 一. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对...
    Stan_Z阅读 1,458评论 0 25
  • Ø每做完一题目,自动切换到一下题; 点击下一题才切换到下一题,没有选中选项时下一题是不可选状态。 Ø做到最后一题时...
    亭止阅读 114评论 0 0