java程序运行时jvm内存分配

来源:互联网 发布:澳洲好还是加拿大 知乎 编辑:程序博客网 时间:2024/05/05 10:15

概述

众所周知,在内存管理方面,对于从事C,C++的开发人员来说,他们是内存管理方面的“上帝”,负责着每一个对象生命开始到结束,这样一定程度上给程序员增加了很多麻烦(每个new操作都要写相应的delete/free代码),而对于java程序员来说,虚拟机提供内存管理机制,不容易出现内存泄漏和内存溢出问题(但是带了的缺点就是一旦出现问题,如果不了解虚拟机内存分配将很难定位错误)。接下来说说程序运行时jvm内存的各个区域划分以及这些区域的作用、服务对象和可能产生的问题。

运行时数据区域

jvm在执行程序时会把它所管理的内存划分为若干不同数据区域。这些区域有各自的用途、创建时间和销毁时间。这几个区域分别为:程序计数器、虚拟机栈、本地方法栈、方法区和堆。
如图:
jvm内存分配
其中淡黄色区域为线程共享的,橘黄色区域为线程私有的。
那么这些区域的作用是什么呢?

1. 程序计数器

程序计数器(Program Counter Register)是一块很小的内存空间,它是当前线程所执行的字节码的行号指示器“指哪打哪”)。
jvnm的多线程是通过线程轮流切换并分配处理器执行时间(时间片)的方式来实现的,在任何一个时刻,一个处理器(对于多核处理器来说是一个内核)都会执行一天线程中的指令,为了确保线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的程序计数器独立存储,它们是线程私有的内存区域
如果线程正在执行一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器的值为Undefined。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemroryError情况的区域。

2. 虚拟机栈

虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,他的生命周期于线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时会在虚拟机栈中创建一个栈帧(Stack Frame),栈帧用于存储局部变量、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的操作就对应一个栈帧在虚拟机中入栈到出栈的过程。
说到栈帧,有人会疑惑,栈帧和虚拟机栈是什么关系呢?

每个任务(进程)对应有一个虚拟机栈,在这个进程(线程)中每个函数(方法)被调用时分别从这个栈占用一段区域,称为帧。简而言之,虚拟机栈是相对于某个进程而言的,栈帧则是相对于某个函数(方法)而言的。虚拟机栈中包含栈帧,每个栈帧对应于一个未完成运行的函数。

局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference)和returnAddress类型。其中64位长度的long和double类型数据会占用两个局部变量空间(Slot),其余数据类型占用一个。局部变量表所需要的内存空间在编译期完成分配,方法运行时不会改变局部变量表的大小。关于栈帧结构的详细说明大家可以参考http://www.360doc.com/content/14/0925/13/1073512_412236522.shtml
如果线程请求的栈深度大于虚拟机所允许的深度,则会抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存时,就会抛出OutOfMemoryError异常。

3. 本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用类似,区别就是虚拟机栈为虚拟机执行java方法(即字节码)服务,而本地方法栈则为虚拟机使到的native方法服务。有趣的是,我们常用的HotSpot虚拟机直接把本地方法栈河虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError。

4. 堆

java堆(Java Heap)是jvm管理的内存中最大的一块。java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例,所有的对象实例和数组都要在堆上分配。(但是随着JIT编译器的发展与逃逸分枝技术的成熟,栈上分配、标量替换优化技术使对象分配不是那么“绝对”了)。
java堆是GC管理的主要区域,因此很多时候我们也叫其为GC堆(Garbage Collected Heap)。从内存回收角度来看,现在收集器基本都采用分代收集算法,因此java堆中还可细分为:新生代和老年代;从内存分配角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)。但是,无论什么情况,存储的都是对象实例。进一步划分只是为了更好的回收内存。
java堆可以处于物理上不连续的内存空间中,逻辑上连续即可(使用逻辑地址很容易实现)。如果堆中没有内存完成实例分配,而且堆也无法拓展时(主流虚拟机都是按照可拓展来实现),将会抛出OutOfMemoryError异常。

5. 方法区

方法区(Method Area)与java堆一样,是线程共享的内存区域,方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。相对于堆来说,垃圾收集行为在这个区域比较少,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载(卸载条件非常苛刻)。那么常量池是什么呢?

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池这个概念是针对Java class文件而言的。当java代码被编译成字节码(class)文件时,会将字面量(文本字符串,声明为final的常量值)、符号引用存放在class文件的常量池中。这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池(Runtime Constant Pool)是方法区的一部分。运行时常量池就是class文件被加载(load)到JVM之后,常量池存放的内存区,该内存区属于Heap区。
运行时常量池相对于class文件常量池的另外一个特征是具备动态性,java语言并不要求常量一定只有在编译期才能产生,也就是说并非预置入calss文件常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。如String的intern()方法。

如下面的代码,当string对象s调用intern()时,如果运行时常量池中有s的值,则直接返回该字符串在运行时常量池的地址;如果不存在s的值,则将s的地址(引用)存放在运行时常量池,并返回s的地址(引用)。

public class Test(){    public static void main(args[]){    String s = new String("123");    s.intern();    }}

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

补充:直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但这部分内存也被频繁使用,也可能会导致OutOfMemoryError异常。
在JDK1.4中加入了NIO类,引入了基于通道(Channal)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免了在java堆和Native堆中来回复制数据,提高了性能。
直接内存的分配不会受到java堆大小的限制(受本机总内存制约)。在配置虚拟机参数时,应考虑到直接内存的使用,防止内存区域总和大于物理内存限制,从而防止OutOfMemoryError异常。

本文介绍了java程序运行时jvm内存分配的情况,参考《深入理解java虚拟机》

1 0
原创粉丝点击