跳至主要內容

JVM内存区域

pptg大约 5 分钟

1. Java运行时数据区域

JVM在Java运行过程中会把内存划分成若干个不同的数据区域,JDK1.7和JDK1.8在内存划分上略有不同,具体如下:

image_descriptionimage_description

根据线程私有还是共享,分为:

  • 私有:
    • 虚拟机栈
    • 本地方法栈
    • 程序计数器(PC计数器)
  • 共享:
    • 方法区(JDK1.8后移动至本地内存中,名为元空间)
    • 直接内存(非运行时数据区,位于本地内存)

2. 线程私有部分

线程私有部分是为了防止多线程切换后互相影响产生影响而设计的

2.1 虚拟机栈

Java中除了Native方法以外,其他的方法都需要通过虚拟机栈来实现

栈中的每一个栈帧都存储了当前程序运行的一些信息,包括:

  • 局部变量表:存放编译期可知的各种数据类型(如int)和对象引用
  • 操作数栈:存放执行过程中的中间结果临时变量
  • 动态链接:编译class时,所有的变量和方法的引用都是保存在常量池中的符号引用。一部分会在类加载的解析阶段或第一次使用时变成直接引用,另一部分则会在每次运行时经过动态链接转换成直接引用。(全限定名 -> 内存指针)
  • 方法返回地址:方法正常退出或异常退出的地址
虚拟机栈帧
虚拟机栈帧

2.2 本地方法栈

本地方法栈和虚拟机栈类似,只不过时为本地Native方法服务

2.3 程序计数器

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

3. 线程共享部分

3.1 堆

3.1.1 堆

堆是JVM内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存(对象没有被返回或引用,则可以分配在栈上,称之为逃逸分析)。因为堆是垃圾回收的主要区域,所以又以分代垃圾回收算法分为以下三个部分:

  • 新生代(Young Generation):Eden、S0、S1
  • 老生代(Old Generation):Tenured
  • 永久代(Permanent Generation):1.8之后变成元空间,位置在直接内存
image_descriptionimage_description

通常,对象首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁, 因为年龄记录在对象头中,默认大小是4位),就会被晋升到老年代中

3.1.2 字符串常量池

字符串常量池避免了字符串的重复创建: JDK1.7之前,字符串常量池存在永久代。JDK1.7中,因为永久代的GC效率太低(只有FullGC才回收),字符串常量池存在堆中。

3.2 方法区

3.2.1 方法区

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类、字段、方法、常量、静态变量、代码缓存等数据

方法区、永久代和元空间的关系:方法区类似于接口和规范,永久代(1.7)和元空间(1.8)是方法区的不同实现

为什么用元空间替代永久代?

  • 永久代由JVM内存限制,而元空间没有。方法区存放了类的信息,所以元空间可以加载更多的类。
  • 永久代的GC效率太低(只有FullGC才回收)

3.2.2 运行时常量池

存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的常量池表(Constant Pool Table)

3.3 直接内存

直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配

4. HotSpot虚拟机的对象创建过程

  1. 类加载检查:JVM遇到new指令时,首先检查其参数是否能在常量池中定位到符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 分配内存:将内存从JVM堆中分配出来,存在两种内存分配方式
    • 内存分配方式
      • 指针碰撞:堆内存规整,即用过的内存和没用过的中间存在指针,分配至该指针处即可
      • 空闲列表:堆内存不规整,维护内存可用列表,找到容量足够的区域进行分配
    • 并发分配问题
      • CAS重试:冲突即重试
      • TLAB:提前为每个线程分配一块内存,如果用完了或不够用再继续分配
  3. 初始化零值:将分配到的内存空间都初始化为零值,以便不初始化就可以使用
  4. 设置对象头:设置对象的哈希码、GC年龄等信息
  5. 执行init:按照程序初始化

5. JVM虚拟机中常见的异常

  • 虚拟机栈 & 本地方法栈
    • 栈深度超限:StackOverFlowError
    • 内存不足:OutOfMemoryError
  • 程序计数器:唯一不会出现OutOfMemoryError的区域,生命周期和线程的创建、结束一样
  • 堆 & 方法区:内存不足OutOfMemoryError