- 内存简介
- JVM 架构图
- Java 内存模型
内存
计算机程序都是在内存中运行的(包括虚拟内存)
在程序运行过程中, 需要不断的将内存的「逻辑地址」和「物理地址」进行映射,找到相关的【指令】以及【数据】去执行
逻辑地址 -> 分段管理机制 -> 线性地址 -> 分页管理机制 -> 物理地址
Java 程序实际上是一个操作系统进程, 与其他进程一样面临内存限制,即受限于操作系统架构提供的「可寻址地址空间」 (可寻址地址空间由处理器的位数决定(32位/64位))
地址空间划分为:
- 内核空间(操作系统程序, c 运行时的空间 , 连接计算机硬件,调度程序,提供联网和虚拟内存等服务的逻辑和基于 C 的进程)
- 用户空间(java 进程实际运行时使用的内存空间)
JVM 架构图
ref: 《Java虚拟机学习笔记》
其中 Runtime Data Area 就是 Java 的内存模型
Java 内存模型
Java 程序运行在虚拟机之上,运行时需要内存空间,虚拟机在执行 Java 程序的过程中会把「他管理的内存」划分不同的数据区域.
「C 编译器」会将内存区域划分为:数据段(包括「堆」「栈」「静态数据区」)和代码段
Q: JDK 8 是如何划分内存空间的?
从线程的角度讨论
线程私有的区域
- 程序计数器
- 虚拟机栈(Stack)
- 本地方法栈(带有 Native 关键字的方法使用的内存空间)
Java 方法执行的内存模型, 每个方法被执行时都会创建一个栈帧(栈帧是方法运行期间的基础数据结构)存放在虚拟机栈中;
「栈帧」用于存储局部变量表,操作数栈, 动态连接,返回地址等
- 当方法被调用时,jvm 会开辟一块空间(会创建一个栈帧);
- 当方法调用结束时,栈帧就会被销毁;(这也是为什么栈的内存不需要通过 GC 回收, 而是自动释放)
-
局部变量表:包括了方法执行过程中的所有变量,this 引用,所有方法参数,其他局部变量
-
操作数栈: 在执行字节码指令时被用到,类似于原生 CPU 寄存器,大部分 JVM 字节码把时间花费在 操作数栈上,包括入栈,出栈,复制,交换,产生消费变量等
Q: 递归为什么会引起 java.lang.StackOverflow 异常?
package com.ercargo.restart;
/**
* @author ercargo on 2020/4/5
* @DESCRIBE
* 0,1,1,2,3,5,8,13...
* F(0) = 0
* F(1) = 1
* F(n) = F(n-1) + F(n-2)
* e.g:
* F(2) = F(1) + F(0) = 1
* F(3) = F(2) + F(1) = 2
*/
public class Fibonacci {
public static int fib(int n) {
if (n <= 1) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
public static void main(String[] args) {
System.out.println(fib(0));
System.out.println(fib(1));
System.out.println(fib(2));
System.out.println(fib(3));
System.out.println(fib(9));
System.out.println(fib(10000000));
}
}
每【执行】一次方法时, 就会创建一个栈帧, 并将栈帧压入虚拟机栈中,方法执行完成时, 会将栈帧出栈 递归过深,方法调用次数过多, 创建的栈帧数就越多, 当栈帧数会超出整个虚拟机栈的深度时, 就会报错:
Exception in thread "main" java.lang.StackOverflowError
解决方法: 限制递归深度/循环替代
另外:虚拟机栈过多也会引发 java.lang.OutOfMemoryError 异常
线程共享的区域
- 元空间 MetaSpace(类加载信息)
在 jdk8 以后开始把类的元数据放在本地堆内存中,称为 MetaSpace(在 jdk7 以前是属于永久代的),元空间和永久代都是用来存储 Class 信息,包括 Class 对象的 method ,field 等
元空间和永久代都是方法区(Method Area)的实现,方法区只是一种 JVM 的规范;
- 在 Java 7 之后原先位于【方法区(Method Area)】里的「字符串常量池」被移动到 「Java 堆(Heap)」中,
- 并且使用元空间替代了永久代(java.lang.OutOfMemoryError: PermGen space 不复存在)
上面这部分内容还是要参照 JVM 架构图来理解,主要是 Runtime Data Area 包含的部分
- Java 堆(数组和类对象)常量池(字面量和符号引用)
对象实例的分配区域, Java 堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的就可以, 可扩展的
-Xmx
-Xms
-Xss
GC 管理的主要区域(分代回收算法)
新生代 老年代
Eden 「from survivor」「to Survivor」 tenured
从存储的角度讨论
- JVM 3 大性能调优参数
- Xss 规定了每个线程虚拟机栈的大小 (256k)该配置影响进程中并发线程数的大小;
- Xms 堆的初始值 (该进程刚创建出来时 java 堆的大小,一旦对象容量超过了 java 堆的初始容量, java 堆将会自动扩容, 扩容至 -Xmx 设置的大小)
- Xmx 堆能扩展到的最大值 (一般 -Xms 和 -Xmx 的值相等, 堆的扩容会发生内存抖动,影响程序运行时的稳定性)
-
Java 内存模型中「堆」和「栈」的区别:
程序运行时的内存分配策略:
- 静态存储: 在编译时确定每个数据目标在运行时的存储空间需求;(不允许可变数据存储,递归等)
- 栈式存储: 数据去需求在编译时未知,运行时模块入口前确定;
- 堆式存储: 编译/运行时都不能确定, 动态分配
区别 1: 栈存放对象的地址 堆存放对象实例 区别 2: 栈自动释放内存 堆需要 GC 区别 3: 栈比堆小
- 元空间,堆,线程独占部分的联系
- intern() 方法