Java笔记_2
运行时数据区
分类
- 线程不共享: 程序计数器、
Java
虚拟机栈、本地方法栈 - 线程共享: 方法区、堆区
程序计数器
Program Counter Register
, 也叫做PC
寄存器, 每个线程会通过程序计数器记录当前要执行的字节码指令的地址
作用
- 控制程序指令的执行, 比如跳转, 分支, 异常
- 多线程情况下
JVM
通过程序计数器记录CPU切换前执行到哪一句, 切换回来后执行并继续解释运行
程序计数器会产生内存溢出的问题吗
- 内存溢出指在使用某一块内存区域时, 存放的数据需要占用的内存大小超过了虚拟机能够提供的内存上限
- 每个线程只存储一个固定长度的内存地址, 因此程序计数器不会产生内存溢出
PC
不用程序员修改
Java
虚拟机栈
先进后出(
FILO
), 每个方法调用一个栈帧来保存
Java
虚拟机栈随着线程的创建而创建, 线程销毁则栈回收- 由于方法可能会在不同的线程中执行, 所以每个线程都有自己的虚拟机栈
栈帧组成
局部变量表, 操作数栈, 帧数据
- 局部变量表: 方法执行过程中存放所有的局部变量, 与字节码局部变量表不太一样
- 栈帧中的局部变量表是一个数组, 每一个位置成为槽
slot
,long, double
占用两个槽, 其他类型占用一个槽 - 实例方法中的序号为
0
的位置存放的是this
, 指的是当前调用方法的对象, 运行时会在内存中存放实例对象的地址 - 方法参数也会保存在局部变量表中, 顺序与参数定义顺序一致
- 所以局部变量表保存了实例方法的
this
对象, 方法的参数, 方法体中声明的局部变量
- 栈帧中的局部变量表是一个数组, 每一个位置成为槽
1 | public void test(int k, int m) { |
- 操作数栈: 存放中间数据的一块区域, 如果一条指令将一个值压入操作数栈, 则后面的指令可以弹出并使用该值
编译期就可以确定操作数栈的最大深度, 从而在执行时正确分配内存大小
- 帧数据: 包含动态链接, 方法出口, 异常表的引用
- 动态链接保存的是符号引用到运行时常量池中内存地址的映射关系(比如引用其他类)
- 方法出口: 方法在正确或异常结束时, 栈帧会被弹出,
PC
应该指向上一个栈帧中的下一条指令的地址, 所以在当前栈帧中, 需要存储此方法的出口的地址 - 异常表引用: 存放代码中的异常处理信息, 包含异常捕获的生效范围以及异常发生以后跳转到的字节码指令位置
-
栈内存溢出
- 如果栈帧过多, 占用内存超过栈内存可以分配的最大空间以后就会出现内存溢出, 报
StackOverflowError
错误 - 比如递归的时候没有设置递归出口, 则会栈内存溢出
- 如果栈帧过多, 占用内存超过栈内存可以分配的最大空间以后就会出现内存溢出, 报
-
设置栈内存
- 通过参数
-Xss<值>
设置虚拟机栈大小, 单位是[字节, 且必须是1024的倍数, 默认] [k或者K] [m或者M] [g或者G] - 也可以用
-XX:ThreadStackSize=<大小>
, 但是比较复杂 HotSpot JVM
对栈的大小的最大值和最小值有要求, 超过范围会自动调整- 局部变量过多, 操作数栈深度过大也会影响栈内存大小
- 可以手动指定
-Xss256k
节省内存
- 通过参数
本地方法栈
Java
虚拟机栈存储了Java
方法调用时的栈帧, 本地方法栈存储的是本地方法的栈帧, 用cpp
编写的
- 在
Hotspot
虚拟机中,Java
虚拟机栈和本地方法栈实现上用了同一个栈空间, 本地方法栈会在栈内存上生成一个栈帧, 临时保存方法参数的同时, 方便出现异常时也把本地方法的栈信息打印出来
堆
堆内存是最大的内存区域, 创建出来的对象都在堆上
栈上的局部变量表中可以存放堆上对象的引用, 静态变量也可以存放堆对象的引用, 通过静态变量可以实现对象在线程之间共享
- 堆内存溢出
- 堆内存大小有上限, 如果一直向堆中放入对象达到上限后, 就会抛出
OutOfMemory
错误 - 有三个需要关注的值:
used, total, max
, 在Arthas
中可以使用dashbord -i
ormemory
命令查看used
: 当前已经使用的堆内存total
:JVM
已经分配的可用堆内存max
: 是可以分配的最大堆内存total
内存不足的时候,JVM
会扩大total
, 直到达到max
为止- 但不是在
total = used = max
的时候产生堆内存溢出, 这是因为有垃圾回收器存在的原因 - 默认情况下,
max
默认是系统内存的1/4
,total
默认是系统内存的1/64
, 实际应用中需要单独设置 -Xmx值 -Xms值
, 分别表示max
和total
的大小,max
必须大于2MB
,total
必须大于1MB
- 但是实际上
Arthas
堆内存显示的比实际上分配的要小一点, 这是因为使用了JMX
技术, 与垃圾回收器有关, 计算的是可以分配对象的内存, 而不是整个内存
- 一般设置的时候, 可以将
total
设置成max
相同的大小, 减少申请内存和压缩内存的开销
- 堆内存大小有上限, 如果一直向堆中放入对象达到上限后, 就会抛出
方法区
存放基础信息的位置(类的元信息, 运行时常量池, 字符串常量池), 线程间共享, 是一个虚拟概念
- 用来存放每个类的基本信息(元信息), 一般称为
InstanceKlass
对象, 在类的加载阶段完成 - 还用来存放了字节码中的运行时常量池, 通过编号查表的方式找到常量, 称为静态常量池
- 常量池加载到内存中之后, 可以通过内地址快读定位到常量池中的内容, 就是运行时常量池
JDK 7
以及之前
- 方法区存放在堆的永久代空间, 堆的大小由
JVM
参数控制 - 在
Arthas
中可以看见ps_perm_gen
就是永久代, 因为在堆上, 所以设置了max
, 存储空间大小有限制
JDK 8
以及之后
- 方法区存放在元空间中, 位于操作系统内存中, 独立于
JVM
内存之外, 默认情况下只要不超过操作系统上限, 可以一直分配 - 在
Arthas
中可以看见metaspace
, 就是元空间,max=-1
, 表示没有上限, 因此只要不超过操作系统的上限即可
- 方法区溢出
JDK 7
在堆上, 十几万个类会出现错误JDK 8
运行上百万次, 不会出现错误JDK 7
方法区存储在堆区中的永久代空间, 可以设置-XX:MaxPermSize=值
来控制JDK 8
方法区位于元空间中, 默认不超过操作系统内存上限即可, 同样可以使用-XX:MaxMetaspaceSize=值
设置元空间大小- 启动程序的时候最好设置元空间的最大大小, 没有特殊情况设置成
256M
, 以免占用其他程序的内存, 能够容纳三十多万个类的加载
字符串常量池
存储代码中定义的常量字符串内容, 比如
"123"
1 | public class Test { |
- 早期的设计中, 字符串常量池是运行时常量池的一部分, 存储位置相同, 但后续将字符串常量池和运行时常量池进行拆分
JDK 7
之前, 运行时常量池包含字符串常量池, 都在方法区的永久代中JDK 7
时, 字符串常量池从方法区拿到了堆中, 运行时常量池剩下的东西还在方法区的永久代中JDK 8
之后, 没有永久代了, 所以运行时常量池在元空间中, 而字符串常量池仍然在堆中
1 | public static void main(String[] args) { |
1 | public static void main(String[] args) { |
1 | public static void main(String[] args) { |
- 静态变量存储在哪里?
JDK 6
以及之前, 静态变量存储在方法区中, 也就是永久代中JDK 7
以及之后的版本中, 静态变量存储在Class
对象中, 脱离了永久代
直接内存
不属于
Java
运行时的内存区域, 在JDK 1.4
中引入了NIO
机制, 使用了直接内存, 比如Netty
网络框架
-
直接内存解决两个问题
Java
堆中的对象如果不再使用要回收, 回收时会影响对象的创建和使用IO
操作比如读文件, 需要先把文件读入直接内存(缓冲区), 然后再把数据复制到Java
堆中
-
现在直接放入直接内存即可, 同时
Java
堆上维护直接内存的引用, 减少了数据复制的开销, 写文件也是同样的思路 -
所以现在不需要从直接内存上复制到堆中了, 减少回收对象的影响, 提升读写文件的效率
-
可以使用
ByteBuffer
在直接内存上创建数据, 在Arthas
中的memory
可以查看direct
部分的相关信息ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
- 直接内存存在溢出现象
-XX:MaxDirectMemorySize=值
修改直接内存的大小, 如果不设置这个参数, 则会自动选择最大分配的大小- 如果底层使用了
NIO
, 则需要设置这个参数; 如果没有用到直接内存, 也可以不设置这个参数 - 具体设置的大小需要进行压力测试以后, 确定最大内存
总结
- 运行时数据区分为了哪几个部分? 每个部分的作用是什么?
- 程序计数器,
Java
虚拟机栈, 本地方法栈. 是线程不共享, 每个线程有一块独立的区域- 程序计数器: 记录当前要执行的字节码指令的地址, 不会出现内存溢出的问题
- Java虚拟机栈和本地方法栈: 每个方法的调用会使用一个栈帧来保存数据, 会内存溢出, 一般因为递归没有出口
- 方法区, 堆, 是线程共享的
- 堆中存放创建的对象, 最容易内存溢出, 与垃圾回收有关
- 方法区存放类的元信息, 以及常量池, 会出现内存溢出
- 程序计数器,
- 不同
JDK
版本之间运行时区域的区别是什么?JDK 6
方法区放在堆里面, 称为永久代, 字符串常量池放在方法区中JDK 7
字符串常量池从方法区中独立, 放在了堆上JDK 8
字符串常量池依然在堆里面, 但是方法区称为元空间, 从堆中独立
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!