2018年第51周-JAVA虚拟机内存模型及垃圾回收机制(概要)

JAVA内存区域

运行时数据区域

根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机管理的内存将会包括以下运行时数据区域:

1.程序计数器

2.Java虚拟机栈(在HotSpot虚拟机中本地方法栈和虚拟机栈合二为一)

3.Java堆

4.方法区(包括运行时常量池) ,存放的是Class的相关信息和常量,又称“永生代”

5.直接内存(NIO使用的)

程序计数器

程序计数器(Program Counter Register)是当前线程所执行的字节码的行号指示器。各个线程之间的计数器互不影响,所以这块内存是线程私有的内存。

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有,生命周期与线程相同。是Java方法执行的内存模型,每个方法执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出等信息。每个方法从调用执行到执行完毕的过程,就是对应这栈帧的一次入栈和出栈过程。

方法区

方法区(Method Area)是线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java堆

这就是重头戏了,是Java虚拟机所管理的内存中最大的一块,也是所有线程共享的一块内存。
此内存存放对象实例,所有也是垃圾回收的主要区域。采用分代收集算法,简单说就是分为新生代和老年代。

新生代又根据对象朝生暮死的特性,分为Eden空间、两块Survivor空间(From空间和To空间)。From空间和To空间是相对的,两者是两快大小相同的内存。新生代垃圾回收器采用复制算法(Copying)来回收新生代,新生的对象都会分配到Eden空间,当回收时,会将Eden空间和一块Survivor空间的存活对象,复制到一块Survivor空间(假设是To空间),此时Eden空间和From空间就会被直接清空,如果To空间不够分配内存给存活的空间,则存活的对象会直接进入老年代的内存空间。
新生代的回收频率高,但每次回收的耗时短,这个回收过程称为Minor GC。HotSpot虚拟机默认Eden空间和一块Survivor空间是8:1

老年代,Java虚拟机管理的内存空间,除去新生代、虚拟机栈、方法区等内存,就是属于老年代。老年代其特点就是对象的存活率很高,甚至100%,所以使用复制算法,会严重浪费内存。所以根据老年代的特点,首先标志所有需要回收的对象,在标记完成后,将所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。这就是标志-整理(Mark-Compact)算法,也有称为标志压缩法,其实是一个东西。
老年代的回收频率低,但每次回收的耗时长,这个回收过程称为Major GC,也粗略认为是Full GC。

直接内存

直接内存(Direct Memory)并不是Java虚拟机运行时数据区的一部分。在JDK1.4引入了NIO,引入了基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数直接分配堆外内存。
其GC回收也是通过Full GC进行回收。

各区域的内存溢出情况

Java堆的异常是:

//可通过配置参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../logs/dump.hprof 把虚拟机在出现内存溢出时Dump当前的内存堆转储存快照
java.lang.OutOfMemoryError: Java heap space

Java虚拟机栈有两种异常分别是:
StackOverflowError异常

//虚拟机配置-Xss128k来设置虚拟机栈内存容量
//stack深度长度为:1000
Exception in thread "main" java.lang.StackOverflowError
   at com.jc.vm.StackOverflowDemo.stackLeek(StackOverflowDemo.java:7)
   ...

OutOfMemoryError异常

java.lang.OutOfMemoryError: unable to create new native thread

虚拟栈OutOfMemoryError异常情况需说明下:

虚拟机提供了参数控制Java堆和方法区的这两部分的内存的最大值。而操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制是2GB,则2GB减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略。如果虚拟机进程本身耗费的内存不计算在内,则剩下的内存就是由Java虚拟机栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数就自然减少,建立线程时容就越容易把剩下的内存耗尽。

如果是建立过多的线程导致的内存溢出,在不减少线程数或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

方法区的异常是:
能导致内存溢出,也就是常量变多或Class变多,可以使用CGLib动态创建类来造成方法区内存溢出

//在JDK7及以下版本,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小
//注:Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
//Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
//JDK8已将参数-XX:PermSize和-XX:MaxPermSize移除了
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
   ...

