乐趣区

关于前端:图文并茂JVM内存布局详解

前言

本 JVM 系列属于自己学习过程当中总结的一些知识点,目标是想让读者更快地把握 JVM 相干的常识要点,难免会有所偏重,若想要更加零碎更加具体的学习 JVM 常识,还是须要去浏览业余的书籍和文档。

本文主题内容:

  • JVM 内存区域概览
  • 堆区的空间调配是怎么样?堆溢出的演示
  • 创立一个新对象内存是怎么调配的?
  • 办法区 到 Metaspace 元空间
  • 栈帧是什么?栈帧里有什么?怎么了解?
  • 本地办法栈
  • 程序计数器
  • Code Cache 是什么?

注:请 辨别 JVM 内存构造(内存布局)和 JMM(Java 内存模型)这两个不同的概念!

概览

内存是十分重要的系统资源,是硬盘和 CPU 的两头仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、调配、治理的策略,保障了 JVM 的高效稳固运行。

上图形容了以后比拟经典的 Java 内存布局。(堆区画小了 2333,按理来说应该是最大的区域)

如果依照线程是否共享来分类的话,如下图所示:

PS:线程是否共享这点,实际上了解了每块区域的理论用途之后,就很自然而然的就记住了。不须要死记硬背。

上面让咱们来理解下各个区域。

一、Heap (堆区)

1.1 堆区的介绍

咱们先来说堆。堆是 OOM 故障最次要的产生区域。它是内存区域中最大的一块区域,被所有 线程共享 ,存储着 简直所有 的实例对象、数组。 所有的对象实例以及数组都要在堆上调配 ,然而随着 JIT 编译器的倒退与 逃逸剖析技术 逐步成熟,栈上调配、标量替换优化技术将会导致一些奥妙的变动产生, 所有的对象都调配在堆上也慢慢变得不是那么“相对”了

延长知识点:JIT 编译优化中的一部分内容 – 逃逸剖析

举荐浏览:深刻了解 Java 中的逃逸剖析

Java 堆是垃圾收集器治理的次要区域,因而 很多时候也被称做“GC 堆”。从内存回收的角度来看,因为当初收集器根本都采纳分代收集算法,所以 Java 堆中还能够细分为:新生代和老年代。再粗疏一点的有 Eden 空间、From Survivor 空间、To Survivor 空间 等。从内存调配的角度来看,线程共享的 Java 堆中可能划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与寄存内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目标是为了更好地回收内存,或者更快地分配内存。

1.2 堆区的调整

依据 Java 虚拟机标准的规定,Java 堆能够处于物理上不间断的内存空间中,只有逻辑上是间断的即可,就像咱们的磁盘空间一样。在实现时,既能够实现成固定大小的,也能够在运行时动静地调整。

如何调整呢?

通过设置如下参数,能够设定堆区的初始值和最大值,比方 -Xms256M -Xmx 1024M,其中 -X 这个字母代表它是 JVM 运行时参数,msmemory start 的简称,中文意思就是内存初始值,mxmemory max 的简称,意思就是最大内存。

值得注意的是,在通常状况下,服务器在运行过程中,堆空间一直地扩容与回缩,会造成不必要的零碎压力 所以在线上生产环境中 JVM 的 Xms 和 Xmx 会设置成同样大小,防止在 GC 后调整堆大小时带来的额定压力。

1.3 堆的默认空间调配

另外,再强调一下堆空间内存调配的大体状况。

这里可能就会有人来问了,你从哪里晓得的呢?如果我想配置这个比例,要怎么批改呢?

我先来通知你怎么看虚拟机的默认配置。命令行上执行如下命令,就能够查看以后 JDK 版本所有默认的 JVM 参数。

java -XX:+PrintFlagsFinal -version

输入

对应的输入应该有几百行,咱们这里去看和堆内存调配相干的两个参数

>java -XX:+PrintFlagsFinal -version
[Global flags]
  ...
    uintx InitialSurvivorRatio = 8
    uintx NewRatio = 2
    ...
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

参数解释

参数 作用
-XX:InitialSurvivorRatio 新生代 Eden/Survivor 空间的初始比例
-XX:NewRatio Old 区 /Young 区的内存比例

因为新生代是由 Eden + S0 + S1 组成的,所以依照上述默认比例,如果 eden 区内存大小是 40M,那么两个 survivor 区就是 5M,整个 young 区就是 50M,而后能够算出 Old 区内存大小是 100M,堆区总大小就是 150M。

1.4 堆溢出 演示

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author Richard_Yi
 */
public class  HeapOOMTest  {

    public static final int _1MB = 1024 * 1024;

    public  static  void  main(String[] args) {List<byte[]> byteList = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {byte[] bytes = new byte[2 * _1MB];
            byteList.add(bytes);
        }
    }
}

