深入理解Java虚拟机 - java内存区域和内存溢出异常

1 概述

自动内存管理是Java区别于C , C++ 的一个重要特性,因为Java程序员把控制内存的权利交给了Java虚拟机,Java程序就不容易出现内存泄露和内存溢出,但是如果出现内存溢出和内存泄漏就不容易排查出错误,了解Java虚拟机的运行有助于错误的查找

2 运行时数据区

Java虚拟机在执行java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域

在这里插入图片描述

2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前执行线程的字节码行号指示器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令

线程私有 : Java虚拟机的多线程是通过线程轮流切换来实现的,在任何一个准确的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此为了线程切换之后能够回到正确的执行位置,每一条线程都需要一个程序计数器

特点 :
1 如果线程执行Java方法,计数器记录的是虚拟机字节码的地址,如果是执行本地方法,计数器的值为空

2 在《Java虚拟机规范》当中,程序计数器所在内存区域,是唯一规定不会出现OutOfMemoryError情况

2.2 Java虚拟机栈

1 线程私有 :
2 生命周期与线程相同 :
虚拟机栈执行的是Java方法的线程内存模型,每一个方法被执行时,Java虚拟机栈都回同步创建一个栈帧用于存储局部变量表操作数栈动态连接方法出口等信息,每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程

局部变量表 :存放了编译期可知的基本数据类型,对象引用,returnAddress类型

这些数据类型在局部变量表的存储空间中以局部变量槽来表示,64位长度的long和double类型咱局两个槽,其他类型占据一个槽,局部变量表的空间在编译期间就完成分配,当进入一个方法时,栈帧中的局部变量表的空间是完全确定的,在方法运行期间不会改变局部变量表的大小(大小 :指槽的数量)

可能出现异常 :
如果线程请求的栈深度大于虚拟机栈所允许的深度抛出StackOverflowError异常如果java虚拟机栈允许动态扩展,当栈扩展时无法申请到足够的空间时,抛出OutOfMemoryError异常

注意 :HotSpot虚拟机的栈容量不允许动态扩展,但是如果第一次申请栈容量时就失败,也会报OOM异常

2.3 本地方法栈

本地方法栈与虚拟机栈的作用十分相似,区别与java虚拟机栈为java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用本地方法服务的

2.4 Java堆

1 Java堆是虚拟机管理的内存当中最大的一块,并且是被所有线程共享
2 此内存区域的唯一目的就是存储对象实例
3 Java堆在最先的虚拟机中存储了所有的对象实例,但是随着逃逸分析技术的日渐强大,栈上分配,标量替换等优化手段,导致一些对象实例并没有存储到堆内

Java堆是垃圾收集器管理的主要区域

从分配内存来看 , 所有线程共享的Java堆可以划分出多个线程私有分配缓冲区,提高对象分配时的效率

当前主流的java虚拟机中,java堆都是可扩展的(通过-Xmx 和 -Xms设定),如果堆中没有内存来完成实例分配并且无法扩展时就会报OOM异常

2.5 方法区

1 线程共享
2 用于存储已经被虚拟机加载类型信息常量静态变量即时编译器编译之后的代码缓存等数据

3 在jdk1.7以前使用永久代来实现方法区,jdk1.8及其之后使用本地内存来实现方法区

异常 :如果方法区无法满足新的内存分配需求,抛出OOM异常

2.6 运行时常量池

1 运行时常量池是属于方法区的一部分
2 Class文件中除了有类的版本字段方法接口信息等描述信息之外,还有常量池表用于存放编译器生成的各种字面量与符号引用和符号引用翻译生成的直接引用,这部分内容将在类加载后存放到方法区的运行时常量池当中

重要特征 :具备动态性
Java语言并没有要求常量一定要编译器才能产生,运行期也可以产生新的常量放入池中

异常 :当常量池无法申请到内存时,抛出OOM异常

2.7 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机》中定义的内存区域,但是这块内存被频繁使用,而且有可能产生OOM异常

