前言
Java Virtual Machine(Java虚拟机) |
---|
- 不得不说,想要真正学会一些东西,一定要理解以后,总结下来
- 尤其是像JVM这种非常基础又核心的东西,总结后写下来,才能有更深的理解
本文是我的JVM的学习笔记,可以当做复习和参考资料,如果想要真正学会,还是要去看书哦!我学习JVM的书籍如下 |
---|
- 深入理解Java虚拟机:JVM高级特性与最佳实践(看完爆杀面试官)
- 深入剖析Java虚拟机:源码剖析与实例详解(基础卷)(HotSpot的源码,让面试官眼前一亮)
- 深入解析Java编译器:源码剖析与实例详解(开始修仙)
一、Java虚拟机有哪些,未来发展,编译自己的JDK
1. Java虚拟机有哪些
2. Java技术的未来
3. 编译自己的OpenJDK
- 进入openJDK中国,下载源码:https://jdk.java.net/java-se-ri/11
- 完成后,解压
二、Java内存和内存溢出
1. 运行时数据区
- 方法区(保存方法信息)和堆(保存创建的对象)属于所有线程
- 虚拟机栈(每个线程都有一个栈空间),本地方法栈(C、C++的方法),程序计数器(记录当前线程执行到哪一行JVM指令),属于线程隔离的数据区,每个线程都有自己的。
1.1 程序计数器(Program Counter Register)
- 较小的内存空间,相当于
当前线程所执行字节码的行号指示器 ,字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令 - 是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- Java虚拟机的多线程通过线程轮流切换、分配处理器执行时间方式来实现,为了线程切换后能恢复到正确执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,可以说这类内存是"线程私有"内存
- 如果执行的是Java方法,记录正在执行的虚拟机字节码指令的地址
- 如果执行本地(Native)方法,计数器的值应该为空(Undefined)。
1.2 Java虚拟机栈(Java Virtual Machine Stack)
- 线程私有,生命周期与线程相同,是
Java方法执行的线程内存模型 ,每个方法被执行时,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息 。 - 每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 我们平常说的栈,通常就是指这个虚拟机栈,或者是指虚拟机栈中局部变量表部分
- 《Java虚拟机规范中》对虚拟机栈这个内存区域规定了两类异常状况
- 如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够内存会抛出OutOfMemoryError异常。
- 存放编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,不是对象本身,可能是指向对象起始地址的引用指针,或者是指向一个代表对象的句柄或者其它与此对象相关的位置)以及returnAddress类型(指向一条字节码指令地址)
- 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示,64位长度的long和double类型数据占用两个变量槽,企业数据类型占用一个。
- 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大局部变量空间是完全确定的。方法运行期间,不会改变局部变量表大小(大小指变量槽的数量,1个变量槽占32、64或更多bit,完全由具体虚拟机自己决定)
1.3 本地方法栈(Native Method Stacks)
- 和虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法(就是字节码)服务,而本地方法栈则
为虚拟机使用到的本地(Native)方法服务 - 《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构没有任何强制规定,虚拟机因此可以根据需求,自由实现它,甚至有些虚拟机(例如Hot-Spot)直接把本地方法栈和虚拟机栈合二为一
- 和虚拟机栈一样,本地方法栈也会在栈深度溢出或栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常
1.4 Java堆
Java虚拟机所管理的内存中最大的一块,所有线程共享,在虚拟机启动时创建,唯一目的就是存放对象实例,Java中几乎所有对象实例都在这里分配 (些许迹象表明,日后可能出现值类型支持,另外即时编译技术的进步,尤其逃逸分析技术,栈上分配,标量替换的进步已经导致一些变化,所以Java对象实例都分配到堆上,不是那么绝对了。)- 《Java虚拟机规范》对堆的描述是“所有的对象实例以及数组都应该在堆上分配”
- 因此它又称“GC堆”,由于现代垃圾回收器大部分采用分代收集理念设计,所以Java堆经常出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空 间”“To Survivor空间”等名词
- 这些区域划分仅仅是一些垃圾回收器共同特性或者设计风格而已,而Java虚拟机固有内存布局可不是这么实现的,以前HotSpot虚拟机的回收器基本都是经典分代理念,但是现在,也出现了不采用分代设计的新垃圾回收器了。
- 线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(TLAB),提升对象分配时的效率,不过无论哪个区域,存储的都只能是对象实例
- Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存
- 《Java虚拟机规范》中规定,Java堆可以处于物理上不连续的内存空间,但是逻辑上应该视为连续
- 但是对于大对象(数组对象等),多数虚拟机为了实现简单、存储高效,很可能要求连续的内存空间
- Java堆既可以被实现成固定大小,也可以是可扩展的,当前主流的Java虚拟机都是按照可扩展来实现(通过参数-Xmx和-Xms设定)
- Java堆中没有内存完成实例分配,并且堆中也无法再扩展时,Java虚拟机将抛出OutOfMemoryError异常。
1.5 方法区(Method Area)
- 各个线程共享,用于存储已被虚拟机加载的类型信息(类的元信息)、常量、静态变量、即时编译器编译后的代码缓存等数据
- 《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但它还有个别名“非堆”,目的是让大家不要把方法区和Java堆混为一谈
- JDK8以前,使用HotSpot虚拟机的程序员,喜欢将方法区称为"永久代",但两者不是等价的,当时HotSpot设计团队把收集器的分代设计扩展到了方法区,或者说用永久代实现了方法区。好处是让HotSpot的垃圾回收器能像管理Java堆一样管理这部分内存。
- 当年使用永久代实现方法区,不是个好主意,导致Java应用更容易遇到内存溢出问题(永久代有-XX:MaxPermSize上限),而且有极少数方法(例如String::intern())会因为永久代的原因而导致不同虚拟机下有不同表现
- 由于永久代导致Java应用更容易内存溢出,所以从JDK6开始,就逐步进行剔除
JDK 6 及以前 ,HotSpot开发团队,有逐步采用本地内存(Native Memory)来实现方法区的计划,但是常量、静态变量、类型信息(类的元信息)依然全部放在永久代 JDK 7 的HotSpot ,将原本放在永久代的字符串常量池、静态变量等移出放进了堆 中,那么既然是线程共用,除了方法区,就剩堆了,所以就放在了堆中JDK 8 ,完全废弃永久代概念 ,改用与JRockit、IBM J9虚拟机一样在本地内存中实现的元空间(Meta-space)替代 ,JDK 7中永久代剩余的内容(主要是类型信息(类的元信息))全部移到元空间
- 约束是非常宽松的,和Java堆一样不需要连续的内存和可以选择固定大小或可扩展,甚至可以选择不实现垃圾回收
- 这区域内存回收目标主要是针对常量池的回收和对类型的卸载,回收效果比较难以令人满意,尤其类型(类元信息)的卸载,条件相当苛刻。但是这部分区域的回收有时候又的确是必要的
- 根据《Java虚拟机规范》的规定,当方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
1.6 方法区- - -运行时常量池(Runtime Constant Pool)
方法区一部分 ,Class文件 中,除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表 (Constant Pool Table),用于存放编译期生成的各种字面量与符号引用 ,这些内容在类加载后存放到方法区运行时常量池 中。- Java虚拟机对Class文件每一部分(包括常量池)的
格式 都有严格规定,但对于运行时常量池的细节 ,《Java虚拟机规范》没有做任何要求,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。 - 具备动态性,不要求常量只有编译期才能产生,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将常量放入池中。开发人员利用此特性较多的是String类的intern()方法。
- 因为是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
1.7 直接内存(Direct Memory)
- 不是Java虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- 此部分就是内存条直接的内存区域,也被频繁的使用,可能导致OutOfMemoryError异常
- JDK1.4中加入的NIO类,引入基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。可以在一些场景中显著提高性能,避免了Java堆和Native堆中来回复制数据
- 本机直接内存的分配不受Java堆大小限制,但是肯定受到计算机总内存(物理内存、SWAP分区或分页文件)大小以及处理器寻址空间的限制,一般服务器管理员调参,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使各个内存区域总和大于物理内存限制(包括物理和操作系统级限制),从而导致动态扩展时出现OutOfMemoryError异常
2. HotSpot虚拟机的对象
2.1 对象的创建
- Java虚拟机遇到一条字节码new指令时,先检查指令的参数能否在常量池中定位到一个类的符号引用
- 如果此符号引用代表的类还没有被加载、解析和初始化,那么先执行相应类加载过程,类加载检查通过后,虚拟机才会为新生对象分配内存
- 对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间,等同于把一块确定大小的内存块从Java堆中划分出来
- 指针碰撞(Bump The Pointer):假设Java堆内存绝对归整,分为左右两边(左:已分配,右:空闲空间),中间由一个指针作为分界点。那么分配时,仅仅是将指针向空闲空间方向挪动一段与对象大小相等的距离。
- 空闲列表(Free List):如果Java堆内存并不归整,已使用和空闲内存相互交错,那么没法简单的进行指针碰撞。此时虚拟机需要维护一个列表,记录哪些内存块可用,分配时从列表中找一块足够大的空间分配给对象实例。最后在列表中记录。
- 选择那种分配方式,由Java堆决定,Java堆是否规整由采用的垃圾回收器是否带有空间压缩整理(Compact)能力决定。
- 使用Serial、ParNew等待压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,简单高效。
- 使用功能CMS这种基于清除(Sweep)算法的收集器时,理论上只能采用较为复杂的空闲列表来分配内存。
- 对象创建在并发情况下不是线程安全的,可能出现,给对象A分配内存,指针还没来得及修改,对象B又使用这个指针分配内存的情况。有两种可选方案
- 方案1:对分配内存空间的动作进行同步处理(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性)
- 方案2:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程需要分配内存,就在哪个线程本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓冲区时才需要同步锁定
- 虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来 设定
赋默认值的过程:这是Java基本类型有默认值的原因 |
---|
- 内存分配完成后,虚拟机需要将分配到的内存空间(不包括对象头)都初始化为零值
- 使用了TLAB的情况下,此步骤会提前到TLAB分配时顺便进行
- 这个步骤保证了对象的实例字段在java代码中可以不赋初始值就可以直接使用。使程序能访问到这些字段的数据类型所对应的零值。
- 赋默认值后,还要对对象进行必要的设置,例如对象是哪个类的实例、如果才能找到类的元信息、对象的哈希(对象的哈希实际会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。
- 这些信息存放在对象的对象头中(Object Header)
- 根据虚拟机当前运行状态的不同,例如是否启用了偏向锁等,对象头会有不同的设置方式
- 在虚拟机视角下,新的对象已经产生,但是站在Java视角下,所有字段都是默认的零值,对象需要的其它资源和状态信息还没有按预定意图构造好,对象创建,才刚刚开始,也就是调用构造函数(Class文件中的< init>()方法)
- 一般来说,new指令之后会接着执行< init>()方法,安装程序员意愿对对象进行初始化,这样一个真正的可用的对象才是完全被构造出来。
- 上面是一般的情况,实际上,由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但是如果直接通过其它方式产生的则不一定如此
2.2 对象的内存布局
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
- 第一类数据“Mark Word”:存储对象自身运行时数据(哈希码、GC分代年龄、锁转态标志、线程持有的锁、偏向线程ID、偏向时间戳),长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit。官方称为Mark Word
- 第二类数据“类型指针”:对象指向它的类型元数据的指针,Java虚拟机通过这个指针确定该对象是哪个类的实例。查找对象的元数据其实也不一定非要经过对象本身(某些虚拟机没有在对象数据上保留类型指针)。此外,如果对象是数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度不确定,将无法通过元数据中的信息推断出数组大小。
- Mark Word是对象头的运行时数据,实际上这类数据很多,已经超出了32、64Bitmap结构能记录的最大限度
- 但是对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,MarkWord被设计成一个有着动态定义的数据结构以便在极小空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
- 例如:32为HotSpot虚拟机中,如果对象没有被同步锁锁定的转态下,Mark Word的32个比特存储空间中存储情况:
- 25个比特用于存储对象哈希码
- 4个比特用于存储对象分代年龄
- 2个比特用于存储锁标志位
- 1个比特固定为0
- 其它状态(轻量级锁定、重量级锁定、GC标记、可偏向)下存储内容如下
- 对象真正存储的有效信息,也就是我们代码中定义的各种类型的字段内容,无论从父类继承下来的,还是子类中定义的字段都必须记录起来。
- 这部分的存储顺序不固定,受虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)字段在Java源码中定义顺序的影响
- 默认分配顺序是longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,普通对象指针OOPs),可见相同宽度的字段总是被分配到一起存放,满足这个前提条件的情况下,父类中定义的变量会出现在子类之前。
- 如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间
- 不是必然存在的,就单纯起占位符的作用,因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是任何对象的大小必须是8字节的整数倍
- 对象头已经被精心设计成正好8字节的倍数(1倍或2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
2.3 对象的访问定位
- Java程序通过栈上的reference数据来操作堆上的具体对象。
- 由于reference类型在《Java虚拟机规范》中只规定它是一个指向对象的引用,没有定义这个引用应该通过什么方式定位、访问到堆中对象的具体位置,因此访问方式,也会由虚拟机实现而定
- 主流的访问方式有使用句柄和直接指针两种
- 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
- 使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。如果只是访问对象本身,就不需要多一次间接访问的开销了。
- 只能说各有优势吧,使用句柄来访问的好处是reference中存储的是稳定的句柄地址,对象被移动时(垃圾回收时移动对象经常发生)只会改变句柄中的实例数据指针,而reference本身不用改变
- 使用直接指针的好处就是速度更快一点,节省了一次指针定位的时间开销
- HotSpot主要使用的是第二种方式直接指针,例外的情况是,如果使用了Shenandoah回收器的话,也会有一次额外的转发。
- 整体来看,各种语言、框架使用句柄来访问的情况也很常见。
3. 了解OutOfMemoryError异常发生的情况
3.1 Java堆溢出
- 堆用来存储实例对象,只要不断创建对象,保证GC Roots到对象之间有可达路径(只要可达,就可以避免被垃圾回收器清除),随着对象数量增加,总容量触及最大堆的容量限制后就会产生内存溢出异常
- 出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”
- 常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析
- 首先应确认内存中导致OOM的对象是否是必要的
- 到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
- 指定运行参数-Xms20m -Mmx20m -XX:+HeapDumpOnOutOfMemoryError
- 编写代码,运行,看结果
3.2 虚拟机栈和本地方法栈溢出
- HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,此对于HotSpot来说,-Xoss参数(设置 本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定
- 虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常
- 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常
- HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的。只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常
3.3 方法区和运行时常量池溢出
- 运行时常量池是方法区的一部分,HotSpot从JDK 7开始逐步“去永久代”的计划,JDK 8中完全使用元空间来代替永久代,使用“永久代”还是“元空间”来实现方法区,对程序有什么 实际的影响么?
- String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加 到常量池中,并且返回此String对象的引用
JDK1.6,因为使用永久代实现方法区,会发生永久代溢出 |
---|
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
- 可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,可间接限制其中常量池的容量
- 运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息 是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK 6的HotSpot虚拟机中的永久代)的 一部分
JDK1.7字符串常量和静态变量都放在堆中,所以会发生堆溢出 |
---|
- 无论是在JDK 7中继续使 用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同 样限制在6MB,也都不会重现JDK 6中的溢出异常,循环将一直进行下去,永不停歇
- 是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版 本,限制方法区的容量对该测试用例来说是毫无意义的。这时候使用-Xmx参数限制最大堆到6MB就能看到效果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.lang.Integer.toString(Integer.java:440)
at java.base/java.lang.String.valueOf(String.java:3058)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.HashMap.resize(HashMap.java:699)
at java.base/java.util.HashMap.putVal(HashMap.java:658)
at java.base/java.util.HashMap.put(HashMap.java:607)
at java.base/java.util.HashSet.add(HashSet.java:220)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)
- 方法区的主要职责是用于存放类型的相关信息,如类 名、访问修饰符、常量池、字段描述、方法描述等。对于这部分区域的测试,基本的思路是运行时产 生大量的类去填满方法区,直到溢出为止
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
- 在JDK 6中运行,会得到两个false
- 在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池 中存储
- 返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false
- 在JDK 7中运行,会得到一个true和一个false
- 而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个
- 而对str2比较返回false,这是因为“java”这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true
- 在JDK 8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。在默认设置下,正常的动态创建新类型已经很难再迫使虚拟机产生方法区的溢出异常了。不过为了让使用者有预防实际应用里出现的破坏性的操作,HotSpot还是提供了一 些参数作为元空间的防御措施,主要包括
- -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存 大小
- -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集 进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放 了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值
- -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可 减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最 大的元空间剩余容量的百分比
3.4 本机直接内存溢出
- 直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定
- 如果不 去指定,则默认与Java堆最大值(由-Xmx指定)一致
- 反射获取Unsafe实例进行内存分配,真正申请分配内存的方法是Unsafe::allocateMemory()。
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
- 直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常 情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了
|