输入

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32372.hprof ...
Heap dump file created [7774077 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at jvm.HeapOOMTest.main(HeapOOMTest.java:18)

-XX:+HeapDumpOnOutOfMemoryError 能够让 JVM 在遇到 OOM 异样时,输入堆内信息,特地是对相隔数月才呈现的 OOM 异样尤为重要。

创立一个新对象 内存调配流程

看完上面对堆的介绍,咱们趁热打铁再学习一下 JVM 创立一个新对象的内存调配流程。

绝大部分对象在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC。垃圾回收的时候,在 Eden 区实现革除策略,没有被援用的对象则间接回收。仍然存活的对象会被移送到 Survivor 区。Survivor 辨别为 so 和 s1 两块内存空间。每次 YGC 的时候,它们将存活的对象复制到未应用的那块空间,而后将以后正在应用的空间齐全革除,替换两块空间的应用状态。如果 YGC 要移送的对象大于 Survivor 区容量的下限,则间接移交给老年代。

一个对象也不可能永远呆在新生代,就像人到了 18 岁就会成年一样,在 JVM 中-XX:MaxTenuringThreshold 参数就是来配置一个对象从新生代降职到老年代的阈值。默认值是 15,能够在 Survivor 区替换 14 次之后,降职至老年代。

上述波及到一部分垃圾回收的名词,不相熟的读者能够查阅材料或者看下本系列的垃圾回收章节。

二、Metaspace 元空间

在 HotSpot JVM 中,永恒代(≈ 办法区)中用于寄存类和办法的元数据以及常量池,比方 Class 和 Method。每当一个类首次被加载的时候,它的元数据都会放到永恒代中。

永恒代是有大小限度的,因而如果加载的类太多,很有可能导致永恒代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen,为此咱们不得不对虚拟机做调优。

那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?(详见:JEP 122: Remove the Permanent Generation):

  1. 因为 PermGen 内存常常会溢出,引发宜人的 java.lang.OutOfMemoryError: PermGen,因而 JVM 的开发者心愿这一块内存能够更灵便地被治理,不要再经常出现这样的 OOM
  2. 移除 PermGen 能够促成 HotSpot JVM 与 JRockit VM 的交融,因为 JRockit 没有永恒代。

依据下面的各种起因,PermGen 最终被移除,办法区移至 Metaspace,字符串常量池移至堆区。

精确来说,Perm 区中的字符串常量池被移到了堆内存中是在 Java7 之后,Java 8 时,PermGen 被元空间代替,其余内容比方类元信息、字段、动态属性、办法、常量等都挪动到元空间区。比方 java/lang/Object 类元信息、动态属性 System.out、整形常量 100000 等。

元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存。因而,默认状况下,元空间的大小仅受本地内存限度。(和前面提到的间接内存一样,都是应用本地内存)

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace .

对应的 JVM 调参:

参数 作用
-XX:MetaspaceSize 调配给 Metaspace(以字节计)的初始大小
-XX:MaxMetaspaceSize 调配给 Metaspace 的最大值,超过此值就会触发 Full GC,此值默认没有限度,但应取决于零碎内存的大小。JVM 会动静地扭转此值。
-XX:MinMetaspaceFreeRatio 在 GC 之后,最小的 Metaspace 残余空间容量的百分比,缩小为调配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio 在 GC 之后,最大的 Metaspace 残余空间容量的百分比,缩小为开释空间所导致的垃圾收集

延长浏览:对于 Metaspace 比拟好的两篇文章。

Metaspace in Java 8

lovestblog.cn/blog/2016/1…

三、Java 虚拟机栈

对于每一个线程,JVM 都会在线程被创立的时候,创立一个独自的栈。也就是说虚拟机栈的生命周期和线程是统一,并且是线程公有的。除了 Native 办法以外,Java 办法都是通过 Java 虚拟机栈来实现调用和执行过程的(须要程序技术器、堆、元空间内数据的配合)。所以 Java 虚拟机栈是虚拟机执行引擎的外围之一。而 Java 虚拟机栈中出栈入栈的元素就称为「栈帧」。

栈帧 (Stack Frame) 是用于反对虚拟机进行办法调用和办法执行的数据结构。栈帧存储了办法的局部变量表、操作数栈、动静连贯和办法返回地址等信息。每一个办法从调用至执行实现的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

栈对应线程,栈帧对应办法

在流动线程中,只有位于栈顶的帧才是无效的,称为 以后栈帧 。正在执行的办法称为 以后办法 。在执行引擎运行时,所有指令都只能针对以后栈帧进行操作。而 StackOverflowError 示意申请的 栈溢出,导致内存耗尽,通常呈现在递归办法中。

虚拟机栈通过 pop 和 push 的形式,对每个办法对应的流动栈帧进行运算解决,办法失常执行完结,必定会跳转到另一个栈帧上。在执行的过程中,如果呈现了异样,会进行异样回溯,返回地址通过异样处理表确定。

能够看出栈帧在整个 JVM 体系中的位置颇高。上面也具体介绍一下栈帧中的存储信息。

1. 局部变量表

局部变量表就是 寄存办法参数和办法外部定义的局部变量的区域

局部变量表所需的内存空间在编译期间实现调配,当进入一个办法时,这个办法须要在帧中调配多大的局部变量空间是齐全确定的,在办法运行期间不会扭转局部变量表的大小。

这里间接上代码,更好了解。

public int test(int a, int b) {Object obj = new Object();
    return a + b;
}

如果局部变量是 Java 的 8 种根本根本数据类型,则存在局部变量表中,如果是援用类型。如 new 进去的 String,局部变量表中存的是援用,而实例在堆中。

2. 操作栈

操作数栈(Operand Stack)看名字能够晓得是一个栈构造。Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。当 JVM 为办法创立栈帧的时候,在 栈帧 中为办法创立一个 操作数栈,保障办法内指令能够实现工作。

还是用实操了解一下。

/**
 * @author Richard_yyf
 */
public class  OperandStackTest  {public  int  sum(int a, int b) {return a + b;}
}

编译生成.class 文件之后,再反汇编查看汇编指令

> javac OperandStackTest.java
> javap -v OperandStackTest.class > 1.txt
public int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3 // 最大栈深度为 2 局部变量个数为 3
  0: iload_1 // 局部变量 1 压栈
  1: iload_2 // 局部变量 2 压栈
  2: iadd // 栈顶两个元素相加,计算结果压栈
  3: ireturn
 LineNumberTable:
 line 10: 0

3. 动静连贯

每个栈帧中蕴含一个在常量池中 对以后办法的援用 ,目标是 反对办法调用过程的动静连贯

4. 办法返回地址

办法执行时有两种退出状况:

  • 失常退出,即失常执行到任何办法的返回字节码指令,如 RETURNIRETURNARETURN
  • 异样退出

无论何种退出状况,都将返回至办法以后 调用的地位。办法退出的过程相当于弹出以后栈帧,退出可能有 三种形式:

  • 返回值压入下层调用栈帧
  • 异样信息抛给 可能解决 的栈帧
  • PC 计数器指向办法调用后的下一条指令

延长浏览:JVM 机器指令集图解

四、本地办法栈

本地办法栈(Native Method Stack)与虚拟机栈所施展的作用是十分类似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。

在虚拟机标准中对本地办法栈中办法应用的语言、应用形式与数据结构并没有强制规定,因而具体的虚拟机能够自在实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)间接就把本地办法栈和虚拟机栈合二为一。与虚拟机栈一样,本地办法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异样。

