为什么要懂

对Java程序员来说,有虚拟机的自动内存管理机制,不容易出现内存泄漏和内存溢出问题,一旦出现这种问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将异常艰难。

版本:JDK1.6

一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),每个java程序都需要运行在自己的JVM上,然后告知 JVM 程序的运行入口,再被 JVM 通过字节码解释器加载运行。

Markdown

概括地说来,JVM初始运行的时候都会分配好Method Area(方法区)Heap(堆),而JVM 每遇到一个线程,就为其分配一个Program Counter Register(程序计数器), VM Stack(虚拟机栈)和Native Method Stack (本地方法栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)的原因。

Markdown

概念

  • Java虚拟机所管理的内存
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • Java堆
    • 方法区
      • 运行时常量池
  • 直接内存(堆外内存)

Markdown

运行时数据

程序计数器 Program Counter Register

  • 字节码的行号指示器
  • “线程私有”的内存
  • 没有规定任何OutOfMemoryError情况的区域

当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就时通过改变这个计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成)。

Java虚拟机的多线程是通过线程轮流切换并分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,线程独立存储,互不影响。

  • 计数器的内容
    • 线程执行Java方法
      • 正在执行的虚拟机字节码指令的地址
    • 线程执行Native方法
      • Undefined

Markdown

Java虚拟机栈 Java Virtual Machine Stacks

  • 线程私有
  • 为虚拟机执行Java方法服务

Java方法执行的内存模型


Java指令的构成:

  • 操作码(方法本身),保存在Stack中
  • 操作数(方法内部变量),简单类型保存在Stack中,对象类型在Stack保存地址,在Heap保存值

栈帧(Stack Frame):每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈(Operand Stack,记录出栈、入栈的操作)、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

Markdown

局部变量表:存放编译器可知的各种基本数据类型、对象引用和returnAddress类型。所需内存空间在编译期间完成分配,运行时不会改变。

  • 各种基本数据类型
    • boolean
    • byte
    • char
    • short
    • int
    • float
    • long
    • double

其中,64位长度的long和double会占用2个局部变量空间(Slot),其余的只占用1个。

  • 对象引用
    • reference类型
    • 不等同对象本身
    • 可能是一个指向对象起始地址的引用指针
    • 也可能是指向一个代表对象的句柄或其它与此对象相关的位置
  • returnAddress
    • 指向一条字节码执行的地址
  • Java虚拟机规范中,对此区域规定了两种异常情况
    • 如果线程请求的栈深度大于虚拟机所允许的深度
      • StackOverflowError异常
    • 如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存
      • OutOfMemoryError异常

参考资料

本地方法栈 Native Method Stack

  • 与虚拟机栈类似
  • 为虚拟机执行native方法服务
  • 虚拟机规范中没有强制规定使用的语言、使用方式与数据结构
  • 异常
    • StackOverflowError异常
    • OutOfMemoryError异常

Java堆 Heap

  • 线程共享
  • 虚拟机启动时创建
  • 存放对象实例
    • 包括对象的属性值、属性类型、对象本身的类型标记等,对象的方法以栈帧的形式保存在Stack中
    • 对象在Heap分配好后,需要在Stack中保存一个4字节的Heap内存地址,用来定位该对象实例在Heap中的位置
  • 也称GC堆,因为是垃圾收集器管理的主要区域
  • 可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
  • 大小通过-Xmx-Xms控制
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。

内存回收角度来看:由于现在收集器基本都采用分代收集算法,可分为新生代老年代。细致划分为:Eden空间From Survivor空间To Survivro空间等。

内存分配角度来看:可能划分出过个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),进一步划分是为了更好地回收内存,或者更快的分配内存。

Markdown

方法区 Method Area

  • 线程共享
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • Java虚拟机规范中描述为堆的一个逻辑部分,别名为Non-Heap(非堆)
  • 不需要连续的内存
  • 可以选择固定大小或者可扩展
  • 可以选择不实现垃圾收集
  • 内存回收目标主要是针对常量池的回收和对类型的卸载
  • 当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常

