在Java项目中,内存的分配与回收由Java虚拟机进行管理,为开发者省去了回收内存的工作。不过,当出现内存泄漏和溢出问题的时候,如果不理解虚拟机的内存管理,就会很难发现问题。
Java虚拟机的内存结构如图:
虚拟机内存包括左侧的方法区、堆和右侧的虚拟机栈、本地方法栈和程序计数器。其中左侧的两个区为线程共享区,右侧为线程所私有,随线程的销毁而收回。
程序计数器与操作系统中的程序计数器功能非常相似;本地方法栈与虚拟机栈很相似,不同的是虚拟机栈为Java方法服务,而本地方法栈是为本地方法(Native method)服务的;方法区是用来存储类信息、常量、静态变量等信息的,具体所包含的信息也已经在笔记(1)http://blog.csdn.net/shijing_0214/article/details/50865806中介绍过,所以这里只重点介绍虚拟机栈与Java堆。
1、虚拟机栈
虚拟机栈也就是我们常说的Java栈内存,是Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法返回地址和一些附加信息。这些栈帧保存在虚拟机栈中,栈帧结构如图:
局部变量表
局部变量表用于存放方法参数和方法内部定义的局部变量。虚拟机通过索引定位的方式来使用局部变量表,索引范围从0到最大值。如果虚拟机栈中执行的实例方法(非static方法),那局部变量表中第0位索引是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。接来下存储的是参数列表和方法体内部定义的变量。操作数栈
操作数栈是用来辅助方法执行的,如算术运算和调用方法的参数传递。在方法执行过程中,会有各种字节码指令往操作数栈中写入和读取,例如,整数加法的字节码指令iadd在运行的时候会将操作数栈中栈顶两个元素弹出相加,并将结果压入操作数栈中。动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,通过该引用来实现方法调用过程中的动态连接,大家可以借助Java多态中的重写来理解动态连接的含义。方法返回地址
当方法执行完成或遇到异常退出时,需要返回方法被调用的位置。一般说来,方法正常退出时,会在栈帧中保存PC计数器的值作为返回地址;而异常退出时,返回地址是要通过异常处理器表来确定的。附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,如与调试相关的信息。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
下面举一个简单的例子说明当执行方法getPrice()时的栈帧结构,
public class Fruit{
public void getPrice(double price){};
}
public class Apple{
public void getPrice(double price){
int n=5;
System.out.println(n*price);
}
}
public class SumPrice{
public static void main(String args[]){
Fruit fruit=new Apple();
fruit.getPrice(5.6);
System.out.print("hello,world");
}
}
执行方法getPrice()时栈帧结构如下:
2、Java堆
Java堆是虚拟机管理的内存中最大的一块,在虚拟机启动(即main方法执行时)创建,被内存中运行的线程共享。
Java堆是用来存放对象实例的,几乎所有的对象实例都存放在Java堆上。Java堆从内存回收角度可以分为新生代和老年代,其中新生代又分为Eden区和Survivor区,Survivor区再分为From Survivor和To Survivor,结构如下:
http://incdn1.b0.upaiyun.com/2015/01/0cdfa00b1ed116f15a884cb6b0262df7.png">” title=”” />
在使用新生代时,每次使用Eden和其中一块Survivor区,当回收时候,将这两个区中仍存活的对象复制到另一块Survivor区,然后将这两个区清理掉。
对象实例在创建的时候,首先分配在Eden区上,当Eden区空间不足时,会触发Java虚拟机对Eden区的一次Minor GC,当回收后空间仍满足不了需求时,则将对象创建在老年代中。
3、总结
虚拟机栈和Java堆是Java虚拟机管理的使用最频繁,也是开发者接触最多的两块区域,通过对其了解,出现OutOfMemoryError异常或*Error等内存错误时,可以很快知道错误出现的位置,从而快速排查错误。