JVM堆空间

概述

  • 一个 JVM实例中只存在一个堆空间, 以及在运行时数据区中空间最大的区域, 同时 GC(Garbage Collection, 垃圾收集器)最频繁的区域
  • 所有的线程间共享一个堆空间, 也就是线程共享的区域. 不过也不完全是线程共享的, 堆还给每个线程分配一种私有的空间, 叫做 TLAB(Thread Local Allocation Buffer)缓冲区
  • 堆主要负责数据存储 如对象实例以及数组, 当实例一个对象时, 一般会将对象的实体分配到堆中, 而对象的引用存放在虚拟机栈的变量表内. 不过随着编译技术的日益成熟, 编译期可以通过逃逸分析技术, 将未发生逃逸的对象实体存到栈上来减少堆的 GC, 以此提高性能
  • JDK7开始, 将字符串常量池(StringTable)和静态变量, 从方法区转移到了堆空间中. 不过在 JDK7以前版本中静态变量如果是通过 new关键字实例化的也会将对象实体存放到堆空间中的
  • 有 GC(垃圾回收)和 OOM(OutOfMemoryError, 内存溢出)异常

内存结构(分代收集算法的落地实现)

分代收集算法(Generational Collecting)是按照对象的生命周期长短划分的多个 Region, 由此可以根据各个区的特点, 使用不同的回收算法, 提高回收效率

堆内部结构

  • 堆分为两部分:新生代(Young Generation Space)和老年代(Tenure Generation Space)
  • 新生代又分为伊甸区(Eden)和幸存者区(Survivor)
  • 幸存者区又分为 S0(Survivor0, 幸存者0)和 S1(Survivor1, 幸存者1), 两个空间大小相同. 幸存者区是复制算法(Copying)的落地实现
    在这里插入图片描述

JVM内存逻辑上分为三部分

  • Java7时堆空间(新生代 + 老年代) + 方法区(永久代/Permanent Space)
  • Java8时堆空间(新生代 + 老年代) + 方法区(元空间/Meta Space)

GC(JVM的垃圾回收)

  • 什么是垃圾? 垃圾指的是不再被其它对象所引用的对象

GC按回收区域分为两大类

  1. 部分收集(Partial GC)
  • 新生代收集(Minor GC/Young GC/年轻代), 只收集新生代
  • 老年代收集(Major GC/Old GC/养老代), 只收集老年代. 目前只有 CMS GC有单独收集老年代的行为
    * 注: 很多时候 Major GC会和 Full GC混淆使用
  • 混合收集(Mixed GC)收集整个新生代以及部分老年代的垃圾收集. 目前只有 G1 GC有这种行为
  1. 整堆收集(Full GC)

GC触发机制

  • 当伊甸区空间不足时, 就会触发 Minor GC
  • 当老年代空间不足时, 就会触发 Major GC或 Full GC
  • 方法区空间不足时, 就会触发 Full GC
  • 不过幸存者区空间不足时, 不会触发 Minor GC时, 此区的垃圾收集永远是被动的, 当伊甸区的 Minor GC或整堆的 Full GC时, 则同时处理幸存者区
    * 注: 调用的方法执行结束后, 堆中的对象不会马上被移除(出栈后不会马上回收), 只会在 GC时才会被销毁

GC过程

  1. 绝大部分的 Java对象会在伊甸区创建& 销毁, 在伊甸区对象经过一次 Minor GC后, 认定为存活的对象, 将依次转移到幸存者 to区同时指定对象的年龄(对象头的 GC分代年龄计数器)设为1
  2. 当伊甸区 Minor GC时, 将认定为存活的对象从(伊甸区/Survivor from区)转移到 Survivor to区(from区和 to区的设定: 最初 S0是 to之后通过复制算法来回交替存, 哪个空就是 to区, 直到对象的年龄达到阈值(默认15), 最后还是存活的对象将转移到老年代)
  3. 当对象太大, 伊甸区空间不足时, 会直接将对象转移到老年代(一般会经历一次 Minor GC, 但如果比伊甸区自身还要大甚至不会经过伊甸区, 而直接在老年代创建对象)
  4. 当对象太大, 幸存者区空间不足时, 会直接将对象转移到老年代
  5. 当对象太大, 老年代空间不足时, 会触发 Major GC/Full GC, 如果还是不足就会 OOM(OOM异常肯定至少经历过一次 Full GC)
    * 注: 当 GC时会产生 STW(Stop The world), 就是用户的线程的停顿, 由此导致程序的吞吐量降低. 调优时需要尽可能减少 GC其中主要是 Major GC和 Full GC的执行频率, 因为这两个回收的执行时间比 Minor GC慢10倍以上, 意思为 STW的时间更长

