第二章 Java内存区域与内存溢出异常
2.2 运行时数据区域
2.2.1 程序计数器
- 程序计数器是一块较小的内存空间,它的作用可以看做是当前线程锁执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- “线程私有”:每条线程都有一个独立的程序计数器
- 如果正在执行一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果是native方法,则计数器值为空
- 程序计数器是JVM中唯一没有规定OutOfMemoryError情况的区域
2.2.2 Java虚拟机栈
- 线程私有,生命周期与线程相同
- 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息
- 每个方法执行的过程对应的就是一个栈帧在虚拟机栈中从入栈到出栈的过程
- Java虚拟机栈规定了两种异常情况:
- 线程请求的栈深度 超过了虚拟机允许的深度,会抛出StackOverflowError(当我们在代码中看到这个异常时,就应该想到可能是虚拟机栈出了问题)。
- 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常。
2.2.3 本地方法栈
- 本地方法栈和虚拟机栈的功能类似,只不过是为JVM调用native方法时服务的
- 与虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。
2.2.4 Java堆
- Java堆是Java虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域
- 目的:存放实例对象
- Java堆是垃圾收集器管理的主要区域。可以细分为:“新生代和老生代”,或者“Eden\ From Survivor\ To Survivor等”
- 堆中没有足够内存完成实力分配,并且堆也无法再扩展时,就会出现OOM异常
- Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可
2.2.5 方法区
- 所有线程共享
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.2.6 运行时常量池
- 运行时常量池是方法区的一部分
- 用于存放编译期生成的各种字面量和符号引用
- 动态性,常量不一定编译时才能产生:String类的intern()方法
2.2.7 直接内存
- 直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但可能产生OOM异常
2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建
-
虚拟机在遇到一条new指令时,
- 首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查该类是否已加载。
- 如果没有加载,还要先执行相应的类加载过程
-
虚拟机为新生对象分配内存。方法有两种
-
指针碰撞:Java堆中内存规整,分配内存时,指针向空闲的内存那边挪动与对象大小相等的距离
-
空闲列表:Java堆中内存不规整,列表上记录哪些内存是可用的
-
-
解决并发环境下的分配内存有两种方式:
- 对分配内存空间的动作进行同步处理(CAS)
- 把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在堆中预先分配一小块内存(TLAB)
-
内存分配完成后还要将分配的内存空间都初始化为零值,从而保证对象的实例字段在Java代码中可以不赋初始值就直接使用
-
初始化后还要给对象进行相关各设置,诸如哈希码、GC分代年龄信息等,存放在对象头
-
对象初始化操作
2.3.2 对象的内存布局
-
对象在内存中存储的布局可以分为三块区域:
- 对象头
- 实例数据
- 对齐填充
-
对象头一搬包含两部分内容:
- 对象的运行时数据(如HashCode、GC年龄、线程持有的锁等信息)和类型指针(对象指向他的类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例)。
- 如果对象是数组,则还有一个部分来记录数组的长度
-
实例数据是对象真正存储的有效信息,即程序中的字段内容
-
对齐填充不是必然的,仅仅起着占位符的作用。因为对象的大小必须是8字节的整数倍
2.3.3 对象的访问定位
-
Java程序通过栈上的reference数据来操作堆上的具体对象
-
对象的访问形式取决于虚拟机,目前主流的有两种方式:
- 使用句柄
- 直接指针
-
使用句柄是间接访问,优点是reference中存储的是稳定的句柄地址,对象移动时只会改变句柄中的实例数据指针
-
使用直接指针是直接访问,优点就是速度快
参考
《深入理解Java虚拟机 第2版》