深入学习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(引用)中存储的就是对象地址

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

相关文章
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
570 1
|
Java API 调度
如何避免 Java 中的 TimeoutException 异常
在Java中,`TimeoutException`通常发生在执行操作超过预设时间时。要避免此异常,可以优化代码逻辑,减少不必要的等待;合理设置超时时间,确保其足够完成正常操作;使用异步处理或线程池管理任务,提高程序响应性。
704 13
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
357 1
|
Java 程序员 数据库连接
我们详细地讲解一下 Java 异常及要如何处理
我是小假 期待与你的下一次相遇 ~
311 1
|
SQL Java 中间件
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
在BeetISQL 2.13.8版本中,客户使用batch insert向yashandb表插入数据并尝试获取自动生成的sequence id时,出现类型转换异常。原因是beetlsql在prepareStatement时未指定返回列,导致yashan JDBC驱动返回rowid(字符串),与Java Bean中的数字类型tid不匹配。此问题影响业务流程,使无法正确获取sequence id。解决方法包括:1) 在batchInsert时不返回自动生成的sequence id;2) 升级至BeetISQL 3,其已修正该问题。
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
|
SQL druid Oracle
【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
客户Java日志中出现异常,影响Druid的merge SQL功能(将SQL字面量替换为绑定变量以统计性能),但不影响正常业务流程。原因是Druid在merge SQL时传入null作为dbType,导致无法解析递归查询中的`start`关键字。
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
873 14
|
缓存 Java 应用服务中间件
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
5149 5
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
IDE 前端开发 Java
怎样避免 Java 中的 NoSuchFieldError 异常
在Java中避免NoSuchFieldError异常的关键在于确保类路径下没有不同版本的类文件冲突,避免反射时使用不存在的字段,以及确保所有依赖库版本兼容。编译和运行时使用的类版本应保持一致。
803 8