推广 热搜: csgo  vue  angelababy  2023  gps  新车  htc  落地  app  p2p 

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

   2023-08-07 网络整理佚名2410
核心提示:程序计数器是唯一没有内存溢出()的区域)并不是虚拟机运行时数据区的一部分,这部分内存也被频繁的使用,也可能导致异常出现其一通过代码验证各个运行时区域储存的内容,其二根据异常提示信息迅速得知时哪个区域的内存溢出,怎样的代码可能会导致这些区域内存溢出,如何处理异常线程请求的栈深度大于虚拟机所允许的最大深度,将抛出异常虚拟机的栈内存允许动态扩展,当扩展容量无法申请到足够的内存时,将抛出异常

1 概述

java中的内存管理是由虚拟机自动管理的。 虽然不需要手动清理和回收垃圾,但是当发生内存泄漏和溢出时,了解虚拟机如何使用内存对于Java程序员排查和纠正问题非常有帮助。 有帮助的

2.运行时数据区

Java程序执行过程中,Java虚拟机将其管理的内存划分为几个不同的数据区域,如下图所示:

2.1. 程序计数器

程序计数器是一个很小的内存空间,可以将其视为当前线程正在执行的字节码行号的指示器。 它是程序控制流程的指标,分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖它来完成。

为了线程切换后能够回到正确的执行位置,每个线程都需要有一个独立的程序计数器。 线程之间的计数器互不影响,独立存储。 因此,程序计数器是线程私有的。

程序计数器是唯一没有溢出()的区域

2.2. Java虚拟机栈