运行时常量池 Runtime Constant Pool

  • 方法区的一部分
  • 存放编译器生成的各种字面量和符号引用,将在类加载后进入方法区的运行时常量池中存放
  • 一般来说,还会存放翻译出来的直接引用
  • 具备动态性(和Class文件常量池比)
    • 编译期和运行期(String.intern()方法)均可放入常量
  • JVM规范没有细节要求
  • 无法再申请到内存时会抛出OutOfMemoryError异常

Class文件中的内容:

  • 类的版本
  • 字段
  • 方法
  • 接口
  • 常量池Constant Pool Table

参考资料

直接内存Direct Memory

  • JVM管理的内存区域之外的
  • 不受Java堆大小的限制
  • 本机总内存大小(RAM以及SWAP区或者分页文件)及处理器寻址空间的限制
  • 在设置-Xmx参数时要注意直接内存
    • 各个内存区域总和<物理内存限制(物理的和操作系统及的限制)
    • 大于会导致动态扩展时可能会出现OutOfMemoryError异常

JDK1.4新增NIO,引入了基于(Channel)与缓冲区(Buffer)的IO方式,NIO使用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBufffer对象作为这块内存的引用进行操作。

一些场景中显著提高性能

避免了在Java堆和Native堆中来回复制数据

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JVMShowcase {  
//静态类常量
public final static String ClASS_CONST = "I'm a Const";
//私有实例变量
private int instanceVar=15;
public static void main(String[] args) {
//调用静态方法
runStaticMethod();
//调用非静态方法
JVMShowcase showcase=new JVMShowcase();
showcase.runNonStaticMethod(100);
}
//常规静态方法
public static String runStaticMethod(){
return ClASS_CONST;
}
//非静态方法
public int runNonStaticMethod(int parameter){
int methodVar=this.instanceVar * parameter;
return methodVar;
}
}

JVM执行步骤

  • JVM向操作系统申请指定空闲内存
    • 操作系统分配出指定内存,返回给JVM内存段的起始地址、终止地址
    • JVM准备加载类文件
  • 分配Java内存
    • 分配Heap内存
    • 。。。
  • 文件检查、分析class文件
    • 有错误立即返回
  • 加载类
    • JVM默认使用bootstrap加载器,加载rt.jar包下所有类到堆内存的Method Area
    • 加载用户类(如上文中的JVMShow类),方法区中:
      • main方法的符号引用
      • runStaticMethod方法的符号引用
    • 此时,Heap是空的,Stack是空的
      • 没有新建对象和执行线程
  • 执行方法
    • 启动新线程,执行main方法
    • 方法区中:
      • main方法的符号引用
      • runStaticMethod方法的符号引用
      • CLASS_CONST(runStaticMethod方法内部第一次被访问时产生)
    • 堆中(JVMShowcase showcase=new JVMShowcase(); 被执行):
      • 两个object???
        • JVMShowcase的父类
      • showcase对象
    • 栈中有三个栈帧:
      • runNonStaticMethod
      • runStaticMethod
      • main
    • 程序计数器
      • 指向下一条要执行的语句
  • 释放内存
    • 程序运行结束后,向操作系统释放内存。

版本对比

  • Before JDK1.8

before 1.8

  • After JDK1.8

after 1.8

  • 在JDK1.7及之后,字符串常量池如果指的是SymbolTableStringTable,这俩table本身都在native memory中,但它们的引用所存储的位置却发生了变化。
    • SymbolTable所引用的符号引用Symbols -> native heap
    • StringTable所引用的interned Strings -> Java heap;
  • 类的静态变量class static variables -> Java heap
  • class metadata 转移到了 native memory(本地内存,而不是虚拟机);
  • 移除了永久代(PermGen),替换为元空间(Metaspace);
  • 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

参考资料

Java (JVM) Memory Model

JVM Memory Model / Structure and Components

Java Memory Model

深入理解JVM之JVM内存区域与内存分配