五、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间。是线程公有的。它能够看作是以后线程所执行的字节码的行号指示器。什么意思呢?

文言版本:因为代码是在线程中运行的,线程有可能被挂起。即 CPU 一会执行线程 A,线程 A 还没有执行完被挂起了,接着执行线程 B,最初又来执行线程 A 了,CPU 得晓得执行线程 A 的哪一部分指令,线程计数器会通知 CPU。

因为 Java 虚拟机的多线程是通过 线程轮流切换并调配处理器执行工夫的形式来实现 的,CPU 只有把数据装载到寄存器才可能运行。寄存器存储指令相干的现场信息,因为 CPU 工夫片轮限度,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

因而,为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。每个线程在创立后,都会产生本人的程序计数器和栈帧,程序计数器用来寄存执行指令的偏移量和行号指示器等,线程执行或复原都要依赖程序计数器。此区域也不会产生内存溢出异样。

六、间接内存

间接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机标准中定义的内存区域。然而这部分内存也被频繁地应用,而且也可能导致 OutOfMemoryError 异样呈现,所以咱们放到这里一起解说。

在 JDK 1.4 中新退出了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I / O 形式,它能够 应用 Native 函数库间接调配堆外内存 ,而后通过一个 存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的援用进行操作。这样能在一些场景中显著进步性能,因为 防止了在 Java 堆和 Native 堆中来回复制数据

显然,本机间接内存的调配不会受到 Java 堆大小的限度,然而,既然是内存,必定还是会受到本机总内存(包含 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限度。如果内存区域总和大于物理内存的限度,也会呈现 OOM。

Code Cache

简而言之,JVM 代码缓存是 JVM 将其字节码存储为本机代码的区域。咱们将可执行本机代码的每个块称为 nmethod。该  nmethod 可能是一个残缺的或内联 Java 办法。

实时(JIT)编译器是代码缓存区域的最大消费者。这就是为什么一些开发人员将此内存称为 JIT 代码缓存的起因。

这部分代码所占用的内存空间成为 CodeCache 区域。个别状况下咱们是不会关怀这部分区域的且大部分开发人员对这块区域也不相熟。如果这块区域 OOM 了,在日志外面就会看到 java.lang.OutOfMemoryError code cache。

延长浏览 Introduction to JVM Code Cache

1、《深刻了解 Java 虚拟机》– 周志明

2、《码出高效》

3、Metaspace in Java 8

4、JVM 机器指令集图解

5、Introduction to JVM Code Cache

退出移动版