NIO中频繁使用直接内存,引入一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过存储在java堆里面的DirectByteBuffer对象来作为这块内存的引用操作

作用 : 显著提高性能 ,因为避免了在Java堆和Native堆之间来回复制数据

异常直接内存的分配并不会受到java堆大小的限制,但是会受到本地总内存大小的限制,如果在分配虚拟机内存时忽略了直接内存导致动态扩展出现OOM异常

3.1 对象的创建(简易版)

1 对象的创建普遍是通过 new 关键字进行创建(还有复制和反序列化)

在虚拟机中,普通对象是如何创建 ?(不包括数组和Class对象)

  1. 当java虚拟机遇见一条字节码new指令时 , 首先检查指令的参数在常量池中能否匹配到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析,初始化,如果没有,先执行类的加载过程(类加载)
  2. 类加载检查通过之后,虚拟机为新生对象分配内存,对象所需内存大小在类加载过程当中就已经确定

分配内存有两种方式 :

指针碰撞
条件 :java堆的内存绝对规整
在这里插入图片描述

指针碰撞 :假设Java堆中的内存是绝对规整的,所有使用过的内存放到一边,空闲的内存放到另一边,中间放置一个指针,分配内存时,只需要将指针向空闲区域移动一段和分配大小相等的距离

空闲列表
条件 :java堆内内存并不规整

在这里插入图片描述
空闲列表 :如果java堆中内存并不规整,已经使用的内存和空闲内存交错在一起,就不能进行指针碰撞,java虚拟机需要维护一个列表,记录那些内存块是可用的,那些是不可用的,在分配内存时,从列表中查找一块足够大的内存分配给对象实例,并更新列表上的记录

选择标准 :看垃圾收集器是否带有空间压缩能力

2 出现并发现象时如何解决?
两种方法 :

  1. 对分配内存空间的动作进行同步处理 (虚拟机采用CAS配上失败重试方法保证更新操作的原子性)
  2. 每一个线程在java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB),当本地线程分配缓存用完后,分配新的缓存区时进行同步锁定,通过-XX:+/-UseTLAB参数来设定
  3. 内存分毕之后,虚拟机将分配到的内存空间初始化为默认值(不包括对象头),如果如果使用了TLAB,可以在分配TLAB时进行
  4. 对象头进行设置(例此对象是那个类的实例 ,对象的哈希吗 , GC年龄 , 是否使用偏向锁 等等 ),完成之后,原始对象就构造完成
  5. 执行init方法 , 对对象进行资源注入

3.2 对象的内存布局

在hotspot 虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头 , 实例数据 , 对齐填充

  1. 对象头包含两类数据 :
    第一类用于存储对象自身运行时数据(对象的哈希吗 , GC年龄 , 是否使用偏向锁 等等)
    第二类 :类型指针 ,即对象指向它的类型数据的指针,明确对象是那个类的实例,如果对象是数组,则对象头中还有一块内存区域必须用来存储记录数组长度的数据

  2. 实例数据 :存储对象真正的有效的数据,定义的各种数据类型字段,父类的也要记录,字段顺序受java源码的影响,(相同字段放在一起,从大到小,父类在子类之前)
    (+XX:CompactFields) 默认为true ,子类较窄的可以插入父类空隙

  3. 对齐填充 :**起着占位符的作用,**虚拟机要求对象其实地址必须是8字节的整数倍,如果大小不够,占位符填充

3.3 对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象
主流访问有两种 :句柄 指针

句柄 :Java堆中会划分出一块内存来作为句柄池,reference中存储对象句柄地址,句柄包含对象实例数据地址类型数据地址

在这里插入图片描述
好处 :当对象被收集器回收之后,,reference数据并不会被改变,只是改变句柄池当中的数据

指针 :Java堆中对象的内存布局需要放置访问类型数据的相关信息,reference中存储对象地址,如果只是访问对象,比句柄少一次访问开销

在这里插入图片描述
好处 :速度开 , 少了一次指针定位的时间开销

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值