◇运行时数据区
这部分不多赘述,一张图说明:
运行时数据区由Java虚拟机管理。
- 程序计数器(一块不会发生oom的区域)
这部分是一块较小的区域,主要帮助字节码解释器读取字节码指令(通过改变计数器的值)并且负责记录当前线程位置以方便正确恢复位置。
线程私有
- Java虚拟机栈
它也是线程私有的,生命周期与线程相同,描述的是Java方法执行的线程内存模型:Java方法在执行的时候,虚拟机会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法入口等信息。方法被调用的过程,对应栈帧入栈到出栈的过程。
特别说明局部变量表:存放编译期可知的基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)、和returnAddress类型(粗略理解为返回值地址)。
在《Java虚拟机规范》中,对于这片区域有两种异常:如果线程请求的栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常,如果在动态扩展中无法申请到足够的内存时,会抛出OOM异常。(hotspot虚拟机栈容量不可动态扩展)
- 本地方法栈
本地方法栈和虚拟机栈很类似,区别就是虚拟机栈为虚拟机执行Java方法而服务,而本地方法栈则为Java虚拟机使用到的本地方法而服务。
- Java堆(Heap)(逻辑连续)(线程共享)
这部分内存区域是虚拟机管理下的最大的一块内存区域,唯一目的是存放对象实例和数组。Java世界中几乎所有的对象实例都在这里分配。(几乎所有不代表全部,如果一个对象确定不会在一个方法中发生逃逸的化,那么这个对象可以被分配在栈上,并且随着栈帧弹出而消亡,不需要GC)并且它是垃圾收集器管理的区域。
从内存分配的角度看,所有线程共享的Java堆可以划分为多个线程私有的分配缓冲区,此举目的是提升内存分配时的效率,并且能够提升垃圾回收效率,但要注意的是,这不改变堆是线程共享区域的特性。
物理不连续但逻辑连续。
Java堆的内存是可以扩展的,通过参数-Xmx和-Xms设定,如果内存分配没完成的情况下堆空间用完了,会抛出OOM异常。
⒌方法区(存放已加载的类的结构性信息,给类上户口)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、访问修饰符、字段描述。方法描述即时编译器编译后的代码缓存等数据。方法区是堆的逻辑概念,但它是非堆的,需要与堆区分。
JDK8以前实现为永久代,从JDK9之后引入元空间概念将这部分区域与堆分离。
6.运行时常量池(在方法区分配)
是方法区的一部分,用于在类加载后存放编译期生成的各种字面量和符号引用,此外还有存放由符号引用翻译出来的直接引用。
无法再申请到内存时会抛OOM异常。
⒎直接内存
不是运行时数据区的部分,但仍然会被频繁使用,如NIO这种基于缓冲区和通道的I/O方法会引用这块区域来提高效率。
◇HotSpot虚拟机对象探秘
对象的创建
①当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②在类加载检查之后,开始为对象分配内存,对象所需的空间大小在这个时候已经完全确定下来,虚拟机开始为对象在Java堆上划分出一块区域。在这个过程中可能会出现分配冲突的问题,这时候有两种解决方案,一种是用CAS乐观锁机制进行失败重试,操作失败就再来;第二种是利用分配缓冲区(TLAB),现在各自缓冲区中分配,只有缓冲区用完了之后在分配对象时才进行同步锁定。
③分配内存完成后,虚拟机将分配到的空间(不包括对象头)都初始化为零值。
④之后对对象头进行必要的初始值设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等。进行这些的目的是保证不会访问到其他没有初始化的对象。
⑤在程序层面对对象进行程序员想要的初始化(<init>)。
对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例
数据(Instance Data)和对齐填充(Padding)。
对象头包含:ⅰ对象自身的运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有的锁等。
ⅱ类型指针,通过这个指针确定该对象是哪个类的实例。
实例数据:对象真正存储的有效数据,例如代码的字段。
对齐填充:这没啥特殊含义,仅仅起到占位符的作用,将实例数据填充为8的整数倍(虚拟机要求的)。
对象的访问定位
间接:通过句柄访问
直接:指针访问
句柄访问的话,会在堆分配一块内存作为句柄池,本地变量表中的Reference存放句柄池的地址,再根据句柄池的指针找到对象实例数据和对象类型数据。
优点:Reference中存放的是句柄地址,在对象被移动的时候,只会改变句柄中实例指针的指向,而Reference本身不需要修改。
直接访问:本地变量表中存放的就是对象地址,直接去就行。
优点:速度快,开销小。
内存异常
先分是内存溢出还是内存泄漏,如果是内存泄漏,那就比较麻烦了,根据对象到GCROOTS(可达性分析)的引用链查找对象和哪些其他类有关,再根据泄露信息和引用链信息找到泄露位置。
如果是内存溢出,看看是否有向上调整的空间,再从代码上检查是否有对象生命周期过长,持有时间过长、存储结构不合理等情况的出现。