jvm内存布局

概念java

内存是很是重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操做系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程当中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。算法

上图描述了当前比较经典的 JVM 内存布局。(堆区画小了 2333,按理来讲应该是最大的区域)数组

若是按照线程是否共享来分类的话,以下图所示:缓存

PS:线程是否共享这点,实际上理解了每块区域的实际用处以后,就很天然而然的就记住了。不须要死记硬背。服务器

下面让咱们来了解下各个区域。数据结构


Heap (堆区)多线程

1. 堆区的介绍 并发

咱们先来讲堆。堆是 OOM 故障最主要的发生区域。它是内存区域中最大的一块区域,被全部线程共享,存储着几乎全部的实例对象、数组。全部的对象实例以及数组都要在堆上分配,可是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会致使一些微妙的变化发生,全部的对象都分配在堆上也渐渐变得不是那么“绝对”了。jvm

延伸知识点:JIT 编译优化中的一部份内容 - 逃逸分析。

推荐阅读:深刻理解 Java 中的逃逸分析函数

https://www.hollischuang.com/...

Java 堆是垃圾收集器管理的主要区域,所以不少时候也被称作“GC 堆”。从内存回收的角度来看,因为如今收集器基本都采用分代收集算法,因此 Java 堆中还能够细分为:新生代和老年代。再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过不管如何划分,都与存放内容无关,不管哪一个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

2. 堆区的调整

根据 Java 虚拟机规范的规定,Java 堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的便可,就像咱们的磁盘空间同样。在实现时,既能够实现成固定大小的,也能够在运行时动态地调整。

如何调整呢?

经过设置以下参数,能够设定堆区的初始值和最大值,好比 -Xms256M -Xmx 1024M,其中 -X这个字母表明它是 JVM 运行时参数,ms是memory start的简称,中文意思就是内存初始值,mx是memory max的简称,意思就是最大内存。

值得注意的是,在一般状况下,服务器在运行过程当中,堆空间不断地扩容与回缩,会造成没必要要的系统压力因此在线上生产环境中 JVM 的Xms和Xmx会设置成一样大小,避免在 GC 后调整堆大小时带来的额外压力。

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)`

参数解释

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

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 调参:

延伸阅读:关于 Metaspace 比较好的两篇文章

Metaspace in Java 8

http://lovestblog.cn/blog/201...


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. 方法返回地址

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

  • 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN等
  • 异常退出

不管何种退出状况,都将返回至方法当前被调用的位置。方法退出的过程至关于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给可以处理的栈帧
  • PC 计数器指向方法调用后的下一条指令
    • *

本地方法栈

本地方法栈(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。

诊断选项

相关文章
相关标签/搜索