深入学习Java虚拟机——虚拟机内存区域与内存溢出异常

简介: 强烈推荐书籍《深入理解Java虚拟机》,本文为个人学习笔记,删除一些不必要文字,并加入部分个人理解,日后复习较为简洁易懂   1.1 程序计数器     1. 程序计数器是一段较小的内存空间,可以看作为当前线程所执行字节码的行号指示器。

1. 运行时数据区域

 

1.1 程序计数器

    1. 程序计数器是一段较小的内存空间,可以看作为当前线程所执行字节码的行号指示器。通过改变这个计数器的值来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器。

    2. 每条线程都会有一个独立的程序计数器,各线程间程序计数器互不影响,独立存储,所以这个内存区域是线程私有的

    3. 如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是本地(Native)方法,则这个计数器值为空,此内存区域是唯一一个在Java虚拟机中没有OutOfMemoryError情况的区域

1.2 虚拟机栈

    1. 首先,虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法的执行模型:每个方法在执行的同时都会创建一个栈桢,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成,就对应着一个栈桢从入栈到出栈的过程。

    2. 局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,long,float,double)、对象引用类型和returnAddress类型(指向一条字节码指令的地址)。且局部变量表的内存空间会在编译期完成分配,方法运行期间不会改变局部变量表的大小。

    3. 异常状况:

(1)如果线程请求的栈深度大于虚拟机所允许的深度,则将抛出StackOverflowError异常

(2)如果虚拟机栈可以动态扩展,而扩展时无法申请的足够的内存,就会抛出OutOfMemoryError异常

1.3 本地方法栈

    1. 本地方法栈与虚拟机栈类似,但虚拟机栈是为虚拟机执行Java方法服务的,而本地方法栈是为虚拟机使用的本地方法服务。

    2. 同样的,该内存区域也会有StackOverflowError异常和OutOfMemoryError异常。

1.4 Java堆

    1. Java堆是被所有线程共享的内存区域,在虚拟机启动时创建。此区域只用来存储对象实例,几乎所有的对象都会在这里被创建(并不是所有的对象都在堆中创建)。

    2. Java堆是垃圾收集器管理的主要区域。从内存回收角度看,垃圾收集器主要采用分代收集算法,所以还可以将Java堆分为新生代和老年代,进一步细分为Eden区,From Survivor区和To Survivor区。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。

    进行这些划分的目的都是为了更快更好的回收内存或者分配内存

    3. 可能发生的异常:Java堆可能会处于物理上内存空间不连续的内存空间中,但逻辑上必须是连续的。其空间大小可以通过-Xmx和-Xms来控制,可以实现为固定大小,也可以为可扩展大小。当没有足够的内存空间完成分配并且堆无法扩展时,就会抛出OutOfMemoryError异常

1.5 方法区

    1. 方法区是线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法区属于堆的一个逻辑部分,但它却仍然要与Java堆区分开。

    2. 方法区与堆类似,不需要连续的内存空间、内存空间大小可以固定或者可扩展,还可以选择不实现垃圾收集。在方法区,很少出现垃圾收集,这个区域的内存回收主要针对常量池的回收以及对类型的卸载。

    3. 当方法区无法满足内存分配需求时,就会抛出OutOfMemoryError异常

    4. 运行时常量池:该区域是方法区的一部分,Class文件中除了有类似的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类被加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,即运行期间也可能将新的常量放入运行时常量池,比如String类中的intern()方法。

1.6 直接内存

    1.直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

    2.应用:在jdk1.4以后加入了NIO类,引入了一种基于通道与缓冲区的新IO方式,它使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中DirectByteBuffrer对象作为这块内存的引用进行直接操作,避免了在Java堆与Native堆之间来回复制数据,显著提高了性能。由于分配的是Native内存空间,所以大小不会受到Java堆大小的限制,但是肯定会受到本机总内存的限制。在通过设置虚拟机参数来设置堆等区域的内存空间时,忽略直接内存大小就有可能导致各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常

2. Java虚拟机对象

2.1 对象的创建

    1. 创建过程(一般对象,即不包括数组和Class对象):

(1)加载对象类:当虚拟机运行一条new指令时,首先检查这个指令的参数(也就是new后面的类名)是否能在常量池中定位到一个类的符号引用,并且检查这个类是否已被加载、解析和初始化过。如果没有,那就必须进行相应的类加载过程(该过程在后面会详细分析)。

