JVM——Java内存、HotSpot虚拟机中的对象和OOM异常实战

第2章 JAVA内存区域与内存溢出异常

2.1 motivation

Java程序运行时,由java虚拟机管理内存。
需要知道虚拟机是如何管理内存,才能处理内存泄漏/溢出问题

2.2 运行时数据区

内存分为以下几个数据区:方法区,堆,虚拟机栈,本地方法栈,程序计数器

2.2.1 PC:指示程序控制流

  • PC的值:
    Java方法:PC的值是正在执行的字节码指令的地址
    本地方法:undefined
  • PC是唯一一个在JVM规范中没有规定OutOfMemoryError情况的区域
  • 线程私有:
    多线程同步,保证线程切换不出错,每个线程有自己的PC

2.2.2 虚拟机栈:以栈帧为单位,方法对应栈帧

  • 线程私有(生命周期=线程的生命周期)
  • 执行一个方法->同步创建一个栈帧
  • 方法调用->栈帧入Java虚拟机栈;方法执行完毕->栈帧出栈
  • 栈帧存储:局部变量表、操作数栈、动态连接、方法出口等
  • 局部变量表:
    • 存储编译期间的各种Java虚拟机基本数据类型、对象引用和returnAddress类型(字节码指令地址),数据以局部变量槽(slot)来存储,在编译期间确定局部变量表的大小(slot的数量)
    • 有两种异常:StackOverflowError、OutOfMemoryError

2.2.3 本地方法栈:

为本地方法调用 服务,类似虚拟机栈

2.2.4 Java堆:存放对象实例

  • Java堆由所有线程共享
  • 垃圾收集器就是管理Java堆的
  • 堆空间逻辑连续,可以物理上不连续
  • Java堆的大小既可以固定,也可以扩展
  • 异常:没有足够内存存对象实例,也无法扩展的情况下OOM

2.2.5 方法区:

  • 存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • JDK6之前,用永久代来实现方法区,到JDK8废弃,方法区全部移到本地内存的元空间中。
  • 方法区的垃圾回收:针对常量池的回收和对类型的卸载,条件苛刻,回收效果差
  • 异常:OOM

2.2.6 运行时常量池:

  • 存储位置:方法区
  • 内容:编译期间生成的各种字面量和符号引用(一般还包括直接引用)
  • 特点:动态性(除了 class文件中的常量池表加载的来得来的常量,运行期间产生的新常量也可以存入)
  • 异常:OOM

2.2.7 直接内存:

  • 不是虚拟机运行时数据区的一部分
  • 由Native函数库分配的堆外内存,被堆里的DirectByteBuffer对象引用
  • 一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

2.3 HotSpot虚拟机

2.3.1 对象的创建

• 类加载检查:检查字节码new指令的参数能否在常量池中定位到一个类的符号引用,如果没有需先执行类加载(能确定对象所需内存大小)
• 分配内存:
分配方式由Java堆是否规整(GC是否带有空间压缩整理)决定:
规整->指针碰撞:一个指针作为空闲与已使用内存空间之间的指示器,分配内存只需要指针往空闲空间方向移动对象所需大小的距离
不规整->空闲列表:虚拟机维护一个列表,记录哪些内存块可用

分配内存时的并发线程安全问题:可能两个对象同时修改指针
解决方案:一种是对分配内存空间的动作进行同步处理(实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性);另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
• 将分配到的内存空间都初始化为零值:保证了对象的实例字段 在Java代码中可以不赋初始值就直接使用
• 设置对象头:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息
• 执行构造函数: new指令之后执行’<init> ()'方法,按照程序员的意愿对对象进行初始化

2.3.2 对象的内存布局

• 在HotSpot虚拟机里,对象的内存布局划分为:对象头、实例数据、对齐填充
• 对象头:Mark Word + 类型指针
Mark Word :对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
Java数组对象:对象头中还必须有一块用于记录数组长度的数据,虚拟机根据元数据信息确定数组对象大小
• 实例数据:代码里定义的各字段内容
存储顺序:虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序
• 对齐填充:由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,任何对象的大小都必须是8字节的整数倍

2.3.3 对象的访问定位

• 主流的两种方式:使用句柄、直接指针(HotSpot主要使用的)
在这里插入图片描述
在这里插入图片描述

2.4 实战:OOM异常

2.4.1 Java堆溢出

异常排查:首先Dump出当前的内存转储快照,通过内存映像分析⼯具对堆转储快照进⾏分析,确认导致OOM的对象是否时必要必要的:是则内存溢出,否则内存泄漏

异常处理:

  • 内存泄漏:通过⼯具进⼀步找到GC Roots引⽤链找到泄漏的具体代码位置
  • 内存溢出:检查堆参数(-Xmx和-Xms)设置,对⽐机器内存,检查代码(是否存在某些对象⽣命周期过⻓,持有时间过⻓、存储结构设计不合理等)

2.4.2 虚拟机栈和本地方法栈溢出(-Xss)

1) StackOverflowError --> 线程的栈深度超出,新的栈帧内存⽆法分配
2) OutOfMemoryError --> 支持扩展的情况下,扩展栈容量是⽆法申请到⾜够的nei内存;不支持扩展的情况下,创建线程申请内存时⽆法获得⾜够内存,出现时考虑减少线程数量/更换64微虚拟机/减少最⼤堆/减少栈容量
如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取 更多的线程。

2.4.3 ⽅法区和运⾏时常量池溢出

JDK7及以后,运⾏时常量池从永久代移到元空间
JDK6中intern()⽅法会把⾸次出现的字符串实例复制到永久代的字符串常量池中 存储,返回永久代中这个字符串实例的引⽤;JDK7中intern()⽅法由于字符串常 量池移动⾄堆中,不需要拷⻉字符串实例到永久代,⽽是在常量池中记录其⾸次出现的实例引⽤

方法区溢出场景:运行时生成大量动态类的应用场景: CGLib字节码增强和动态语言、大量JSP或动态产生JSP 文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)

2.4.4 本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不指定,默认与Java堆最大值(由-Xmx指定)一致
由直接内存溢出导致的OOMError明显的特征是在Heap Dump⽂件中不会看⻅有什么明显的异常情况,如果发现内存溢出后产⽣的Dimp⽂件很⼩并且使⽤了直接内存,就需要考虑⼀下这⽅⾯的问题

Reference:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) (华章原创精品) - 周志明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值