Java将内存控制权利交给Java虚拟机,其在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
1. JDK1.7 VS JDK1.8
需要注意的是,相较于JDK1.8之前的版本,JDK1.8 中PermGen(永久代) 被 Metaspace(元空间) 取代,并且元空间使用的是本地内存。
2. 线程私有部分
作用 | 内存溢出 | |
虚拟机栈 | 调用方法创建栈帧,方法结束则弹出栈帧 | 不允许栈内存动态扩展,则可能出现OutOfMemoryError,若允许,则在动态扩展栈时无法申请到足够的内存空间时出现OutOfMemoryError |
本地方法栈 | 作用与虚拟机栈类似,为虚拟机使用的native方法服务 | 与虚拟机栈一样,可能出现 StackOverFlowError 和 OutOfMemoryError 两种错误 |
程序计数器 | 实现代码的流程控制,保证线程切换后能恢复到正确的执行位置 | 不存在 |
3. 栈帧
栈由一个个栈帧组成,而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法返回地址。
作用 | |
局部变量表 | 存放编译器可知的基本数据类型以及对象引用 |
操作数栈 | 存放中间计算结果以及计算过程中产生的临时变量 |
动态链接 | 当一个方法要调用其他方法,其将指向该方法的符号引用转化为内存地址的直接引用 |
方法返回地址 | 存放被调用方法的返回地址,该返回地址是一个具体数值 |
4. 线程共享部分
4.1 堆
堆是Java 虚拟机所管理的内存中最大的一块,也是垃圾收集器管理的主要区域,用于存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。为什么说是几乎呢?随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术导致并非所有的对象都分配到堆上。
JDK1.7堆空间 VS JDK1.8
4.2 方法区
方法区和永久代以及元空间是什么关系呢?
永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 永久代受 JVM 空间大小限制,无法进行调整,而元空间使用的是本地内存,内存溢出几率更小
- 能加载的类更多
- 在 JDK8合并 HotSpot 和 JRockit 的代码时, JRockit 没有永久代这一说法
5. 运行时常量池
- 存放编译期生成的各种字面量和符号引用的 常量池表(符号引用与直接应用一一对应)
- 常量池表在类加载后存放到方法区的运行时常量池中
6.字符串常量池
HotSpot 虚拟机中字符串常量池类似为一个固定大小的HashTable,
保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
注意:JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
永久代(方法区实现)的 GC 回收效率太低,Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存,避免内存溢出。
7.直接内存
直接内存并不是虚拟机运行时数据区的一部分,不会受到 Java 堆的限制,但会受到本机总内存大小以及处理器寻址空间的限制。
8.Java虚拟机对象创建过程
- 类加载检查,在运行时常量池中定位到这个类的符号引用,查这个符号引用代表的类是否已被加载过、解析和初始化过,决定是否执行类加载过程
- 为新生对象分配内存,分配方式有 “指针碰撞” 和 “空闲列表” 两种,虚拟机采用两种方式来保证内存分配并发线程安全:CAS+失败重试与TLAB
- 初始化零值,将分配到的内存空间都初始化为零值
-
设置对象头
-
执行 init 方法
9.对象的访问定位
Java 程序通过栈上的 reference 数据来操作堆上的具体对象,访问方式有以下两种:
- 句柄
-
直接指针
句柄 VS 直接指针
句柄方式reference中存放的是对象实例数据与对象类型数据的地质,而直接指针方式reference中存放的是对象地址
使用句柄来访问 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
10.补充
运行时常量池、方法区、字符串常量池是抽象概念,Metaspace、Heap 是与具体某种虚拟机实现相关的物理概念,是私有且具体的。