第六篇:JVM内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)

1、开篇

JVM总体分为堆区和非堆区,即heap区和非heap区。

heap区又分为:

  • Young Gen(新生代),新生代又分为下面两个区域
    • Eden Space(伊甸区)
    • Survivor Space(幸存者区),幸存者区又分为from区和to区
      • Survivor from (from区,也称为A区)
      • Survivor to (to区,也称为B区)
  • Old Gen(老年代)

非heap区又分为:

  • Code Cache(代码缓存区);
  • Perm Gen(永久代,jdk8已经移除了永久代,增加了Metaspace概念);
  • Jvm Stack(java虚拟机栈);
  • Local Method Statck(本地方法栈);

2、Young Gen (新生代)

2.1、新生代内存划分

年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。当对象在堆创建时,将进入年轻代的Eden Space。
新生代中98%的对象都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。

当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

当Survivor空间不够用时,则需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。

HotSpot实现的复制算法流程如下:

1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。

2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。

3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
在这里插入图片描述

2.2 标记清除算法

标记清除算法是最基础的回收算法,分为标记和清除两个部分:首先标记出所有需要回收的对象,这一过程在可达性分析过程中进行,在标记完之后统一回收所有被标记的对象。
标记清除算法有如下不足:

  • 效率问题
    标记和清除这两个过程的效率不高
  • 空间问题
    清除之后会产生大量不连续的内存碎片,内存碎片太多会导致以后的程序运行中无法分配出较大的连续的内存空间,从而不得不触发另外的垃圾回收。
    在这里插入图片描述
    如上图中,经过标记清除之后,假设有了100M空间,但是这100M是不连续的,最大的一块连续空间可能才10M,所以导致之后程序需要一块20M内存空间时就不得不再进行一次GC来继续清理空间,效率极低。
    鉴于标记清除算法有如上的缺陷,所以现在一般是用的是其的变种算法。

2.3 复制算法(新生代算法)

复制算法是针对Java堆中的新生代内存垃圾回收所使用的回收策略,解决了”标记-清理”的效率问题。

复制算法将堆中可用的新生代内存按容量划分成大小相等的两块内存区域,每次只使用其中的一块区域。当其中一块内存区域需要进行垃圾回收时,会将此区域内还存活着的对象复制到另一块上面,然后再把此内存区域一次性清理掉。

这样做的好处是每次都是对整个新生代一半的内存区域进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :
在这里插入图片描述
现在主流的虚拟机,包括HotSpot都是采用的这种回收策略进行新生代内存的回收。

发生在新生代的垃圾回收成为Minor GC,Minor GC又称为新生代GC,因为新生代对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。

2.4 为什么会有年轻代?

我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.5 有关年轻代的JVM常用参数

1)-XX:NewSize和-XX:MaxNewSize

  用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

  用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

  这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

  用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

3. 老年代(Tenured Gen)

年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

3.1 标记整理算法(老年代回收算法)

复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以在栈的老年代不适用复制算法。

针对老年代对象存活率高的特点,提出了一种称之为”标记-整理算法”。标记过程仍与”标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
在这里插入图片描述
发生在老年代的GC称为Full GC,又称为Major GC,其经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

4. 非堆区(非heap区)

Code Cache代码缓存区,它主要用于存放JIT所编译的代码。CodeCache代码缓冲区的大小在client模式下默认最大是32m,在server模式下默认是48m,这个值也是可以设置的,它所对应的JVM参数为ReservedCodeCacheSize 和 InitialCodeCacheSize,可以通过如下的方式来为Java程序设置。

-XX:ReservedCodeCacheSize=128m

CodeCache缓存区是可能被充满的,当CodeCache满时,后台会收到CodeCache is full的警告信息,如下所示:

“CompilerThread0” java.lang.OutOfMemoryError: requested 2854248 bytes
for Chunk::new. Out of swap space?

注:JIT编译器是在程序运行期间,将Java字节码编译成平台相关的二进制代码。正因为此编译行为发生在程序运行期间,所以该编译器被称为Just-In-Time编译器。

Perm Gen全称是Permanent Generation space,是指内存的永久保存区域,因而称之为永久代。这个内存区域用于存放Class和Meta的信息,Class在被 Load的时候被放入这个区域。因为Perm里存储的东西永远不会被JVM垃圾回收的,所以如果你的应用程序LOAD很多CLASS的话,就很可能出现PermGen space错误。默认大小为物理内存的1/64。
永久代,jdk8已经移除了永久代,增加了Metaspace概念,查看详情

发布了47 篇原创文章 · 获赞 0 · 访问量 1175

猜你喜欢

转载自blog.csdn.net/weixin_43103748/article/details/104049264
gen