一、内存区域(运行时数据区域)划分:
-
程序计数器
- 线程安全,每条线程都有一个独立的程序计数器
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 分支、循环、跳转、异常处理、线程恢复等都是依靠它实现
-
Java虚拟机栈
- 也是线程私有,生命周期与线程相同
- 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
- 局部变量表存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向一条字节码指令的地址);其中long和double占用两个局部变量空间,其余的一个。
- 局部变量表所需的空间在编译期间完成分配,且在方法运行期间不会改变大小
- Java虚拟机规范对这个区域规定了两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;
- 如果虚拟机可以动态扩展,如果扩展时无法申请到足够多的内存,就会抛出OutOfMemoryError异常。
-
本地方法栈
- 与虚拟机栈非常相似
- 与虚拟机栈区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈则为虚拟机使用的Native方法服务。
-
Java堆
- 是被所有线程共享;唯一目的就是存放对象实例
- Java虚拟机规范:所有的对象实例以及数组都要在堆上分配。但随着JIT编译器的发展与逃逸分析技术逐渐成熟等原因,所有对象都分配在堆上也就不是那么绝对。
- 是垃圾收集器管理的主要区域,很多时候也被称为“GC堆”。
-
方法区
- 与Java堆一样,是各个线程共享的内存区域
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- HotSpot虚拟机使用永久代来实现方法区
- 运行时常量池是方法区的一部分
二、HotSpot虚拟机对象探秘
- 对象的创建
- 虚拟机遇到一条new指令(创建对象)时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的是否已被加载、解析和初始化过。若没有则先执行相应的类加载过程。类加载检查通过后,虚拟机为新生对象分配内存。
- 内存分配方式:
- 指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空余的内存放在另一边,中间放着一个指针作为分界点的指示器,所分配的内存仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表:如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,此时无法使用指针碰撞;则虚拟机维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 分配内存时保证线程安全
- 对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲
- 对象的内存布局
- 在HotSpot虚拟机中,对象在内存中存储的布局分为3块区域:
- 对象头(Header)。 包含两部分信息,如下:
- 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 实例数据(Instance Data)。是对象存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
- 对齐填充(Padding)。仅仅起着占位符的作用。
- 对象头(Header)。 包含两部分信息,如下:
- 在HotSpot虚拟机中,对象在内存中存储的布局分为3块区域:
- 对象的访问方式
- 使用句柄。
- Java堆中划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图:
- 好处:reference中存储的是稳定的句柄地址,在对象被移动(如垃圾收集时)时只会改变句柄中的示例数据指针,而reference本身不需要修改。
- 直接指针。
- reference中存储的直接就是对象地址。
- 好处:速度更快,节省了一次指针定位的时间开销
- 使用句柄。
- 实战:OutOfMemoryError异常
- Java堆溢出
- 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出
- 本机直接内存溢出