Java虚拟机栈为虚拟机执行Java方法提供服务。 它的生命周期和线程一样,也和程序计数器一样是线程私有的。 每个方法执行时,Java虚拟机都会同步创建一个栈帧(Stack frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用直到执行完成的过程都对应虚拟机中栈帧从入栈到出栈的过程。

如果线程请求的堆栈深度大于虚拟机允许的深度,则会抛出异常

如果Java虚拟机的栈容量可以动态扩展,那么当栈扩展时,如果没有足够的内存可以申请,就会抛出异常。

2.3. 原生方法栈

本地方法栈为虚拟机栈使用的本地方法提供服务。 和虚拟机栈一样,当栈深度溢出或者扩展失败时,也会分别抛出异常和异常。

2.4. Java堆

Java堆是虚拟机管理的内存中最大的区域。 它是所有线程共享的内存区域。 它是在虚拟机启动时创建的,其目的是存储对象实例。

Java堆是由垃圾收集器管理的内存区域,因此也称为GC堆。

Java堆可以位于物理上不连续的内存空间中,但逻辑上应该被认为是连续的。 Java 堆可以实现为固定大小,也可以扩展。 如果Java堆中没有内存来完成实例分配,并且堆无法再扩展,Java虚拟机就会抛出异常。

2.5. 方法区

与Java堆一样,方法区是每个线程共享的内存区域,用于存储已加载的类型信息、常量、静态变量以及即时编译器编译出来的代码缓存等数据。虚拟机。

方法区不需要连续的内存,可以选择固定大小或可扩展。 您也可以选择不实施垃圾收集。 这方面的垃圾收集行为比较少见,主要是常量池的回收和类型的卸载。

如果方法区不能满足新的内存分配要求,则会抛出异常

2.6。 运行时常量池

运行时常量池(Pool)是方法区的一部分。 Class文件中有类的版本、字段、方法、接口和常量池表。 常量池表用于存储编译期间生成的各种文字和合规性引用。 类加载后,部分内容会存储在方法区的运行时常量池中。

常量池在运行时的另一个重要特点是它是动态的。 Java语言并不要求常量只能在编译时生成,即Class文件中未预设的常量池内容可以在运行时在方法区进入常量池。 也可以将新的常量放入池中,例如类的()方法。

如果常量池无法再申请内存,就会抛出异常。

2.7. 直接记忆

直接内存()在虚拟机运行时不属于数据区,这部分内存也被频繁使用,也可能会引起异常

3.虚拟机对象探索

是最常用的虚拟机

3.1. 对象创建

类加载检查-->为新生对象分配内存-->初始化-->设置元数据、哈希码、GC分代年龄等信息-->()方法

类加载检查。 当Java虚拟机遇到字节码new指令时,首先会检查该指令的参数是否能在常量池中定位到某个类的符合引用,并检查该符合引用所代表的类是否已经被加载、解析和释放。已初始化,如果没有,那么必须先执行相应的类加载过程。

为新生对象分配内存。 对象所需内存的大小在类加载完成后就可以完全确定,而为对象分配空间的任务实际上相当于从Java堆中划分出一定大小的内存块。

指针碰撞(Bump):假设Java堆内存绝对规则,所有已用内存放在一侧,空闲内存放在另一侧,中间放置一个指针作为分界点的指示符,那么分配内存只是将指针向空闲空间方向移动等于对象大小的距离。

空闲列表(Free List):如果Java堆中的内存不规则,并且使用的内存和空间内存相互交错,那么没有办法简单地碰撞指针,虚拟机必须维护一个列表,记录哪些内存块可用,分配时从链表中找到足够大的空间分配给对象实例,并更新链表上的记录

选择哪种分配方式取决于Java堆是否规则,而是否规则又取决于所使用的垃圾收集器是否具有压缩和组织的能力()。

当使用带有压缩过程的收集器时,系统采用的分配算法是指针碰撞,简单高效。

当使用CMS等基于Sweep算法的收集器时,理论上只能使用更复杂的空闲列表来分配内存。

内存创建是虚拟机中非常频繁的行为。 即使只修改指针指向的位置,在并发情况下也不是线程安全的。 可能会发生这样的情况:内存正在分配给对象 A,而指针还没有来得及修改它。 对象B同时使用原来的指针来分配内存。

方案一:同步分配内存空间的动作。 事实上,虚拟机使用带有失败重试的CAS来保证更新操作的原子性。

方案二:根据线程将内存分配划分到不同的空间。 即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(Local,TLAB)。 无论哪个线程想要分配内存,都会在该线程的本地缓冲区中分配,并且只有本地缓冲区被用完。 ,仅当分配新缓冲区时才需要同步锁。

虚拟机是否使用TLAB可以通过-XX:+/-参数设置。

将分配的内存空间(不包括对象头)初始化为零值。 如果使用TLAB,这项工作也可以在分配TLAB时提前完成。 它保证了对象的实例字段可以在Java代码中直接使用而无需分配初始值,从而程序可以访问与这些字段的数据类型对应的零值。

对对象进行必要的设置,比如对象是哪个类的实例,如何查找类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。

至此,对于虚拟机来说,一个新的对象已经生成了。 从Java程序的角度来看,对象的创建开始——构造函数,new指令后会执行()方法

3.2. 对象内存布局

对象在堆内存中的存储布局分为:对象头()、实例数据(Data)和对齐填充()

对象头包含两类信息:

第一种用于存储对象本身的运行时数据,如哈希码()、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,以32位形式存储虚拟机中间是32位,64位虚拟机中间是64位。

当对象没有被同步锁锁定时,32位存储空间中的25个用于存储对象的哈希码,4个用于存储对象的分代年龄,2个用于存储锁标志,并且 1 固定为 0。

轻量级锁、重量级锁、GC标记、可偏向时间如下:

存储内容标志状态

对象哈希码、对象分代年龄

01

解锁

指向锁记录的指针

00

轻量级锁定

指向重量级锁的指针

10

膨胀(重量级锁)

空,无需记录信息

11

GC标志

偏置线程 ID、偏置时间戳、对象分代年龄

01

可能有偏见

第二种类型是类型指针,即对象指向其类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例

实例数据部分是对象实际存储的有效信息,即程序代码中定义的各类字段的内容,存储顺序会受到虚拟机分配策略参数(-XX:e参数)的影响)以及Java源代码中字段的定义顺序。

默认的分配顺序是longs/, ints, /chars, bytes/, oops( , OOPs),相同宽度的字段总是分配在一起存储。 除此之外,父类中定义的变量会出现在子类之前,如果+XX:参数值为true,则允许子类中较窄的变量插入到父类变量的间隙中。

