java的内存区域

虚拟机在java程序运行的过程中,会把它所管理的内存划分成为若干个不同的数据区。这些不同的数据区的作用、创建和销毁的时候也是不同的,有的区域随着虚拟机的进程的启动和存在,有的区域则依赖于用户线程的启动和结束而建立和销毁。按照java虚拟机SE7版本的规定,java虚拟机内存区域主要包含如下图所示的几个数据区域。

       

java虚拟机运行时数据区

1.程序计数器

程序计数器是是一块由每个线程独有的一块较小的内存空间,是独立存储的,线程与线程之间的计数器是互不影响的。当一个线程正在执行一个java方法时,这个线程的程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是native方法,那这个计数器就不记录任何数据,计数器的值是为空的。另外,该内存区域是也在java虚拟机规范中唯一一个没有规定任何OutOfMemoryError(内存溢出)情况的区域。这个应该很好理解,因为这块区域中,除了虚拟机字节码的地址,不会记录其他的东西,所以一般也不会发生内存溢出的问题。

2.虚拟机栈

和程序计数器一样,虚拟机栈也是由每个线程独有的内存空间,它的生命周期和线程是一致的。虚拟机栈是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用来存储方法执行过程中的变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈和出栈的过程。如果下图所示。

方法在虚拟机中调用的过程

局部表量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用类型(reference类型,它不是对象本身,可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者与此对象相关的一个位置)和returnAddress类型(指向了一条字节码指令的地址,我感觉可能是对应下一个帧的方法字节码地址)。

另外,在这个区域中java虚拟机规定了两种异常状况:StackOverflowError(栈溢出异常)和OutOfMemoryError(内存溢出异常)。

  • 栈溢出:当线程请求的栈深度大鱼虚拟机所允许的深度时,抛出。
  • 内存溢出:如果虚拟机支持动态扩展虚拟机栈(当前大部分虚拟机都可以动态扩展,不过也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时,就会抛出内存溢出。

3.本地方法栈

本地方法栈和虚拟机栈所发挥的作用是 一样的,他们之间的区别是虚拟机栈执行的是java代码,而本地方法栈是为虚拟机执行Native方法而服务的。在虚拟机规范中,对于本地方法中使用的语言、使用方式与数据结构么有强制规定,虚拟机可以根据自己的需要自行实现它。例如,对于我们熟悉的Sun的HotSpot虚拟机,它直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也有栈溢出和内存溢出。

4.堆

对于日常应用中,java堆通常都是Java虚拟机所管理中所占内存最大的一块内存区域。这块内存区域是被所有的线程所共享的,它的生命周期伴随着java虚拟机的启动和关闭而创建和消亡。按照java虚拟机的规范的描述,在这个区域内是存放所有的java对象实例的地方。实际的虚拟机实现上,这个区域也是存放了几乎所有的java对象实例。值得注意的是,随着技术的发展,特别是JIT技术(Just-In-Time Compiler:即时编译技术,它是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。)的发展,所有的对象都在堆内存上分配也不是那么的绝对了。

而我们常常所说的Java虚拟机的垃圾回收机制通常就是针对这块区域的,所以这段区域也通常被亲切地叫做GC堆。现在GC堆回收算法基本都是采用的分带回收的算法,这样做的目的是为了提高垃圾回收的效率,把java对象实例按照可能的生命的周期分配在不同的堆空间,也就是不同的代中,这样就可以根据不同的代而采用不同的回收算法,从而提高GC的效率。具体怎么分代,不同的虚拟机可以有不同的实现,简单可以分为新生代和老年代,再详细点还可以分为:Eden代、From Suivive和To Survive等等。

另外,在Java中我们常用的Thread Local(Tread Local Allocation Buffer,TLAB)线程私有内存区域并不是在线程私有的虚拟机栈或者是本地方法栈中。实际上,Thread Local也是在堆内存中分配的,只是从对内存中划分出来部分区域作为线程私有的。所以,堆内存所有的对象都是线程共享的,起码从这点来看不是那么的绝对。

Java对内存区域是逻辑上连续的存储空间,物理上可以不连续。在虚拟机实现时,堆的大小可以是固定的,也可以是可扩展的,现在基本上主流的虚拟机的堆大小都是可扩展的(可以通过-Xmx:JVM初始内存和-Xms:JVM最大的内存),当堆中没有内存完成实例分配,而且堆也无法再扩展时,将会抛出OutOfMemoryError。

5.方法区

方法区和java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息 、常量、静态变量、即时编译器编译后的代码等数据。虽然java虚拟机规范把方法去描述为堆的一个逻辑部分,但是其实它还有另一个别名,叫做Non-Heap,目的应该是与java堆区分开。方法区和堆一样,可以有物理上不连续但是逻辑上连续的内存空间,可以选择固定的内存大小,也可以选择扩展内存,另外,它还可以不实现垃圾回收。跟堆一样,当方法去无法满足内存分配的需求时,就会抛出OutOfMemoryError异常。

运行常量池是方法去的一部分,Class文件中除了有类的版本、字段、方法、接口等信息描述外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。java并不要求常量一定是编译期才会产生的,也就是并非预置在Class文件中的常量池才会进入到运行时常量池中,运行期间也可以产生新的常量放入常量池,这种特性常用的就是String类的intern()方法。

6.直接内存

直接内存并不是虚拟机运行时内存的一部分,当然也不是java虚拟机规范中定义的内存区域。但是这部分区域在java应用中也是会被频繁使用的部分,也会导致OutOfMemoryError。在JDK1.4中引入了NIO类,它是一种基于通道(Channel)和缓冲区(Buffer)的IO方式,它可以使用Native函数库在堆外分配内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。显然,这种方式的直接内存跟java堆内存没有直接的关系,所以它的内存空间的大小并不会收到堆内存大小的限制。当然,它还是会收到本机的总内存以及处理器寻址空间的限制。如果在配置JVM运行时参数时忽略了这部分内存,极有可能发生运行时各个内存区域大小的总和大于物理内存的大小而导致动态扩展时发生OutOfMemoryError异常。

总结:Java虚拟机中大致分为线程私有和线程共享等两大内存区域。其中线程私有的内存区域分为:虚拟机栈、本地方法栈和程序计数器;线程共享的区域可分为:方法区和堆(严格上来说,方法区也是属于堆内存的一部分,这里稍微区分开了);另外还有一个不属于上述内存区域的java直接内存,是在堆之外独立分配的内存空间,其大小不受堆内存的限制。每个内存区域的大小、作用和声明周期各有不同。GC机制即垃圾回收主要发生在堆内存区域,堆内存区域在GC时候按照划分的不同区域(即常说的分代)使用不同的垃圾回收的算法来提高垃圾回收的效率。

猜你喜欢

转载自my.oschina.net/u/1757225/blog/1550944
今日推荐