java 虚拟机内存模型是java 程序运行的基础。jvm虚拟机将内存数据 分为程序计数器, 虚拟机栈,本地方法栈,java堆 和方法区等部分。
程序计数器用于存放下一条的指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息,java堆用于存放java运行时所需的对象等数据,方法区用于存放程序的类元数据信息。
1. 程序计数器(Program Counter Degister)
程序计数器是一块很小的内存空间。由于java 是支持线程的语言,当线程数量超过CPU的数量时,线程之间根据时间片轮询来抢夺CPU资源。对于单核CPU来说,任一时刻只能运行一个线程,其他线程必须被切换出去。为此,每一个线程必须有一个独立的程序计数器,用于记录下一条运行的指令。各个线程之间的程序计数器互不影响,独立工作,是一块线程私有的内存空间。
如果当前线程正在执行一个java方法,程序计数器记录正在执行的java字节码地址,如果现在执行的是一个native方法,程序计数器为空。
2.java虚拟机栈
java虚拟机栈也是线程私有的内存空间,它和java线程在同一时间创建,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
java虚拟机规范允许java栈的大小是动态的或者固定的。java虚拟机规范中定义了两种异常与栈空间有关:StackOverflowError 和 OutOfMemoryError。
如果线程在计算过程中,请求的栈深度大于可用的最大栈深度,则抛出StackOverflowError。如果java栈可以动态扩展,而在扩展栈的时候没有足够的内存空间
空间来支持扩展,则会抛出OutOfMemoryError。
在HotSpot虚拟机中,可以使用-Xss来设置栈的大小,栈的大小直接决定了函数调用的可达深度。
以下代码展示了一个递归的应用。计数器count记录了递归的层次,此递归没有出口一定可以栈溢出,在栈溢出的时候打印栈深度。
public class TestStack {
private static int count = 0;
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args){
try{
recursion();
}catch(final Throwable e){
System.out.println("deep of stack is "+count);
e.printStackTrace();
}
}
}上述程序打印结果:deep of stack is 12057
java.lang.StackOverflowError
如果想要程序支持更深的栈调用,则可以使用-Xss1M来运行程序。
虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表,操作数栈,动态连接方法和返回地址等信息。
每一个方法的调用都伴随这栈帧的入栈操作,方法的返回则伴随着栈帧的出栈操作。如果方法调用时方法的参数和局部变量相对较多,那么栈帧中局部变量表就会变大
栈帧会膨胀以满足方法调用所需的传递信息。单个方法调用所需的栈的空间大小也会增大。看一下代码结果:
public class TestStack {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e = 0L,f = 0L,d = 0L;
count++;
recursion(a,b,c);
}
public static void main(String[] args){
try{
recursion(1L,2L,3L);
}catch(final Throwable e){
System.out.println("deep of stack is "+count);
e.printStackTrace();
}
}
}结果:deep of stack is 4492
java.lang.StackOverflowError
可以看出,随着调用函数参数和局部变量的增加,单次函数调用对栈空间的需求也会增加。(函数调用次数由12057变为4492)
3. 本地方法栈
本地方法栈和虚拟机栈功能类似,java虚拟机栈用于管理java函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是java实现的,而是C实现的。
4. java堆
java堆是java运行时内存中最重要的部分,几乎所有的对象和数组都是堆中的分配空间中。java堆分为新生代和老年代两个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存的足够长,老年对象就会被移入老年代。
新生代又可进一步细分为eden,survivor space() 和survivor space1()。
为了更方便的理解对象在内存中的分配方式,结合下例。
public class TestHeapGC {
public static void main(String[] args){
byte[] b1 = new byte[1024*1024/2];
byte[] b2 = new byte[1024*1024*8];
b2 = null;
b2 = new byte[1024*1024*8];
System.gc();
}
}
第一次在注释System.gc()。结果:
如果加上System.gc(),结果
可以看出在 full gc 之后,新生代对象被清空,未被回收的对象全部被移入老年代。
5.方法区
方法区也是jvm内存区中一块重要的内存区域,与堆空间类似,它也是被所有线程共享的,方法区主要保存的信息是类的元数据。
方法区中最为重要的是类的类型信息,常量池,域信息,方法信息。
类型信息包括类的完整名称,父类的完整名称,类型修饰符和类型的直接接口类表。常量池包括这个类方法,域等信息所引用的常量信息。
域信息包括域名城,域类型和域修饰符。方法信息包括方法参数,返回类型,方法名称,方法修饰符,方法字节码,操作数栈,方法帧栈的局部变量区大小以及异常表。
方法区是一块永久区,独立于java堆的内存空间,也是可以被GC回收的。
对永久区的GC回收包括两个方面,1.GC对永久区常量池的回收,2.对类元数据的回收。