JVM之运行时数据区
前言
本篇较为详细讲解了 JVM的运行时数据区, 包括堆, 虚拟机栈, 本地方法栈, 方法区, 字符串常量池, 程序计数器和 JVM内存模型变迁史
在整个 JVM构成主要由三部分组成: 类加载系统, 运行时数据区, 执行引擎
运行时数据区按照线程使用情况和职责分成两大类
- 线程独享(程序执行区域)
- 虚拟机栈, 本地方方法栈, 程序计数器
- 不需要垃圾回收
- 线程共享(数据存储区域)
- 堆和字符串常量池
- 存储类的静态数据和对象数据
- 需要垃圾回收
java1.8开始, 运行时常量池就不在运行时常量池中了
图片来自于guide哥网站
堆
java堆在 JVM启动时创建内存区域去实现对象, 数组与运行时常量的内存分配, 它是虚拟机管理最大的, 也是垃圾回收的主要内存区域
同时, 堆的唯一目的就是存放对象实例, 几乎所有的对象实例以及数组都在这里分配内存
JDK1.7开始默认开启逃逸分析, 如果某些方法中的对象饮用没有被返回或者未被外面使用, 那么该对象可以直接在栈上进行内存分配
对象逃逸: 当在某个方法内创建了一个对象, 且该对象被方法体之外其他变量所饮用, 那么当该方法执行完毕之后不会被GC回收, 这就是对象逃逸 逃逸对象的内存在堆中, 未逃逸对象的内存分配在栈中
Java堆是垃圾收集器管理的主要区域, 因此也被称之为GC堆
, 下图是 java 1.7及之前版本的堆内存划分, 其他版本可看后面的 JVM内存模型变迁
大部分情况下, 对象会先在 Eden区域进行分配, 在一次新生代垃圾回收之后, 如果对象存活, 那么会进入到 S0或者 S1区域, 且年龄加一, 当对象的年龄达到某个阈值时, 该对象会进入老年代中
阈值参见: issue552“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。
动态年龄计算的代码如下
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { //survivor_capacity是survivor空间的大小 size_t desired_survivor_size = (size_t)((((double) >survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; while (age < table_size) { total += sizes[age];//sizes数组是每个年龄段对象大小 if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; ... }
如果 Java 堆中没有足够的内存来完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError
异常。
虚拟机栈
栈帧
栈帧是用于支持虚拟机进行方法执行的数据结构
栈帧存储了方法的局部变量表
, 操作数栈, 动态连接和返回地址等地址, 每个方法从调用到执行完成的过程, 都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程
栈内存为线程私有的空间, 每个线程都会创建私有的栈没存, 生命周期与线程相同, 除了一些 Native方法调用是通过本地方法栈实现的, 其他所有java方法调用都是栈来实现的
异常
- 如果线程请求的栈深度大于虚拟机所允许的栈深度, 将抛出
StackOverflowError
异常 - 如果 java虚拟机栈的容量允许动态扩展, 当栈扩展时如果无法申请到足够的内存就会抛出
OutOfMemoryError
异常
本地方法栈
和虚拟机栈所发挥的作用非常相似, 区别如下:
- 虚拟机栈为虚拟机执行 java方法服务
- 本地方法栈为虚拟机使用到的 Native方法(例如 C++程序)服务
方法区
方法区是各个线程共享的内存区域, 它作用于已经被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等数据, 目的是与 java堆进行区分
方法区的具体实现: 永久代, 元空间
永久代和元空间的区别:
- 版本更新
- jdk1.8之前的方法区是永久代
- jdk1.8之后的方法区是元空间
- 存储位置
- 永久代使用的内存区域是 JVM进程所使用的区域, 大小受 JVM的大小限制
- 元空间使用的是物理内存区域, 元空间大小只受物理内存大小的限制
- 存储内容不同
- 永久代主要存储方法区存储内容中的数据
- 元空间只存储类的元信息, 而静态变量和运行时常量池都在堆中
方法区存储数据:
- class
- 类型信息
- 类型的常量池
- 字段信息
- 方法信息
- 类变量
- 指向类加载器的引用
- 指向 class实例的引用
- 方法表
- 运行时常量池(字符串常量池)
- JIT编译器编译之后的代码缓存
如果方法区无法满足新的内存分配需求时,将会抛出
OutOfMemoryError
异常
字符串常量池
字符串常量池是 JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域, 主要目的是为了避免字符串的重复创建, 其数据结构采用了 StringTable的数据结构, 类似于哈希表结构
- 单独使用
""
创建的字符串都是常量, 编译时就存储在字符串常量池中 - 使用只包含常量的字符串连接符, 如
"ning"+"xuan"
创建的也是常量, 编译时就存储在字符串常量池中 - 使用
new Stirng("")
创建的对象会存储到 heap中, 是运行时新创建的 - 使用包含变量的字符串连接
"ning" + xuan
, 会存储到 heap中, 是运行时新创建的 - 运行期刁永刚 String的 inten()方法可以向 字符串常量池中动态添加对象
程序计数器
程序计数器是一块较小的内存空间, 他可以看做是当前线程所执行的字节码的行号指示器
每条线程都有一个独立的程序计数器, 各线程之间的计数器互不影响, 为线程私有
程序计数器的作用:
- 多线程的情况下, 程序计数器用来记录当前执行的位置, 当线程切换回来时能正确运行
- 字节码解释器通过改变程序计数器来以此读取指令, 从而实现代码的流程控制: 如: 选择, 循环, 顺序执行, 异常处理等
程序计数器是唯一不会出现
OutOfMemoryError
异常的内存区域, 其生命周期随着线程的创建而生, 线程的销毁为亡
JVM内存模型变迁
java1.7
- Young区: 年轻区, 主要保存年轻对象, 分为三个部分 Eden区, 两个 Survivor区
- Tenured: 老年区, 主要保存年长对象, 当对象在 Young区复制转移一定的次数之后, 对象就会被转移到 Tenured区
- Perm 永久区: 主要保存 class, method, filed对象, 这部分的空间一般不会溢出, 除非一次性加载了很多的类, 不过在涉及到热部署的应用服务器的时候, 有时候会遇到 OOM: PermGen space的错误
- Virtual区: 最大内存和初始内存的差值
java1.8
由两部分组成:
- 新生代(Eden + 两个 Survivor)
- 老年代(OldGen)
Metaspace 所占用的内存空间不是在虚拟机内部, 而是在本地内存空间中, 区别于 java1.7
java1.9
取消新生代, 老年代的物理划分, 将堆划分为多个区域(Region), 这些区域中包含有了逻辑上的新生代和老年代
本文内容到此结束了
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位大佬指出。
我是 宁轩 , 我们下次再见
转载自:https://juejin.cn/post/7249288087820910651