TLAB(Thread Local Allocation Buffer)缓冲区

  • 由于堆区是线程共享的, 因此会有多个线程操作同一个地址, 需要使用加锁机制来实现线程安全, 进而影响内存分配速度. 使用 TLAB可以避免一系列的非线程安全问题, 同时还能够提升内存分配的吞吐量, 因此我们可以将这种内存分配方式称之为快速分配策略
  • TLAB包含在 Eden区中, TLAB空间在 Eden区中的默认占比只有1%, 所以并不是所有的对象实例都能够在 TLAB中成功分配到内存, 一旦在 TLAB空间中分配内存失败时, JVM就会尝试通过使用加锁机制确保数据操作的原子性, 从而直接在 Eden区中分配内存
    在这里插入图片描述

逃逸分析(Escape Analysis), 堆外存储技术

  • 在 JIT(Just In Time Compiler, 即时编译器)编译时, 根据逃逸分析发现某个对象的方法未发生逃逸(所有的成员变量只在方法内部使用)时:
  1. 将方法内生成的很多局部对象实例不在堆中生成, 而直接在栈上分配, 以此减少堆区的 GC频率
  2. 如果发现一个方法只会通过单一线程访问, 不存在资源的竞争, 则 JIT编译时会自动省略同步锁机制. 当然字节码成面上同步字节码指令还是存在的, 只不过在动态编译时, 经过逃逸分析后会将自动被省略掉
    * 通过此技术, 可以实现部分对象实例分配到栈上, 以此减少堆区的 GC频率

简单例子


# 发生逃逸
    public StringBuffer getName(String str) {
	StringBuffer sb = new StringBuffer();
        sb.append(str);
        return sb;
    }

    public String getName(StringBuffer sb) {
	sb.append("abc");
        return sb.toString();
    }

# 未发生逃逸
    public String getName(String str) {
	StringBuffer sb = new StringBuffer();
        sb.append(str);
        return sb.toString();
    }

标量替换

  1. 标量(Scalar)和聚合量(Aggregate)
  • 标量是指不可再进一步分解的量, 在 Java中基本数据类型就是标量(如 char,int,short,long等等以及 reference类型), 标量的对立就是可以进一步分解的量, 这种量称之为聚合量
  1. 替换过程
  • 通过逃逸分析确定该对象不会被外界访问, 且对象可以再进一步分解时, 就会将该对象成员变量分解成若干个成员局部变量来代替. 这些被代替的局部变量在栈帧或寄存器上分配

* 注: 缺点为无法保证逃逸分析后所消耗的性能一定低于不使用的时候

配置选项

  • -Xms256m设置初始堆大小(不包含永久代/元空间), 等价于 -XX:InitialHeapSize
  • -Xmx256m设置最大堆大小(不包含永久代/元空间), 等价于 -XX:MaxHeapSize
  • -Xmn设置新生代的大小, 等价于 -XX:NewSize(此参一般使用默认值, 因为与选项 -XX:NewRatio相互矛盾, 当然如果设置了优先级高于 -XX:NewRatio)
  • -XX:NewRatio=2设置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio=8设置新生代中的 Eden区和 S0/S1区的比例, 默认占比是8:1:1, 不过实际内部分配是6(由于有自适应机制 -XX:-UseAdaptiveSizePolicy, 不过此参即使关了也同样无法撤销), 所以如需8的占比, 则需要显式的指明8
  • -XX:MaxTenuringThreshold=15设置新生代垃圾的最大年龄
  • -XX:HandlePromotionFailure是否设置空间分配担保(JDK7开始此参已失效, 也就是固定死了开启分配担保)
  • -XX:+UseTLAB是否开启(默认开启)
  • -XX:TLABWasteTargetPercent=1设置 TLAB空间所占用 Eden空间的百分比大小(默认只有1%)
  • -XX:+PrintGCDetails输出详细的 GC处理日志
  • -XX:+PrintFlagsInitial查看所有参数的默认初始值
  • -XX:+PrintFlagsFinal查看所有参数的最终值(输出已改过的最终值)
  • -server启动 Server模式(64位 JVM默认开启)
  • -XX:+DoEscapeAnalysis显式开启逃逸分析(JDK7开始默认开启), JVM启动模式必须为 server, 才可以启用逃逸分析
  • -XX:+EliminateLocks开启同步消除
  • -XX:+EliminateAllocations开启标量替换(默认开启), 允许将对象打散分配在栈上
  • -XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
  • -XX:+PrintEliminateAllocations查看标量替换详情

* 更多调优相关参考: https://blog.csdn.net/qcl108/article/details/103476424

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页