java开发过程中经常见到一堆有关内存的错误,比如:
Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space
Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
我们统一叫做内存溢出,那么这些异常的底层到底有什么不同,各自属于JVM运行时的哪块区域?该怎么解决呢?想要彻彻底底的弄清楚这些疑惑,了解JVM内存结构就会随之而来。
JVM内存概念
首先,Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行也就是常说的java项目运行过程。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们今天分析的JVM内存。
在知道了JVM内存是什么东西之后,下面我们就开始分析它的组成。
JVM内存组成
JVM内存由程序计数器(Program Counter Register)、Java栈(JVM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)组成。
如上图所示,下面我们逐个分析。
堆-Heap
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。内存分配主要是由程序员显示的使用new关键字来触发的,至于new出来的这部分内存在哪分配,如何分配,则是JAVA虚拟机来决定。而这部分内存的释放,则是由自动内存管理系统(以下简称GC)来管理的。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区-Method Area
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
我们知道堆中存储的是程序员new出来的对象实例,那么方法区存储的是什么?
1.方法区包含所有的class和static变量和常量。
2.方法区中包含类的信息,比如各种方法等。
举个例子:
public class HelloWorld {
private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String message) {
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + ": " + message);
}
}
上述代码运行内存分配如下
对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),Java虚拟机规范对这个区域的限制非常宽松,相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
Java栈(JVM Stack)
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 栈帧:简单点说,可以解释为是一个方法运行时,临时数据的存储区域,具体点说,它里面包括了数据和部分的过程结果,与此同时,它又肩负着处理方法返回值、动态链接以及异常分派的任务。栈帧是随着方法的创建而创建,随着方法的结束而销毁,如果方法抛出异常,也算方法结束。然而在每一个栈帧中,都有着自己的局部变量表以及操作数栈以及对当前类的运行时常量池的引用。
- 局部变量表:它是一个方法局部变量的列表,是在编译时期就写入了class文件当中。简单的理解,可以将它理解为一个对象数组,而里面按照索引0到length-1分别对应于每一个局部变量,特别的,如果是实例方法的局部变量表,第0个局部变量会是一个指向当前实例的引用,也就是this关键字,其余的局部变量则从索引1开始。
- 操作数栈:它是一个后进先出(LIFO)栈,而它的长度也是在编译时期就写入了class文件当中,是固定的。它的作用就是提供字节码指令操作变量计算的空间,比如简单的,对于int a=9这句话来说,就需要先将9压入操作数栈,再将9赋给a这个变量。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stack)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
native方法称为本地方法。在java源程序中以关键字“native”声明,不提供函数体。
其实现使用C/C++语言在另外的文件中编写,编写的规则遵循Java本地接口的规范(简称JNI)。
简而言就是Java中声明的可调用的使用C/C++实现的方法。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
控制参数
最后通过一张图来了解如何通过参数来控制各区域的内存大小
-Xms设置java堆的最小空间
-Xmx设置java堆的最大空间
-XX:NewSize设置新生代最小空间
-XX:MaxNewSize设置新生代最大空间
-XX:PermSize设置永久代(方法区)最小空间
-XX:MaxPermSize设置永久代(方法区)最大空间
-Xss设置每个线程的堆栈大小
老年代(Old Generation)大小:java堆大小-年轻代空间大小
本文中的JVM有关问题,欢迎沟通交流哦。