一、JVM整体架构

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

java 虚拟机内存区 java 虚拟机 内存_方法区

名称

特性

作用

配置参数

异常

程序计数器

占用内存小,线程私有,生命周期与线程相同

大致为字节码行号指示器



虚拟机栈

线程私有,生命周期与线程相同,使用连续的内存空间

Java 方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息

-Xss

StackOverflowError/OutOfMemoryError


线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址

保存对象实例,所有对象实例(包括数组)都要在堆上分配

-Xms -Xsx -Xmn

OutOfMemoryError

方法区

线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

-XX:PermSize:16M- XX:MaxPermSize64M/-XX:MetaspaceSize=16M- XX:MaxMetaspaceSize=64M

OutOfMemoryError

本地方法栈

线程私有

为虚拟机使用到的Native 方法服务


StackOverflowError/ OutOfMemoryError

JVM分为五大模块: 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块

java 虚拟机内存区 java 虚拟机 内存_Java_02

二、JVM运行时内存

Java虚拟机有自动内存管理机制,如果出现面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。

java 虚拟机内存区 java 虚拟机 内存_方法区_03

Java7和Java8内存结构的不同主要体现在方法区的实现

方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
我们通常使用的JavaSE都是由SunJDK和OpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM就是HotSpotVM。通常情况下,我们所讲的java虚拟机指的就是HotSpot的版本。

JDK7内存结构:

java 虚拟机内存区 java 虚拟机 内存_本地方法_04


JDK8内存结构:

java 虚拟机内存区 java 虚拟机 内存_本地方法_05


针对JDK8虚拟机内存详解:

java 虚拟机内存区 java 虚拟机 内存_java_06


JDK7和JDK8区别:

java 虚拟机内存区 java 虚拟机 内存_方法区_07


线程私有的:

1、程序计数器

2、虚拟机栈

3、本地方法栈

线程共享的:

1、堆

2、方法区直接内存(非运行时数据区的一部分)

对于Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?

当然不是,方法区只是一个规范,只不过它的实现变了。在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。

方法区Java8之后的变化
  • 移除了永久代(PermGen),替换为元空间(Metaspace)
  • 永久代中的class metadata(类元信息)转移到了native memory(本地内存,而不是虚拟机)
  • 永久代中的interned Strings(字符串常量池) 和 class static variables(类静态变量)转移到了Java heap
  • 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize)
Java8为什么要将永久代替换成Metaspace?
  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太
    大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一,JRockit没有所谓的永久代。
程序计数器

程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行
的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条
需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

PC寄存器的特点:

1、区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟

机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的

地址。

2、当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。

3、程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。

4、此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

java 虚拟机内存区 java 虚拟机 内存_本地方法_08


Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处

理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创

建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态

链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过

程。

java 虚拟机内存区 java 虚拟机 内存_Java_09


java 虚拟机内存区 java 虚拟机 内存_方法区_10

什么是栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数

栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入

栈到出栈的过程。

java 虚拟机内存区 java 虚拟机 内存_Java_11


java 虚拟机内存区 java 虚拟机 内存_java_12

如何设置虚拟机栈的大小

-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

  • Linux/x64 (64-bit): 1024 KB
  • macOS (64-bit): 1024 KB
  • Oracle Solaris/x64 (64-bit): 1024 KB
  • Windows: The default value depends on virtual memory
-Xss1m
-Xss1024k
-Xss1048576

我们通过一案例演示一下:

java 虚拟机内存区 java 虚拟机 内存_Java_13


执行结果:

java 虚拟机内存区 java 虚拟机 内存_本地方法_14


我们设置下 这个-Xss的大小,如下图:

java 虚拟机内存区 java 虚拟机 内存_本地方法_15


我们在执行一下:

java 虚拟机内存区 java 虚拟机 内存_Java_16

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基
本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

操作数栈

操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部

变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者

返回给方法调用者,也就是出栈/入栈操作。

通过以下代码演示操作站执行:

java 虚拟机内存区 java 虚拟机 内存_方法区_17

动态链接

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了
支持方法调用过程中的动态链接(Dynamic Linking)。

方法返回地址

方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异
常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者
的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过
异常表来确定,栈帧中一般不会保存这部分信息。无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
1、本地方法栈加载native的但是方法, native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
2、虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
3、是线程私有的,它的生命周期与线程相同,每个线程都有一个。
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:
1、StackOverFlowError :线程请求的栈深度>所允许的深度。
2、OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。