JVM的内存模型是java语言绕不开的一个话题。要进行java的性能调优,首先就要了解其内存模型。在诸多的面试笔试中,这也是很多面试官会考察的内容。
本篇文章简单介绍JVM内存模型的概念,结构和对应的参数设置,并根据具体的代码案例讲解一下内存分配情况。
1.JVM内存结构
由图可以较为清楚的看到,JVM的内存空间分为3大部分,分别是堆内存、方法区和栈内存。其中栈内存可以再细分为java虚拟机栈和本地方法栈。堆内存可以划分为新生代和老年代。新生代中还可以再次划分为Eden区、From Survivor区和To Survivor区。划分出来的各个区,分别保存不同的数据,并且在一般情况下,其采取的内存回收策略也是不同的。
2.JVM内存区域功能
通过上图可以看到JVM运行时的数据区:
堆内存是JVM内存模型中最大的一块区域,被所有线程共享,是在JVM启动时候进行创建的。几乎所有的对象的空间分配都是在堆内存上进行分配的。
考虑到JVM的内存回收机制,堆内存可以划分为新生代和老年代两个区域(默认新生代与老年代的空间大小为1:2)。新生代可以再划分为Eden区、From Survivor区和To Survivor区(三者比例为8:1:1)。几乎所有的新对象的创建都是在Eden区进行的。在垃圾回收(GC)过程中,Eden中的活跃对象会被转移到Survivor区,当再到达一定的年龄(经历过的Minor GC的次数),会被转移到老年代中。
堆可以处于物理上不连续的内存空间中,但是需要满足逻辑上的连续。在实现时,可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的
方法区又被成为永久代(HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区),同样也是被所有的线程共享的。该区域中主要保存类的信息、静态变量、常量和即时编译器编译后的代码等数据。
JDK1.7中,已经把放在永久代的字符串常量池移到堆中。JDK1.8撤销永久代,引入元空间。
方法区不需要连续的内存,可以选择固定大小或者可扩展。并且还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
当执行java方法时候,计数器中保存的是字节码文件的行号;当执行Native方法时,计数器的值为空。
java栈是线程私有的内存区域,其中存储的是栈帧。对于一个java的方法,其开始调用,则会创建一个栈帧,保存到java栈中;当该方法执行完成,则对应的是出栈的过程。
如果方法methodOne方法调用了methodTwo,那么methodOne就会先入栈创建一个栈桢,接着methodTwo再入栈成为栈顶(假设没有其他的方法执行),methodTwo执行完先出栈,接着methodOne执行完出栈。
每个栈帧中,保存执行对应方法所必须的信息,主要包含局部变量表,操作栈,动态连接和方法出口等信息。
局部变量表中,可以存放的数据有8种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型。其中long和double因为是64位,会占用两个局部变量的空间。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(比如递归调用的时候),将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈也是线程私有的内存区域,与java栈比较相似,不同之处在于该区域主要是保存Native方法相关的数据。Native方法是非Java语言编写的方法。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
3.JVM内存参数设置
http://blog.csdn.net/liyantianmin/article/details/50704434