(2)分配对象所需内存空间:

    在类加载完成后即可确定对象所需的内存空间大小,给对象分配内存空间就是把一块确定大小的内存从Java堆中划分出来。分配方式主要取决于虚拟机所采用的GC是否带有压缩整理功能。

    有压缩整理功能的GC会把Java堆分成两部分,一部分是被占用的,一部分是空闲的,通过一个指针作为分界的指示器,分配内存是只需要把指针向空闲区移动即可;不带压缩整理功能的GC会导致Java堆处于一种空闲与占用交错的内存空间,这时虚拟机就必须维护一个列表,记录堆中可用的内存空间,分配时只需要找到一个足够大小的空间划分给对象即可。

    但是,在并发情况下,以上两种方式也并不安全,比如,正在给对象A分配空间时,指针还未修改,对象B又占用了该指针来分配空间。所以,虚拟机采用了CAS加上失败重试的方式保证更新操作的原子性;另一种方式是把内存分配动作按照线程划分在不同的空间中进行,即每个线程在堆中先分配一小块内存,称为本地线程分配缓冲(TLAB),那个线程要分配内存,就在那个线程的TLAB上分配,当使用完TLAB并分配新的TLAB时,才需要同步锁定,虚拟机使用TLAB可通过 -XX:+/-UseTLAB参数来设定。

(3)内存空间初始化:内存空间分配后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一过程也会提前至TLAB分配时执行。该过程保证了对象即使不赋予初始值额可以使用,能访问到的字段的数据类型均为所对应的零值。

(4)设置对象信息:例如这个对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分带年龄等信息。这些信息存放在对象的对象头中,根据虚拟机当前运行状态的不同,是否启用偏向锁等。

(5)对象数据初始化:以上步骤完成后,对象已经创建成功,但此时对象内所有字段为零或null,此时便进行对象数据初始化,创造程序员所需要的对象。

2.2 对象的内存布局

虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头,实例数据,对齐填充。

    1. 对象头:包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据被称为“Mark  Word”,这部分数据长度在32或64位的虚拟机中分别为32bit或64bit。对象需要存储的运行时数据很多,超出32或64bit所能记录的限度,但对象头信息是与对象自身定义的数据无关的额外存储成本,Mark  Word被设计为一个非固定的数据结构以便在极小的空间内存储尽量多的信息,例如在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,则Mark  Word的32bit空间中,25bit用于存储对象哈希值,4bit用于存储对象分带年龄,2bit用于存储锁标志位,1bit固定为0,其他状态下的存储内容如下

存储内容 标志位 状态
对象哈希码、对象分代年龄、 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 重量级锁定
11 GC标记
偏向线程id、偏向时间戳、对象分代年龄 01 可偏向

    对象头的另一部分数据是类型指针,即对象指向他的类元数据的指针,虚拟机通过该指针确定对象所属的类,但并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据信息并不一定要经过对象本身。对于数组对象,对象头中还必须友谊路爱用于记录数组长度的数据。

    2.实例数据:这里是对象真正存储的有效信息,包括各个字段的内容,无论是当前类的还是父类的。

    3.对齐填充:这一部分并不是必要存在的,也没有特殊含义,仅仅是为了使对象的大小必须是8字节的整数倍,如果对象的大小不足,则会进行填充补全。

2.3 对象的访问定位

创建对象是为了使用对象,在Java程序中,通过栈上的对象引用来操作堆上的对象,那么这个引用通过何种方式去定位和访问堆中的对象的具体位置?具体实现取决于虚拟机实现,主要有两种方法,使用句柄和直接指针。

    1.使用句柄访问:Java堆中划分一块内存作为句柄池,reference(引用)中存储的就是句柄池中存储的对象的句柄地址,而句柄包含了对象的实例数据与类型数据各自的具体地址信息。

    2.使用直接指针访问:reference(引用)中存储的就是对象地址

这两种访问方式各有优势,使用句柄访问的好处是引用中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的示例数据指针,而引用本身不需要修改。而使用直接指针访问方式的最大好处是速度更快,大部分虚拟机都会采用这种方式。

相关文章
|
7天前
|
存储 Java 编译器
Java内存区域详解
Java内存区域详解
21 0
Java内存区域详解
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
37 2
|
1月前
|
存储 算法 安全
【JVM】深入理解JVM对象内存分配方式
【JVM】深入理解JVM对象内存分配方式
26 0
|
21天前
|
存储 缓存 Java
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
34 1
|
21天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
60 0
|
7天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
38 0
|
11天前
|
Java
Java中的异常类总结
Java中的异常类总结
|
11天前
|
存储 算法 安全
深度解析JVM世界:JVM内存分配
深度解析JVM世界:JVM内存分配
|
29天前
|
SQL Java
java中的异常
java中的异常
10 1
|
29天前
|
Java 程序员 编译器
Java中异常
Java中异常
12 0