JVM调优基础--Java内存区域与内存溢出异常
为什么学习JVM?
初出茅庐的我本以为会了点Java基础、Thread、Socket、web等就自命不凡!总觉得Java及其附属知识也还好,不难嘛!但是你是否有感受到很多的不解,当你看到异常报错时你真的了解这些错误信息吗?你真的洞察到问题的本源吗?你真的都融汇贯通了吗?(灵魂三问)由此可见,在学习知识的时候最要不得知其然而不知其所以然。把握根源方可看破红尘,削发而傲视群雄。我在早期视频学习配上近期阅读《深入理解Java虚拟机——JVM高级特性与最佳实践》(推荐大家阅读)的基础上,先入手JVM系列博客,希望将个人对JVM的理解记录下来,供你我他赐教!
JVM发展史
- Sun Classic / Exact VM:世界上第一款商用java虚拟机,Sun Classic由于效率问题现已被淘汰。Exact VM是对Sun Classic的改进,具备现代高性能JVM的雏形,但不久被优秀的HotSpot所取代。
- Sun HotSpot VM:是Sun JDK 和OpenJDK中所使用的虚拟机,也是目前使用范围最广的java虚拟机。之后的JVM的介绍也将主要围绕HotSpot介绍展开。
- Sun Mobile-Embedded VM / Meta-Circular VM:sun公司面对移动和嵌入式市场发布的虚拟机。
- BEA JRockit / IBM J9 VM:BEA和IBM公司研发的虚拟机,与HotSpot都称为高性能Java虚拟机。
- AZul VM / BEA Liquid VM:特定硬件平台专有的虚拟机。
- Apache Harmony / Google Android Dalvik VM:不是Java虚拟机,只是虚拟机。Dalvik是Google公司转为Android操作系统设计的虚拟机。
Java内存区域知多少
JVM所管理的内存包括一下几个运行时数据区域。
程序计数器(确定执行位置)
程序计数器:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。也就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,关系到运行时程序执行的确定位置。注意的是程序计数器是线程私有的,也就是在多线程下,每个线程的执行位置相互独立,当进行线程切换后,通过找到当前线程下的程序计数器来确定当前线程的执行位置好继续执行,是唯一一个没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈(存储局部变量表、操作数栈、动态链接、方法出口…)
Java虚拟机栈:是线程私有的,他的生命周期与线程相同。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。Java虚拟机栈用于存储局部变量表、操作数栈、动态链接、方法出口等信息。人们经常说的的栈内存指的是Java虚拟机栈中局部变量表部分(基本数据类型+对象应用)。存在两种异常:StackOverflowError异常(栈深度问题) 、OutOfMemoryError(扩展时内存不足)。
本地方法栈(与Java虚拟机栈功能相似,作用对象不同)
本地方法栈:为虚拟机使用到的Native方法(本地方法,非Java方法)服务。同样会抛出两种异常:StackOverflowError、OutOfMemoryError。
Java堆(几乎为所有对象实例分配内存)
Java堆:是JVM管理内存中最大的一块,几乎所有的对象实例在这里分配内存,并且是线程共享的。Java堆也是垃圾收集器的主要管理区域,因此Java堆又称为“GC堆”(Garbage Collected Heap)。从内存回收的角度将Java堆细分为:新生代(Eden区、From Survivor区、To Survivor区)和老年代,这种按代分区的原因将在JVM调优---垃圾收集器与内存分配策略介绍。当堆无法再扩展时抛出OutOfMemoryerror异常。
方法区(存储JVM加载的类信息、常量、静态变量、即时编译后的代码等数据)
方法区:是各个线程共享的内存区域。方法区用于存储JVM加载的类信息、常量、静态变量、即时编译后的代码等数据。在HotSpot中又称永久代(为了想管理Java堆一样管理方法区,事实证明不可行,只是当初HotSpot设计者想偷懒罢了!),这个区域的内存回收目标主要时针对常量池的回收和对类型的卸载,但是在JDK1.7及之后的HotSpot中已经将字符串常量池从方法区中移出了。当方法区无法再扩展的时候抛出OutOfMemoryError异常。
HotSpot中永久代拓展知识 <——点击即可
运行时常量池(存储字面量和符号引用、直接引用等)
运行时常量池:是方法区的一部分,在Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,它用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。直接引用也存储在运行时常量池中。运行时常量池具有动态性,并非非要经过Class文件转换到常量池中,可以直接在运行期将新常量放入池中,String类的intern()方法就满足这种特性。当运行时常量池无法再申请到内存时抛出OutOfMemoryError异常。
直接内存(本机内存)
直接内存:不是JVM运行时数据区的一部分,直接内存是在JDK1.4中新加入的NIO类(基于通道和缓冲区的I/O方式)可以以使用Native函数库直接分配堆外内存。在物理内存的限制下会抛出OutOfMemoryError异常。
以上就是运行时数据区域的大轮廓介绍!各部分区域的细分将在之后的博文中展开。
HotSpot VM 对象探秘
对象的创建
对象的创建:HotSpot VM遇到一条new 类型()指令时,①首先在常量池中是否有这个类的符号引用(符号引用代表类是否已被加载、解析和初始化过);②如果没有检测到此类的符号引用就必须先执行相应的类加载过程;3 类加载通过后从Java堆中分配确定大小的内存(分配内存的过程是一个并发进行的过程);④内存分配完成后,将分配到的内存空间初始化为零值(对象头不初始化--对象头在下面介绍);5 虚拟机对对象进行必要的设置,并将这些设置信息存放在对象头中;⑥此时就得到了从JVM视角看到的对象;⑦从java程序的角度来看对象才刚刚创建---<init>方法还没有执行,所有的字段都还为0,执行init方法后真正的可用对象产生。流程如下图:
对象的内存布局
对象内存布局:HotSpot VM中对象内存布局可分为3块区域:对象头、实例数据、对齐填补。各区域作用见下图:
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象,reference类型在Java虚拟机规范中之规定了一个指向对象的引用,而应用应该怎样去定位、访问堆中的对象的具体位置就需要用到对象访问定位方式。
对象访问定位:目前主流访问方式有使用句柄和直接指针两种。句柄---从Java堆中划分一块内存作为句柄池,refernece中存储对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图一所示;直接指针---reference中直接存储堆中对象地址,如图二所示,其访问速度更快,节省指针定位的时间开销,HotSpot VM就是采用直接指针方式。
OutOfMemory解析
根据报错信息确定出是那个区域发生OutOfMemory异常!
OutOfMemory异常原因
OutOfMemory异常原因:根据报错信息确定出是那个区域发生OutOfMemory异常(运行结果如下图:以栈为例),然后分析是内存泄露还是内存溢出。
内存泄漏:与GC Roots相关联并导致GC无法自动回收。(GC Roots将在后续播客中介绍)
内存溢出:众多对象确实还必须活着,导致大量内存被占用而无法GC,当超出限制内存最大值时就抛出OutOfMemory异常。
StackOverflowError异常原因
StackOverflowError异常原因:线程请求的栈深度大于虚拟机所允许的最大深度,就会抛出StackOverflowError异常。
问题解答
在运行时常量池中提到过String类中的intern()方法,请判断如下代码返回值是?
提示:在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用;在JDK1.7及以上版本中,intern()方法只是在常量池中记录首次出现的实例引用(个人理解为:JDK1.7中将字符串常量池移出方法区造成的);StringBuffer创建的字符串实例在Java堆上。
答案:JDK1.6----false false ; JDK1.7?----true false;
原因:“java”字符串本就在字符串常量池中存在其引用,不符合首次出现的条件,故为false。
JVM系列博客持续更新,欢迎关注!
----谢谢阅读 知飞翀