JVM 系列文章目录
第一篇:内存区域与内存异常
第二篇:对象揭秘与堆内存分配策略
第三篇:垃圾回收
第四篇:类加载机制
第五篇:性能优化(上)
目录
一、JVM是什么?
JVM是Java Virtual Machine(Java虚拟机)的缩写,是一种规范,正如名字一样,它是一个虚构出来的计算机。java程序有个很重要的特性就是可以跨平台,正是通过JVM来实现的,你甚至可以理解为它是一个程序和平台解耦的中间件。
执行流程:
二、内存区域
1. HotSpot VM 与 JRockit VM
不同JVM版本内存区域也存在着差异,介绍运行时数据区域之前先提一嘴虚拟机的两个巨头。
- HotSpot VM:最初由一家小公司设计,后来被Sun公司收购,从JDK1.3开始成为了默认虚拟机。
- JRockit VM:由BEA公司发行,被誉为世界上最快的JVM。
而Sun和BEA先后被Oracle公司收购,这样Oracle就拥有了两款非常优秀的虚拟机,于是便宣布在JDK8中完成两款虚拟机的整合,这也直接导致了JDK8和JDK7内存区域的差异化。
2. 运行时数据区
程序计数器(pc寄存器):
可以看做是当前线程所执行的字节码的行号指示器,Java本身就是多线程的,这就意味着可以确保多线程情况下的线程切换程序正常执行。此内存区域是JVM中唯一不会出现OOM的区域。
虚拟机栈:
存储当前线程运行方法所需的数据,指令、返回地址。虚拟机栈描述的是Java方法执行的内存模型,每个方法执行是都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应这一个栈帧从入栈到出栈的过程。可通过-Xss来设置栈内存大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;如果虚拟机栈动态扩展申请不到足够的内存,将抛出OutOfMemoryError异常。
- 局部变量表:存放编译期可知的各种基本数据类型和对象引用。
- 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。
写段简单的代码:
public class JavaStackTest {
public static void sub(int a) {
a = a - 1;
}
}
javap -v 反编译成字节码文件。
- 动态连接:主要服务一个方法需要调用其他方法的场景(多态)。
- 返回地址:对应当前栈帧出栈的过程,在当前栈侦出栈后,就需要恢复上层方法栈侦里的局部变量表,操作数栈,此时会将出栈的栈侦的返回值压入上层方法的操作数栈中,将方法返回地址设置进pc寄存器,以便让执行引擎继续执行下去。如果方法执行过程中遇到异常,则不给上层方法调用者返回值。
本地方法栈:
本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法。
堆:
存放对象实例,是JVM所管理内存最大的一块区域,也是GC的主要区域,可通过-Xms或-XX:InitialHeapSize设置初始堆内存,默认大小是电脑物理内存大小的1/64,-Xmx或-XX:MaxHeapSize设置最大堆内存,默认大小是电脑物理内存大小的1/4。
- 堆内存分为年轻代(Young Generation)和老年代(Old Generation),可通过-Xmn来设置年轻代内存大小,或者-XX:NewRatio来设置老年代和年轻代比例,默认值是2,也就是新生代比老年代等于1比2。
- 年轻代又分为Eden和Survivor区,Survivor区由两个一样的From区和To区组成,我们一般称为S0区和S1区。可通过-XX:SurvivorRatio来设置Eden区和Survivor区比例,默认值是8,也就是Eden区比S0区比S1区等于8比1比1。
- 初始化的对象会先放到Eden区,Eden满了会触发YGC,存活对象放入S0或者S1区(复制算法,后续会细讲),每经过一次YGC,Survivor区的对象年龄加1,达到晋升年龄的最大值或者容量不足时,放入老年代,老年代满了会触发FGC。可通过-XX:TargetSurvivorRatio设置Survivor区使用率,-XX:MaxTenuringThreshold设置晋升年龄的最大值。
方法区:
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。说到这里,很多人对方法区、永久代和元空间的区别产生疑问,我来简单介绍下:方法区是JVM的一种规范,类似于我们的接口,而永久代和元空间是java7和java8的不同实现。前文提过HotSpot VM与JRockit VM两款虚拟机,Oracle在Java8中将两款虚拟机整合,而JRockit VM压根就没有永久代,所以Java8中直接移除了永久代用元空间来替代。那么他们具体有什么区别呢?
-
存储位置:永久代是在 Java 堆中的一个特殊区域(和老年代相连),而元空间是在本地内存中的。
-
大小调整:永久代的大小是有限制的,并且必须在启动时指定,字符串常量池存在永久代中,容易出现性能问题和内存溢出。而元空间可以根据需要自动调整大小。
-
垃圾收集:永久代使用 Java 堆的垃圾收集器进行垃圾回收,会触发FGC,而且回收效率低,而元空间使用本地内存的垃圾收集器。
三、内存异常
1. Java堆溢出
Java堆用于存储对象实例,只要不断创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制来清楚这些对象,当达到最大堆容量后就会发生内存溢出异常,java.long.OutOfMemoryError: Java heap space。
要解决这个区域的异常,一般手段是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,重点确认内存中的对象是否必要,来分清到底出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄露,可进一步通过工具(Java自带的VisualVM就很好用)查看对象到 GC Roots 的引用链。找到泄露对象是通过怎样的路径与 GC Roots 关联并导致垃圾收集器无法回收的。掌握了泄露对象的类型信息及 GC Roots 引用链的信息,就可以比较精准的定位出泄露代码的位置。
如果不存在泄露,也就是说内存中的存活对象是正常的,那就检查下堆信息的配置(-Xms和-Xmx),适当调大堆内存,再从代码检查,优化对象生命周期以减少内存消耗。
2. 虚拟机栈和本地方法栈溢出
关于虚拟机栈和本地方法栈,Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。
- 如果虚拟机栈动态扩展申请不到足够的内存,将抛出OutOfMemoryError异常。
怎么理解呢?减少栈内存容量、定义大量的局部变量(增加栈帧大小)或者死递归(增加栈帧数量),栈帧入栈时栈空间不足,就会抛出StackOverFlowError异常;把栈内存设的很大,这样每新增一个栈都会申请很大的内存,而内存是有限的,当新增一个栈申请不到内存时就会抛出OutOfMemoryError异常。
3. 方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,所以会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError。
- java7中永久代抛OutOfMemoryError: PermGen space异常。
- java8中元空间抛OutOfMemoryError: Metaspace异常。
java7中方法区内存溢出很常见,因为一个类被垃圾收集回收掉,判定条件比较苛刻。在经常动态生成大量class文件的应用中,需特别注意类的回收情况。比如大量使用CJLib动态代理或大量使用JSP(JSP第一次运行需编译为Java类)等。
4. 本机直接内存溢出
由DirectMemory导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果 OOM 后 Dump 文件很小,而程序中又直接或间接使用了 NIO,那就很可能是这方面的原因了。