Java内存区域与内存溢出异常

运行时数据区域

在这里插入图片描述

一. 程序计数器

字节码解释器通过改变这个计数器的值来选取下一个需要执行的字节码指令,它是程序控制流的指示器

以下为需依赖计数器来完成:

  • 分支

  • 循环

  • 跳转

  • 异常处理

  • 线程恢复

    👇
    注意
    由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,为了线程切换能恢复都正常的执行位置,每条线程都需要一个独立的计数器,各个计数器之间互不影响,独立存储。我们称这类内存区域为“线程私有”的内存。
    👇
    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空。
    👇
    1. Java方法:Java编写,编译成字节码,存储在class文件中。
    2. 本地方法:其他语言编写,编译成和处理器相关的代码。

二、虚拟机栈

线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型。

📌 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法出口

📌 每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程

三、本地方法栈

与虚拟机栈作用相似,为虚拟机使用到本地方法服务

四、Java堆

是虚拟机所管理的内存中最大的一块

📌 Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建
👇
1. 目的:存放实例化对象,所有的对象实例以及数组都应当在堆上分配
2. Java堆是垃圾收集器管理的内存区域

📌从分配内存的角度上看
👇
1. 所有线程共享的Java堆可以划分出多个线程私有的分配缓冲区
【以提升对象分配的效率】
【将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存】
2.Java堆可以处于物理上不连续地内存空间中,但逻辑上应该被是为连续的

五、方法区

各个线程共享的内存区域

🚩存储

  1. 被虚拟机加载的类型信息
  2. 常量
  3. 静态变量
  4. 即时编译器编译后的代码缓存的数据

🚩运行时常量池

  1. 字面量
  2. 符号引用
〇、直接内存

不是虚拟机运行时数据区的一部分,也不是定义的内存区域,但是会被频繁地使用,而且也可能导致OutOfMemoryError异常错误

📌 本地直接内存地分配不会受到java堆大小的限制,但是还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制


HotSpot虚拟机对象

创建一个对象的过程的详细步骤
  1. 类加载检查

在java中,new一个对象的时候,Java虚拟机会首先检查这个指令的参数能否在常量池中找到这个对象对应的符号引用。检查这个符号引用代表的类是否被类加载器加载、解析和初始化。如果没有则进行类加载

  1. 分配内存

在类加载之后,虚拟机会为将要创建的对象分配内存、对象所需内存的大小在类加载完成便可以完全确定,给对象分配内存时需在java堆中分出一块确定的内存
👇👇👇
在Java堆内存分配一般有两种方式
🚩1 . 指针碰撞:
            在java堆规整的情况下,适合采用指针碰撞的方式,用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,用来将用过的内存与空闲的内存分割开来,当给新生对象分配内存时,指针便会向空闲区域移动。
            使用Serial,PerNew等带压缩整理过程的收集器,系统采用的分配算法是指针碰撞。简单、高效
🚩2 . 空闲列表:
            当Java不规整的情况下,适合采用空闲列表,Java虚拟机会维护一个列表,该列表是记录内存的块是否可用的,当新生对象分配内存的时候,会找一块足够大的内存分配给新生对象,之后更新这个列表。Java堆是否规整有Java虚拟机采用的垃圾回收器是否有压缩整理的功能决定
            使用CMS这种基于清楚算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配对象。

📌 考虑一个问题
            对象创建在虚拟机中是非常频繁的行为,即时仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存时,指针还没来得及修改,对象B又同时使用了原来的指针分配内存的情况。
👇👇👇两种解决方案:
            1.对象分配内存空间的动作进行同步处理
                        采用CAS配上失败重试的方式保证更新操作的原子性
            2.把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区才需要同步锁定。虚拟机是否使用TLAB,可以通过参数来设定
                        TLAB:JVM在内存新生代Eden Space中开辟了一小块线程私有的区域。每个线程都会有自己的缓冲区称作TLAB

  1. 初始化零值

在给新生对象分配完内存之后,虚拟机需要将分配到的内存空间都初始化为零值,这步操作保证了对象的实例字段在Java代码中可以不赋初值就可以直接使用

  1. 设置对象头

初始化零值之后,要对新生对象设置对象头,对象头包含类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,根据虚拟机当前运行的状态不同,对象头也会有不同的设置方式。

  1. 初始化init

在给对象设置完对象头之后,虚拟机已经将一个对象产生了。此时,方法没有执行,对象所有的字段都为零值,零值的对象在程序中没有意义,只有初始化之后,对象才能真正体现出作用

对象的内存布局
  • 对象头

    包含两类信息:
    1、用于存储对象自身的运行时数据:
            哈希码
            GC分代年龄
            锁状态标志
            线程持有的锁
            偏向线程ID
            偏向时间戳
    2、类型指针
            对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例

  • 实例数据

    对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来,这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响

  • 对齐填充

不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotPot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话就是任何对象的大小都必须是8字节的整数倍

对象访问定位
  • 使用句柄

    Java堆中将可能划分出一块内存来作用句柄池,reference中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自具体的地址信息
    👇👇👇
    好处:
            reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄的实例数据指针,而reference本身不需要被修改

  • 直接指针

    Java堆中对象的内存布局必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
    👇👇👇
    好处:
            速度更快了,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本

OutOfMemoryError异常

Java堆异常

Java堆用于存储对象实例,我们只要不断创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制之后就会产生内存溢出异常

🚩处理方法:
通过内存映像分析工具对dump出来的堆转储快照进行分析

  • 首先确定内存OOM对象是否是必要的,也就是分析清楚到底是出现内存泄漏还是内存溢出
  • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的引用路径与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄露对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确定位到这些对象创建的位置,进而找出内存泄露的代码的具体位置
  • 如果不是内存泄露,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数设置,与及其的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构不合理等情况,尽量减少程序运行期的内存消耗。
虚拟机和本地方法溢出

🚩两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

🚩无论是由于栈帧太大还是虚拟机容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机将抛出的都是StackOverFlowError异常

🚩如果建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

方法区和运行时常量池溢出
  • JDK6及以前,常量池分配在永久代中,运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示 信息时"PermGenn space"

  • JDK7以后,常量池放到了Java堆中

  • 方法区的主要职责就是用于存放类型的相关信息

    类型
    范文修饰符
    常量池
    字符描述
    方法描述

  • 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的,应该关注这些类的回收状况

  • 元空间的防御措施

    -XX:MaxMetaSpaceSize
    1、设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小
    2、指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收起进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在2不超过-XX:MaxMetaspaceSize的情况下,适当提高该值
    3、作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。

  • 本地直接内存溢出

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LEO-max

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值