对齐填充不一定存在,也没有特殊含义,它仅充当占位符。 任何对象的大小必须是 8 字节的整数倍。 如果对象实例的数据部分没有对齐,则需要通过对齐填充来填充。

3.3. 对象访问位置

主流的访问方式是使用句柄和直接指针

使用句柄——可能会在Java堆中分配一块内存作为句柄池,里面存储了对象的句柄地址,句柄中包含了对象实例数据和类型数据的具体地址信息

使用直接指针——Java堆中对象的内存布局必须考虑如何放置要访问的数据类型的信息。 对象地址直接存储在Java堆中。 如果只访问对象本身,则不需要间接访问开销。

两种比较方法:

访问优势

使用手柄

存储的地址是一个稳定的句柄。 对象移动时,只会改变句柄中的实例数据指针,不需要修改

使用直接指针

速度更快,节省了指针定位的时间开销

注:虚拟机主要使用直接指针进行对象访问

3.4. 实战:异常

一是通过代码验证各个运行时区域存储的内容,二是根据异常提示信息快速知道是哪个区域内存溢出,什么样的代码可能会导致这些区域内存溢出,以及如何处理。来处理异常

3.4.1. Java堆溢出

不断创建对象,并保证GC Roots和对象之间有可达路径,以避免垃圾收集机制清除这些对象,那么随着对象数量的增加,当总容量达到最大堆容量限制时,就会出现内存溢出异常发生

解决方案

首先,使用内存映像分析工具来分析Dump中的堆转储快照。 第一步是确认内存中导致OOM的对象是否是必要的,并区分是否存在内存泄漏(Leak)或内存溢出()。

对于内存泄漏,通过工具进一步检查从泄漏对象到GC Roots的引用链,找出泄漏对象关联的是什么样的引用路径以及哪些GC Roots,这样垃圾收集器就无法回收它们。 根据泄漏对象的类型信息和GC Roots引用链的信息可以定位到该对象被创建的位置,进而找出产生内存泄漏的代码位置。

内存溢出,检查Java虚拟机的堆参数(-Xmx和-Xms)设置,与机器的内存进行比较,是否还有向上调整的空间,然后检查代码中是否有某些对象存在内存溢出问题。生命周期长、保持状态时间过长、存储结构设计不合理等。

3.4.2. 虚拟机堆栈和本机方法堆栈溢出

虚拟机栈和本地方法栈有两个例外:

线程请求的栈深度大于虚拟机允许的最大深度,会抛出异常。 虚拟机的堆栈内存允许动态扩展。 当扩展容量无法申请到足够的内存时,会抛出异常

不同版本的Java虚拟机和不同的操作系统对最小堆栈容量都有限制,这主要取决于操作系统的内存分页大小。

线程过多引起的内存溢出,可以通过减少线程数或更换64位虚拟机或减少最大堆、栈容量来换取更多线程。 从JDK7开始,会提示out of 或 /

3.4.3. 方法区和运行时常量池溢出

JDK6运行时常量池溢出时,异常后面的提示信息为“空格”,运行时常量池属于方法区

JDK8使用元空间而不是永久代。 默认情况下,不会发生此异常。 提供一些参数作为元空间防御措施

-XX::设置元空间的最大值,默认为-1,即没有限制,或者只受本地内存大小限制

-XX::指定元空间的初始空间大小,以字节为单位。 当达到这个值时,就会触发垃圾收集进行类型卸载,收集器会调整这个值:如果释放大量空间,就会适当减小这个值; 如果释放的空间很少,请适当增加该值,但不要超过-XX:

-XX:o:控制垃圾回收后最小元空间剩余容量的百分比,可以减少因元空间不足而进行垃圾回收的频率

-XX:Max-:用于控制元空间最大剩余容量的百分比

3.4.4. 本机直接内存溢出

直接内存容量的大小可以通过-XX:参数指定,默认与Java堆的最大值(-Xmx指定)一致

当内存溢出时,Heap Dump文件中不会看到明显的异常。 发现内存溢出后生成的Dump文件较小,程序直接或间接使用(如间接使用NIO)

 
反对 0举报 0 收藏 0 打赏 0评论 0
 
更多>同类资讯
推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报
Powered By DESTOON