直接内存的异常
直接内存是不受Java堆大小的限制,不过可以JDK参数-XX: MaxDirectMemorySize(默认与-Xmx大小一样),如果,大于次参数的值,则会抛出java.langOfMemoryError,实际没有真正向操作系统申请分配内存,而是通过计算得出内存无法分配。

对象的内存分配算法

指针碰撞(Bump the Pointer):这种分配算法指Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的放在一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是那个指针向空闲空间那边挪动一段与对象大小想等的距离。 缺点是处理并发不好。
空闲列表(Bump the Pointer):这种分配算法指虚拟机维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。缺点是造成碎片比较多

选择哪种分配算法,是根据Java堆是否规整来决定,所以不同垃圾收集器使用不同的分配算法:
Serial、ParNew等带Compact过程的收集器,采用的分配算法就是指针碰撞。
CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

对象访问方式

对象的访问方式分为使用句柄直接指针(HotSpot使用直接指针)。
两个访问方式各有好处,使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动时(垃圾回收)只需改变句柄中的数据指针即可。
而直接指针访问方式的好处就是速度快,reference直接指向数据,比使用句柄,节省了一次指针定位时间,由于对象的访问是非常频繁,所以能节省很多时间。

对象是否存活判断算法

引用计数(Reference Counting),此算法实现简单,判定效率高。如微软公司的COM(Component Object Model)技术、使用AcionScript3的FlashPlayer、Python语言和Squirrel都使用引用计数算法进行内存管理。缺点,很难解决对象之间相互循环引用的问题。

可达性分析,此算法基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些检点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话:从GC Roots到这个对象不可达)时,则证明对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。

垃圾收集算法

标记-清除(Mark-Sweep)算法,此算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点有两个:一是效率问题;另一个是空间问题,标记清楚后会产生大量不连续的内存碎片。

复制(Copy)算法,是当前主流垃圾回收的思想(针对新生代的内存区域),此算法就将可用的内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存在的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是代价内存耗费比较多。
但专门研究表明, 新生代中的对象98%是“朝生夕死”的,所以并不需要1:1的比例来划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot默认Eden和Survivor的大小比例是8:1,也就是新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion),所以这种垃圾收集算法如果不想浪费50%空间,就需要有额外的空间进行分配担保以应对所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。

标记-整理(Mark-Compact)算法,这是根据老年代的特点提出一种算法,标记过程仍与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法,此算法是根据对象存活周期的不同将内存划分为几块。一般的把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,那就是选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记整理”算法进行回收。

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。

《2018年第51周-JAVA虚拟机内存模型及垃圾回收机制(概要)》

图展示了7种作用不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。其中?是G1收集器

Serial收集器(新生代)

这个收集器时一个单线程,独占式的收集器,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。这是JDK1.3时之前新生代唯一的选择。
参数-XX:UseSerialGC,新生代使用Serial收集器,老年代使用Serial Old收集器。
虚拟机在Client模式下,默认使用该收集器作为新生代收集器。
使用复制算法。

ParNew收集器(新生代)

ParNew收集器其实就是Serial收集器的多线程版本。由于CMS作为老年代的收集器,无法与新生代的PS收集器配合工作。
参数-XX:+UseConcMarkSweepGC,新生代就是默认使用ParNew收集器,老年代使用CMS收集器。
参数-XX:+UseParNewGC,新生代使用ParNew收集器,老年代使用Serial Old收集器。
需考虑-XX:ParallelGCThreads来控制垃圾收集的线程数。
使用复制算法。

Parallel Scavenge收集器(新生代)

PS收集器跟ParNew收集器一样,都是多线程,独占式的收集器
这个收集器与其他收集器关注点不同,CMS等收集器关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而PS收集器的目标则是达到一个可控制的吞吐量(Throughput)。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。关注吞吐量的收集器适合后台运算。
可控制吞吐量
参数-XX:+UseParallelGC,新生代使用PS收集器,老年代使用Serial Old收集器。
参数-XX:+UseParallelOldGC,新生代使用PS收集器,老年代使用Parallel Old收集器。
使用复制算法。

