Java内存区域与内存溢出异常 JVM笔记1
目录
运行时数据区域
JAVA虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
程序计数器
- 一块较小的内存空间,可以看作当前线程所执行的字节码的
行号指示器
。 - 通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 每条线程都需要一个独立的程序计数器各条线程之间互不影响,独立存储,我们称这类内存区域为线程私有的内存。
虚拟机栈
- 也是线程私有的,生命周期与线程相同。
- 用来描述Java执行的内存模型(局部变量表,操作数栈,动态链接,方法出口等)
- 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
- 如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
本地方法栈
- 线程私有
- 与虚拟机栈作用类似,不过虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务
- 也会抛出StackOverflowError和OutOfMemoryError异常
堆
- 线程共享
- Java虚拟机所管理内存最大的一块,在虚拟机启动时创建。
- 用于存储
对象实例
,几乎所有对象实例都在这里分配内存。 - 是垃圾收集器管理的主要区域
- 如果堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常
方法区
- 线程共享
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 很少出现垃圾回收行为
- 常量池时方法区的一部分,Class文件除了又类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。常量池具有动态性,运行期间也可能将新的常量放入池中,比如String类的intern()方法
- 无法满足内存分配需求时,将抛出OutOfMemoryError异常
直接内存
- 即本机的内存,不是虚拟机运行时数据区的一部分,但是经常用到。
- 也可能导致OutOfMemoryError异常,比如为虚拟机分配的内存大于物理内存限制。
对象的内存布局
在HotSpot虚拟机中,对象在内存中的存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)。
对象头
HotSpot的对象头包括两部分信息。
第一部分:
- 存储对象自身的运行时数据。哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 长度在32位和64位虚拟机中(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。
第二部分:
- 对象头另外一部分时类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
- 若对象是数组,那么在对象头中还必须有一块用于记录数组长度的数据。
实例数据
- 是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。
- HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Pointers),相同宽度的字段总是被分配在一起。
对齐填充
- 并不是必然存在的,也没有特别含义。
- 起到占位符的作用。
对象的访问定位
建立对象是为了使用对象,我们的Java程序主要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过什么样的方式区定位、访问堆中对象的具体位置,所以对象访问方法方式也是取决于虚拟机实现而定的。目前主流的访问方式为句柄
和直接指针
两种。
- 若是使用句柄访问,Java堆中会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例数据(在Java堆)与类型数据(在方法区)各自的具体地址信息。
优点
:在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。 - 若是使用直接指针访问,Java堆就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。
优点
:访问速度更快,节省一次指针定位时间。