本文已收录 【修炼内功】跃迁之路
不论作技术仍是作业务,对于Java开发人员来说,理解JVM各类原理的重要性没必要再多言java
对于C/C++而言,能够轻易地操做任意地址的内存,而对于已申请内存数据的生命周期,又要担负起维护的责任。不知各位在初学C语言时,是否经历过因为内存泄漏致使系统内存不足,又或者由于误操做系统关键内存致使强制关机……segmentfault
对于Java使用者来讲,内存由虚拟机直接管理,不容易出现内存泄漏或内存溢出等问题,将开发人员解放出来,使得更多的精力能够用于具体实现上。也正是所以,一旦出现内存泄漏或溢出问题,若是不了解JVM的内存管理原理,那么将会对问题的排查带来极大的困难。数组
JVM在执行Java程序的过程当中,会将所管理的内存划分为不一样的区域,这些区域各自都有本身的用途、可见性及生命周期,根据《Java虚拟机规范》的规定,JVM所管理的内存包含以下几个区域jvm
程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,用于JVM在解释执行字节码时,存储当前线程执行的字节码行号,每条线程都拥有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储函数
字节码解释器工做时,就是经过改变程序计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常等基础功能都须要依赖计数器来完成测试
若是线程正在执行的是一个Java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址;若是执行的是native方法,则计数器的值为空。此内存区是惟一一个在虚拟机规范中没有规定任何OutOfMemoryError的区域优化
Java堆,是平常工做中最常接触的、也是虚拟机所管理的最大的一块内存区域,其被全部线程共享,在虚拟机启动时建立,此区域惟一的目的就是存放对象实例ui
《深刻理解Java虚拟机》全部的对象实例以及数组都要在堆上分配,可是随着JIT编译器的发展及逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会致使一些微妙的变化发生,全部的对象都分配在对上也逐渐变得不是那么"绝对"了spa
从内存回收角度,Java堆分为新生代和老年代,新生代又分为E(den)空间和S(urvivor)0空间、S(urvivor)1空间操作系统
从内存分配角度,Java堆可能分为多个线程私有的分配缓冲区
若是存在实例未完成堆内存分配,且堆没法再扩展时(经过-Xmx及-Xms控制),将会抛出OutOfMemoryError异常
对于堆上各区域的分配、回收等细节,将在《[JVM] 虚拟机垃圾收集器》系列文章中详述
只要不断建立对象,而且保证GC Roots到对象之间有可达路径来避免GC回收,那么在对象数量达到堆的最大容量限制后就会产生内存溢出异常
/** * VM Args: -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError * * @author manerfan */ public class HeapOOM { static class OOMObject { private int i; private long l; private double d; } public static void main(String[] args) { List<OOMObject> list = new LinkedList<>(); while (true) { list.add(new OOMObject()); } } }
指定堆大小固定为5MB且不能扩展,运行结果
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid71020.hprof ... Heap dump file created [9186606 bytes in 0.069 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at HeapOOM.main(HeapOOM.java:19)
当Java堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟着进一步提示"Java heap space"
对Dump出来的堆转储快照进行分析(如Eclipse Memory Analyzer),能够确认内存中的对象是不是必要的,能够清楚究竟是内存泄漏(Memory Leak)仍是内存溢出(Memory Overflow)
观察堆使用状况,以下图
虚拟机栈也是线程私有的,它的生命周期与线程相同,每一个方法在执行的同时都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息,方法执行时栈帧入栈,方法结束时栈帧出栈
局部变量表存放编译器可知的各类基本数据类型、对象引用及returnAddress类型,局部变量表所需的内存空间在编译期间肯定,运行期间不会再改变,具体的分析会在《[JVM] 虚拟机栈及字节码基础》中介绍
虚拟机栈规定了两种异常:若是线程请求的栈深度大于虚拟机容许的最大栈深度,则会抛出StackOverflow异常;若是虚拟机能够动态扩展栈深度,在扩展时没法申请足够内存,则会抛出OutOfMemoryError异常
可使用递归,无限增长栈的深度
/** * StackSOF * * @author Maner.Fan */ public class StackSOF { private int stackLen = 1; public void stackLeak() { stackLen++; stackLeak(); } public static void main(String[] args) { StackSOF stackSOF = new StackSOF(); try { stackSOF.stackLeak(); } catch (Throwable e) { System.out.println("statck length: " + stackSOF.stackLen); throw e; } } }
运行结果
statck length: 18455 Exception in thread "main" java.lang.StackOverflowError at StackSOF.stackLeak(StackSOF.java:13) at StackSOF.stackLeak(StackSOF.java:13) at StackSOF.stackLeak(StackSOF.java:13) at ...
对于栈空间的OutOfMemoryError,不管是减小最大堆容量、仍是减小最大栈容量、仍是增长局部变量大小、仍是无限建立线程,都没有模拟出栈空间的OutOfMemoryError,却是在堆空间比较小的时候会产生java.lang.OutOfMemoryError: Java heap space
堆异常
环境
java version "1.8.0_212" Java(TM) SE Runtime Environment (build 1.8.0_212-b10) Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode) macOS Mojave 10.14.4 2.2GHz Intel Core i7 16GB 1600 MHZ DDR3
思路
/** * VM Args: -Xms20M -Xmx20M -Xss512K * * @author Maner.Fan */ public class StackOOM { private void dontStop() { long l0 = 0L; long l1 = 1L; long l2 = 2L; long l3 = 3L; long l4 = 4L; long l5 = 5L; long l6 = 6L; long l7 = 7L; long l8 = 8L; long l9 = 9L; long l10 = 10L; long l11 = 11L; long l12 = 12L; long l13 = 13L; long l14 = 14L; long l15 = 15L; long l16 = 16L; long l17 = 17L; long l18 = 18L; long l19 = 19L; while(true) {} } public void stackLeak() { while (true) { new Thread(() -> dontStop()).start(); } } public static void main(String[] args) { StackOOM stackOOM = new StackOOM(); stackOOM.stackLeak(); } }
本地方法栈与虚拟机栈的运行运行机制一致,用于存储每一个Native方法的执行状态,惟一区别在于虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,不少虚拟机直接将本地方法栈与虚拟机栈合二为一
同虚拟机栈同样,本地方法栈也会抛出StackOverflow及OutOfMemoryError异常
在Java7及其以前,虚拟机中存在一块内存区域叫方法区(Method Area),一样为线程共享,其主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,有时候会将该区域称之为永久代(Permanent Generation),但本质上二者并不等价
相对而言,GC行为在这个区域是比较少出现的,但并不是数据进入了方法区就意味着"永久"存在,该区域的GC目标主要是针对常量池的回收及类型的卸载,但这个区域的回收成绩比较难以使人满意,尤为是对类型的卸载
当方法区没法知足内存分配需求时,将抛出OutofmemoryError异常
在Java7中,常量池已经从方法区移到了堆中,到了Java8及以后的版本,方法区已经被永久移除,取而代之的是元空间(Metaspace)
This is part of the JRockit and Hotspot convergence effort. JRockit customers do.
一方面,移除方法区是为了和JRockit进行融合;另外一方面,方法区大小受到-XX: PermSize
和 -XX: MaxPermSize
两个参数的限制,而这两个参数又受到JVM设定的内存大小限制,这就致使在使用过程当中可能出现方法区内存溢出的问题
Metaspace并不在虚拟机内存中,而是使用本地内存,所以Metaspace具体大小理论上取决于系统的可用内存,一样也能够经过参数进行配置(-XX:MetaspaceSize
-XX:MaxMetaspaceSize
)
固然,Metaspace也是有OutOfMemoryError风险的,可是因为Metaspace使用本机内存,所以只要不要代码里面犯过低级的错误,OOM的几率基本是不存在的
因为Java8以后,方法区被永久移除,这里咱们再也不测试方法区(永久代)的内存溢出
最简单的模拟Metaspace内存溢出,咱们只须要无限生成类信息便可,类占据的空间老是会超过Metaspace指定的空间大小的,这里借助Cglib来模拟类的不断加载
/** * VM Args: -XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=16M * * @author Maner.Fan */ public class MetaspaceOOM { public static void main(String[] args) throws InterruptedException { System.out.println("MetaspaceOOM.java"); while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback( (MethodInterceptor)(obj, method, args1, methodProxy) -> methodProxy.invokeSuper(obj, args1) ); enhancer.create(); } } static class OOMObject {} }
运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348) at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492) at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305) at MetaspaceOOM.main(MetaspaceOOM.java:19)
当Java元空间内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"会跟着进一步提示"Metaspace"
观察元空间使用状况,以下图
直接内存并非虚拟机运行时数据区的一部分,最典型的示例即是NIO,其引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,使用Native函数库直接分配堆外内存,经过一个存储在队中的DirectByteBuffer对象做为这块内存的引用进行操做
直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存大小及寻址空间的限制,一旦本机内存不足以分配堆外内存时,一样会抛出OutOfMemoryError异常
对象的建立是为了使用,Java程序执行时须要经过栈上的reference数据来找到堆上的具体对象数据进行操做,目前主流的访问方式有两种:句柄访问、直接指针访问
Java堆中将分配一块内存做为句柄池,栈中的reference存储对象实例句柄的地址
句柄包含两个指针,一个指针记录对象实例的内存地址,另外一个记录对象类型数据的地址
使用句柄的方式访问对象数据,须要进行两次指针定位,但其优势在于,在GC过程当中对象被移动时,只须要修改句柄中对象实例数据指针便可
栈中reference直接存储堆中对象实例数据的内存地址,而对象类型数据的地址存放在对象实例数据中
使用直接指针访问的好处在于访问速度快,其只须要一次指针定位,但在GC过程当中对象被移动时,须要将全部指向该对象实例的reference值修改成移动后的内存地址
参考:
深刻理解Java虚拟机