Serial Old收集器(老年代)

单线程,独占式收集器。
在Server模式下,主要有两大用途:一种用途是JDK5以及之前的版本中于PS收集器搭配使用,另一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
虚拟机在Client模式下,默认使用该收集器作为老生代收集器。
参数-XX:UseSerialGC,新生代使用Serial收集器,老年代使用Serial Old收集器。
参数-XX:+UseParNewGC,新生代使用ParNew收集器,老年代使用Serial Old收集器。
参数-XX:+UseParallelGC,新生代使用PS收集器,老年代使用Serial Old收集器。
使用标志压缩算法。

Parallel Old收集器(老年代)

多线程,独占式收集器
JDK6才提供的,在此之前PS收集器只能于Serial Old收集器搭配,效果并不明显,且无法和CMS搭配,所以才诞生Parallel Old收集器。 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
参数-XX:+UseParallelOldGC,新生代使用PS收集器,老年代使用Parallel Old收集器。
使用标志压缩算法。

CMS收集器(老年代)

多线程,整体上不是独占式收集器,只有初始标识和重新标志阶段是独占系统资源的。
CMS(Concurrent Mark Sweep)收集器时一种以获取最短回收停顿时间为目标的收集器。简单的说,此收集器关注点是低停顿时间。是只有在初始标识和重新标志阶段是STW,其他阶段是可以与应用线程一起执行的。
从名字包含“ Mark Sweep”就知道使用的收集算法是“标记——清除”算法。四步骤:
1.初始标记(CMS initial mark) ,需要STW
2.并发标记(CMS concurrent mark)
3.重新标记(CMS remark) ,需要STW
4.并发清除(CMS concurrent sweep)
优点:并发收集、低停顿

目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应时间,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常适合。如我所在过的一家互联网公司在64位的jdk情况下使用CMS收集器

缺点:
1.CMS收集器对CPU资源敏感
2.CMS收集器无法处理浮动垃圾(Floating Garbage,指的是CMS在整个过程基本都不STW,所以应用程序还在不断产生垃圾,这些垃圾称为浮动垃圾),则需通过-XX:CMSInitiatingOccupancyFraction来配置老年代内存达到多少时触发CMS收集。JDK5默认收68%,JDK6默认是92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集。所以-XX:CMSInitiatingOccupancyFraction配置越高,就很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低。如我所在过的一家互联网公司在64位的jdk情况下使用-XX:CMSInitiatingOccupancyFraction=70
3.CMS是基于“标记——清除”算法实现的收集器,则会产生大量空间碎片。则需配置个参数-XX:+UserCMSCompactAtFullCollection来开启碎片整理。但依然会造成停顿时间太长,所以虚拟机设计者提供了-XX:CMSFullGCsBeforeCompaction,来控制执行多少次不压缩的Full GC后进行带压缩的Full GC,默认值为0(表示每次进入Full GC时都进行碎片压缩)

参数-XX:+UseConcMarkSweepGC,新生代就是默认使用ParNew收集器,老年代使用CMS收集器。
使用标志清除算法。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。葱JDK 6u14开始就有Early Access版本的G1收集器供开发人员实验、试用,由此开始G1收集器的“Experimental”状态持续了数年时间,直至JDK 7u4,Sun公司才认为它达到足够成熟的商用程度,移除了“Experimental”的标识。
未来可以替换JDK 1.5中发布的CMS收集器。G1具备一下特点:
1.并行并发
2.分代收集
3.空间整合,分Region去处理,类似于并发的map
4.可预测的停顿

G1收集器运作大致可分为以下步骤:
1.初始标记(Initial Marking)
2.并发标记(Concurrent Marking)
3.最终标记(Final Marking)
4.筛选标记(Live Data Counting and Evacuation)

附录

并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent): 指用户线程与垃圾收集线程同事执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

参考:
https://bugs.java.com/bugdata…
《深入理解Java虚拟机》
《实战Java虚拟机》

    原文作者:java内存模型
    原文地址: https://segmentfault.com/a/1190000017488793
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