JVM内存划分与GC机制

来源:互联网 发布:运营数据分析的步骤 编辑:程序博客网 时间:2024/05/21 19:35

JVM内存划分

<> 栈 本地方法栈1 堆 方法区 程序计数器 线程私有 true true false false true 定义异常 StackOverFloorError2 & OutOfMemoryError3 OutOfMemoryError OutOfMemoryError OutOfMemoryError4 null 异常备注 unable to create new native thread Java heap space PermGen space 生命周期 线程级别 虚拟机级别 虚拟机级别 存储内容 栈帧5 几乎所有的对象实例都在这里分配内存 已被虚拟机加载的类信息、变量、静态变量、即时编译后的代码等数据,运行时常量池6 动态扩容 支持 支持 支持 支持 设置 -Xms -Xmx -XX:PerSize -XX:MaxPerSize 描述 gc管理的主要区域 当前线程所执行的字节码的行号指示器

处理OutOfMemoryError异常思路:
首先根据异常备注信息判断异常发生区域,Java heap space为最常见。解决堆内存OOM,一般手段是先通过内存映射工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否有必要,也就是要先分清楚是出现了内存泄漏还是内存溢出。如果是内存泄漏可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC RootsRoots相关联并导致垃圾收集器无法自动回收它们。若不存在内存泄漏,那就应当检查虚拟机的堆参数,与机器内存对比看是否还可以调大,从代码上检查是否存在某些对象声明周期过程、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

对象存活判定算法

引用计数法

 *tips:*无法解决对象间的传递引用问题。

可达性分析算法

通过一系列的称为“GCRoots“的对象作为起点,从这些节点开始向下搜素,搜索走过的路经称为“引用链“,当一个对象到GCRoots没有任何引用链相连时,则证明对象时不可用的。GCRoots的对象包括:1. 虚拟机栈(栈帧中本地变量表)中引用的对象2. 方法区中类静态属性引用的对象3. 方法区中常量引用的对象4. 本地方法中JNI引用的对象

垃圾回收算法

标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
不足:
1.效率问题;标记和清除两个过程效率都不高
2.空间问题;标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法

算法将可用内存将容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
不足:
1.将内存缩小为了原来的一半。
2.在对象存活率较高时要进行较多的复制操作,效率会变低。
现在的商业虚拟机都采用这用算法来回收新生代,由于新生代中98%的对象都是“朝生夕死”,所以并不需要1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor空间上,清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存被“浪费”。(当Survivor空间不够用时,需要依赖其它内存(这里指老年代)进行分配担保。)

标记-整理算法

算法分为“标记”和“整理”两个阶段:首先标记出所有需要回收的对象,在标记完成后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。适用于老年代

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因对象存活率高、没有额外空间对他进行分配担保,就必须采用“标记-清理”或“标记-整理”算法来进行回收。

垃圾回收器

Serial收集器

1.回收新生代,采用“复制”算法
2.单线程
3.回收过程中会stop the world

SerialOld收集器

1.回收老年生代,采用“标记-整理”算法
2.单线程
3.回收过程中会stop the world

ParNew收集器

1.回收新生代,采用“复制”算法
2.多线程并行
3.回收过程中会stop the world

Parallel Scavenge

1.回收新生代,采用“复制”算法
2.多线程并行
3.回收过程中会stop the world
4.目标是达到一个可控制的吞吐量

ParallelOld收集器

1.回收老年代,此采用“标记-整理”算法
2.多线程并行
3.回收过程中会stop the word
4.是Parallel Scavenge的老年代版本

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间做为目标的收集器,特点是并发收集、低停顿,作用于老年代。基于“标记-清除”算法实现,整个过程分为4个步骤:
1.初始标记(CMS initial mark)
2.并发标记(CMS concurrent mark)
3.重新标记(CMS remark)
4.并发清除(CMS concurrent sweep)
其中,初始标记、重新标记仍需要“Stop The World”。初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Tracing的过程,而重新标记则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记纪录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
不足:
1.对CPU资源敏感。在并发阶段,虽不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是CPU在4个以上时,并发回收时垃圾回收线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是CPU不足4个时,CMS对用户程序的影响就可能变得很大。
2.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当此收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾被称为“浮动垃圾”。也是由于垃圾收集阶段用户线程还需要运行,那就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其它收集器一样等到老年代几乎完全被填满了再收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK1.6中,CMS收集器的启动阀值提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用SerialOld收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
3.基于“标记-清除”算法实现,会有大量的空间碎片产生。为了解决这个问题,CMS收集器提供了一个–XX:+UseCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程时无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

G1收集器

参考:Getting Started with the G1 Garbage Collector
之前介绍的几种GC(Serial/Parallel/CMS),都是将虚拟机内存划分为年轻代、年老代、永久代三个固定大小的区域;如下图:Hopspot Heap Structure
所有的内存对象都在这三个区域中创建销毁,而G1收集器的实现与之不同;如下图:G1 Heap Allocation
整个堆区划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器的运作大致可分为以下几个步骤:
1.初始标记(Initial Marking)
2.并发标记(Concurrent Marking)
3.最终标记(Final Marking)
4.筛选回收(Live Date Counting and Evacuation)
初始标记阶段仅仅是标记一下GCRoots能直接关联到的对象,这个阶段会发生stop the world,但耗时很短。并发标记阶段是从GCRoots开始对堆中的对象进行可达性分析,找出存活的对象,这个阶段可与用户线程并发执行,耗时较长。最终标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记纪录。筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来定制回收计划(这个阶段也可以做到与用户程序一起并发执行,但因只回收一部分Region,时间是用户可控制的,而且停顿用户线程可大幅度提高回收效率,固采用stop the world方式),然后将选定的需要回收的一个或多个Region中存活的对象复制到一个单独的Region,这个过程清理、压缩了内存空间。

官方推荐使用G1收集器代替其它收集器的场景:
The first focus of G1 is to provide a solution for users running applications that require large heaps with limited GC latency. This means heap sizes of around 6GB or larger, and stable and predictable pause time below 0.5 seconds.
Applications running today with either the CMS or the ParallelOldGC garbage collector would benefit switching to G1 if the application has one or more of the following traits.
Full GC durations are too long or too frequent.
The rate of object allocation rate or promotion varies significantly.
Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second).
需要将堆内存设置为6GB左右或更大时,G1收集器相较于其他收集器更加稳定、高效,且用户线程停顿时间可预测低于0.5秒。

G1收集器特点:
1.并行与并发
2.分代收集
3.空间整合(不会出现内存碎片,较于CMS)
4.可预测的停顿时间(因只是对堆内存中的某些Region进行整理,较于SerialOld)

参考:
<<深入了解Java虚拟机>>
Getting Started with the G1 Garbage Collector


  1. HotSpot虚拟机直接将本地方法栈与虚拟机栈合二为一。 ↩
  2. 固定长度的虚拟机栈中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFloorError异常。 ↩
  3. 如果虚拟机栈可以动态扩容,当扩容时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 ↩
  4. 类的动态加载可能导致OutMemory异常,如使用CGLib等字节码增加技术、反射技术、动态代理、自定义ClassLoader等场景。 ↩
  5. 每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。局部变量表存放了 编译器可预知的各种基本数据类型(boolean byte char short int float long double)、对象的引用。 ↩
  6. 运行时常量池时是方法区的一部分,存放编译期生成的各种字面量和符号引用。 ↩
0 0
原创粉丝点击