【JVM】第一章 Java内存区域与内存溢出异常

1.1运行时数据区域

JVM在执行Java程序的过程中,把它所管理的内存划分为若干个不同的数据区域。
在这里插入图片描述

在这里插入图片描述

  • 1.2.1 程序计数器PC:当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等都依赖PC寄存器。

  • 🔣如果线程执行的是Java方法:PC寄存器记录指令地址。

  • 🔣如果线程执行的是Native方法:PC寄存器值为空。

  • 1.2.2 Java虚拟机栈:生命周期==线程。Java方法执行时,JVM同步创建栈帧用于存储局部变量表、操作数栈、动态连接、方法出口、返回地址等信息。方法执行完,栈帧从JVM栈中弹出。在这里插入图片描述

  • 局部变量表:存放JVM基本数据类型、对象引用、returnAddress类型。

  • 基本数据类型:在局部变量表中的存储空间以局部变量槽表示(long double占2个,其余栈占1个)。

  • 异常状况

  • 1.线程请求的栈的深度>JVM允许的深度,抛出StackOverflowError异常。

  • 2.JVM栈扩展时无法申请到足够的内存抛出OutOfMemoryError(简称:OOM)异常。

  • 1.2.3 本地方法栈:执行本地方法。异常状况和JVM栈相同。

  • 1.2.4 Java堆:在jvm启动的时候就被创建,它的内存大小也就被确认好了。虚拟机里面内存最大的一块。“几乎”所有对象实例都在这里分配内存。

  • 为什么是“几乎”:由于编译技术的进步,尤其是逃逸分析技术、栈上分配、标量替换优化手段的出现,也就不是那么绝对了。

  • -Xmx 最大堆内存

  • -Xms 最小堆内存

  • Java堆是垃圾收集器管理(GC堆)的内存区域。堆的划分如图所示:img

  • 将堆进行划分的目的:更好地回收内存、更快地分配内存。

  • Java堆是可以物理上不连续的,但逻辑上它是应该连续的。(大对象 例如:数组 。可能物理、逻辑都连续)。

  • 异常状况:Java堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OOM异常。

  • 1.2.5 方法区:方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。主要存储加载的是类字节码、class / method / field 等元数据、static final常量、static变量、即时编译器(执行引擎)编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。所以,方法区看作是一块独立与java堆的内存空间。img

  • 异常状况:如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

  • 1.2.6 运行时常量池(动态性):方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等,还有常量池表。

  • 异常状况:当常量池无法在申请到内存时,将抛出OOM异常。

  • 1.2.7 直接内存:并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也会被频繁地使用,而且也会导致OOM异常。

  • 异常状况:内存区域总和>物理内存限制,导致动态扩展时,抛出OOM异常。

1.2 HotSpot虚拟机对象探秘

1.2.1 对象的创建

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,虚拟机将为新生对象分配内存

🔣“指针碰撞”:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针的向空闲空间方向挪动一段与对象大小相等的距离

🔣“空闲列表”:如果Java堆中内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又有所采用的垃圾收集器是否带有空间压缩整理的能力决定。

除如何划分可用空间外,还需要注意一个问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

解决方案:

1.对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性。

2.把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲 (简称:TLAB),哪个线程要分配内存,就在哪个线程的TLAB中分配,只有TLAB用完了,分配新的缓存区,才需要同步锁定。

内存分配完成后,虚拟机必须将分配到的内存空间(无对象头)都初始化为0值。如果使用了TLAB,这一工作可以提前至TLAB分配时顺利进行。这步操作保证了对象的实例字段在Java代码中可以不被赋值就直接使用,使程序能访问到这些字段的数据类型所对应的0值。

从虚拟机角度来看,此时一个新的对象已经产生了。但是从Java程序的角度来看,对象创建才刚刚开始——构造函数,即Class文件的()方法还没有执行,所有的字段为默认的0值。new指令之后会接着执行()方法,这样一个真正可用的才算完全被构造出来。

1.2.2 对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局划分三个部分:对象头、实例数据、对其填充。

对象头部分:

1.存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

2.类型指针,即对象指向它的类型元数据的指针。查找对象的元数据信息并不一定要经过对象本身。(如果对象是一个Java数组,对象头中必须有一块用于记录数组长度的数据)

**实例数据部分:**对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是子类中定义的字段都必须记录起来。

对其填充部分:并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。

1.2.3 对象的访问定位

对象的访问方式是由虚拟机实现而定的,主流访问方式:使用句柄、直接指针。

🔣“使用句柄”:Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址。句柄:包含对象实例数据与类型数据各自具体的地址信息。在这里插入图片描述

🔣“直接指针”:Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。在这里插入图片描述

各自优势:

句柄访问:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

直接访问:速度更快

1.3 实战OutOfMemoryError异常

1.3.1 Java堆溢出:Java堆用于存储对象实例,我们只要不断创建地创建对象,并且保证GC Roots(现代虚拟机基本都是采用可达性分析算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root )到对象间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常

Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常。Java堆内存溢出,异常堆栈信息“java.long.OutOfMemoryError”会跟随进一步提示“Java heap space”。

常规处理方法:首先通过内存映像分析工具堆Dump出来的堆转储快照进行分析。第一步应确认内存中导致OOM的对象是否是必要的,也要分析清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

IF(内存泄露) 进一步通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的引用路径、与哪些GC Roots相关联才导致垃圾收集器无法回收他们。

IF(内存溢出) 内存中的对象确实都是必须存活的,那么应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置。

1.3.2 虚拟机栈和本地方法栈溢出

1)如果线程请求的栈深度>虚拟机所允许的最大深度,抛出StackOverflowError异常

2)如果虚拟机的栈允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常(HotSpot虚拟机不支持扩展)。

实验结果表明:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError。可是如果允许动态扩展栈容量大小的虚拟机上,相同的代码会导致不一样的情况。

出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。如果建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过“减少内存”的手段来解决内存溢出的方式,一般比较难以想到。

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

Srting::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的子串添加到常量池中,并返回此String对象的引用。

在JDK6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,可以通过修改限制永久代的大小,从而间接限制常量池的容量。运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen Space”,说明运行时常量池的确属于方法区。

在JDK7或者更高版本的JDK就不会得到异常信息。

在这里插入图片描述
在这里插入图片描述

在JDK6中会出现两个false,而更高版本的JDK则会一个显示true,一个为false。

产生差异原因:

JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以不然不可能是同一个引用,结果将返回false。

JDK7或更高版本的JDK的intern()方法实现就不需要再复制字符串的实例到永久代了,,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为“java”这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。

方法区溢出异常:一个类如果要被垃圾收集器回收,要达到的条件比较苛刻。在经常运行生成大量动态类的应用场景里,就应该特别关注这些类的回收情况。这类场景常见的有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为java类)、基于OSGi的应用(即使时同一个类文件,被不同的加载器加载也会视为不同的类)等。

在JDK8以后,永久代就退出了历史的舞台,元空间作为替代者上场。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难迫使虚拟机产生方法区的溢出异常了。

1.3.4 本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值一致。

代码越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK10将Unsafe的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存时也会抛出内存溢出异常,但它跑出异常时并没有真正向操作系统申请分配内存,而是通过计算机得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafr::allocateMemory()。

在这里插入图片描述
在这里插入图片描述

由直接内存导致的内存溢出,一个明显的特征时在Heap Dump文件中不会看见有什么明显的异常情况。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

凇:)

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

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

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

打赏作者

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

抵扣说明:

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

余额充值