Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
运行时数据区域
关于虚拟机在执行java程序时,会把它所管理的内存划分为若干个不同的区域,如图所示:执行引擎负责解释命令,提交操作系统执行 引擎负责解释命令,提交操作系统执行。
类装载器负责加载Class文件,能否运行由执行引擎决定。
今天我们只讲运行时的数据区域部分的内容。
- 程序计数器
程序计数器是一块较小的内存空间,可以把它看成当前线程所执行的字节码的行号指示器。在java虚拟机的概念模型中,字节码解释器的作用就是通过修改这个计数器的行号来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、切换等基础功能都是依赖这个计数器完成的。
我们知道java虚拟机的多线程是通过线程轮流切换和分配处理器执行时间来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了在切换线程后,能够恢复到线程正确的位置,每个线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,所以程序计数器的所在内存是线程私有的。
如果线程正在执行一个java方法,那么这个计数器记录的就是正在执行的java虚拟机字节码的地址。如果是一个native方法,则为null。这个区域是唯一一个在java虚拟机规范中没有规定任何内存溢出情况的区域。
- 虚拟机栈(描述Java方法执行的内存模型)
与程序计数器一样,虚拟机栈也是线程私有的。它的生命周期与线程的生命周期一样。虚拟机栈描述的是java方法执行的内存模型,每个java方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程就是虚拟机栈入栈到出栈的过程。
虚拟机栈的返回函数有两种方式:
1.正常return返回
2.抛出异常
两种都会导致栈帧被抛出
经常有人把java内存空间分为堆内存和栈内存,这样的区分方法是不准确的,这能说明绝大多数程序员比较关注的是与对象内存分配相关的比较密切的内存区域是这两块,实际上应该远比这个要复杂。这里所指的"栈",就是虚拟机中的局部变量表。局部变量表存放了基础数据类型、对象引用数据类型和returenAddress(返回的是虚拟机字节码的地址)。
在基础数据类型中,long和double的数据会占用两个局部变量空间,其余的数据类型都只占一个。局部变量表所需的内存空间在编译的时候就已经确定下来了,当进入一个方法时,这个方法在栈帧中需要多大的内存空间是完全确定的,在方法运行期间不会改变局部变量的大小。
//栈深度:栈的高度称为栈的深度,栈深度受栈帧大小影响。
//由此可以看出,局部变量表内容越多,栈帧越大,栈深度越小。
//知道了栈深度,该怎么用呢?对JVM调优有什么用呢?
//当JVM我们定义的方法参数和局部变量过多,字节过大,考虑到可能会导致栈深度多小,可能使程序出现错误。
//这个时候就需要手动的增加栈的深度,避免出错。
public class Test{
private int count = 0;
public void testAdd(){
count ++;
testAdd();
}
public void test(){
try{
testAdd();
}catch(Throwable e){
System.out.println(e);
System.out.println("栈深度:"+count);
}
}
public static void main(String [] args){
new Test().test();
}
}
运行程序,可以看到栈深度:
栈深度:11114
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
java主要的保存内容为栈帧,栈帧又主要分为以下几个部分:
1、局部变量表:保存函数参数及局部变量的区域。
2、操作数栈:是一个初始状态为空的桶式结构栈,在方法执行中,会有各种指令往栈中写入和提取信息。保存计算的中间结果和作为计算过程中变量的临时空间。
3.动态链接(帧数据区):每个栈帧都包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态链接。
4.方法返回地址。
3 本地方法栈
本地方法栈与虚拟机栈是类似的,区别就是本地方法栈是为虚拟机执行java方法服务的,而本地方法栈是则为虚拟机中使用到的native方法服务。在虚拟机规范中,对本地方法所使用的语言、使用方式等没有强制的规定,因此虚拟机可以自由使用它。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
4 Java堆(内存最大,存放对象实例)
java堆是java虚拟机所管理的内存最大的一块,同时也是对所有java线程共享的一块区域。在虚拟机启动的时候创建。
此区域存在的唯一目的就是存放对象实例,几乎所有的实例对象都存放在这块区域。在java虚拟机规范中描述的是:所有的对象实例及数组都要存放在堆上。但是因为随着JIT编译器的发展与逃逸分析技术的成熟,所有的对象分配渐渐变得不是那么绝对了。
java堆是垃圾回收器管理的主要区域,因此也成为"GC"堆,从内存回收的角度来看,由于现在的垃圾回收器基本都采用分代收集算法,因此java堆还可以细分为新生代和老年代,再细致的还有Eden空间和Survivor空间等。从内存分配的角度来看,线程共享的java堆还可以分出多个线程私有的分配缓冲区。不过不管怎么划分,存储的都是对象实例,这些都是为了更好的管理对象实例。
还有虚拟机的规范是说,java堆可以处于物理机上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以设置固定大小,也可以是可拓展的,不过当前主流的都是通过可拓展的(-Xmx和-Xms来控制)。如果在堆中没有完成内存分配并且堆也不可拓展,会抛出OutOfMemoryError异常。
5.方法区(存储已经被加载的类、常量,静态等信息)
类加载子系统负责从文件系统或网络中加载class的信息,加载的信息就会存放在方法区的内存空间。方法区与java堆内存一样是被各个线程共享的一块区域,它用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等数据。虚拟机规范把方法区描述为堆的逻辑部分,但是它有个别名叫“非堆”。
虚拟机规范堆方法区的限制非常宽松,除了可以和碓一样不需要连续的内存和可以选择固定大小或可拓展的内存之外,还可以设置不实现垃圾回收。
这块区域的内存回收主要是针对常量池的回收和类型的卸载。
在jdk1.6、1.7中,方法区可以理解为永久区,。可以使用-XX:PermSize和-XX:MaxPermSize指定,默认情况下,最大为64M,一个大永久区可以保存更多的类信息,但是如果是运行时会产生大量的类,那么这样,就需要设置一个合理的大小,避免内存溢出异常。
总结: 堆+栈+方法区三者之间的关系
在jdk1.8之后,方法区已经被加入到元数据空间去了。
- 运行时常量池
运行时常量池是方法区的一部分,在Class文件中,除了有类信息、版本、接口等参数还有一个就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池进行存放。
java虚拟机对于Class文件的每一个部分都有非常难严格的规定,每个字节用于存放那种数据必须符合规范的要求才会被虚拟机认可、装载和运行。单对于执行运行时常量池,没有做任何细节的要求。
运行时常量池相对于Class文件中的常量池的另一个特征就是具备动态性,什么是动态性呢?就是java语言并不要求常量只有编译期才能产生,也就是并非在Class文件中的常量池才能加入方法区中的运行时常量池,运行期间也可以将新的常量放入运行时常量池。用的比较多的例如: String类的intern方法。
运行时常量池是方法区的部分,当无法再申请内存时,会抛出OutOfMemoryError异常。
7.直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现.
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
正常情况直接内存会快于堆内存。
怎么配置: 直接内存访问效率高于堆内存,但是申请空间的速度远远低于堆内存。
所以,适合申请次数较少,访问比较频繁的场景。
来自《深入理解JVM虚拟机》JVM高级特性与最佳实现。