JVM内存区域
了解Java GC之前,必须先搞清楚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
*** 如果有纰漏,请留言指正。***