深入理解Java虚拟机(1)虚拟机内存区域划分 与内存溢出异常

深入理解Java虚拟机(1)虚拟机内存区域划分  内存溢出异常 

   Java虚拟机本身拥有自动内存管理机制,因此Java程序员不需像C/C++那样给每个new对象去delete/free代码,这样不容易出现内存泄露和内存溢出问题。当然,也正是因为Java程序员吧内存控制的权利交给了Java虚拟机,因此一旦出现内存泄露和溢出方面的问题,如果不了解Java虚拟机是怎样使用内存的 ,那么久不容易进行错误排查。

       Java虚拟机在执行Java程序时,会把它管理的内存划分为若干个不同的数据区。这些区域有不同的特性,起不同的作用。它们有各自的创建时间,销毁时间。有的区域随着进程的启动而创建,随着进程结束而销毁,有的则始终贯穿虚拟机整个生命周期。

Java虚拟机所管理的内存包括以下7个运行数据区域:

(1)程序计数器

(2)Java虚拟机栈

(3)本地方法栈

(4)Java

(5)方法区

(6)运行时常量池

(7)直接内存

 

深蓝色区域包裹的部分为运行时几运行个数据区域:

    白色的部分为线程私有的,既随着线程的启动而创建。每个线程都拥有各自的一份内存区域。它们是: JAVA(JAVA STACK),本地方法栈(NATIVE METHOD STACK),和程序计数器(PROGRAM COUNTER REGISTER)。

    黄色部分是线程共享的,所有的线程共享该区域的内容。他们是:方法区(METHOD AREA),(HEAP)。

我们分别来介绍这些区域。

 

(1)程序计数器

       程序计数器(ProgramCounter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码行号指示器。(注:这和计算机处理器中的程序计数器有区别:计算机处理器中的程序计数器是 寄存器 ,即PC寄存器)。Java代码编译成字节码之后,虚拟机就会一行一行的解释字节码,并翻译成本地机器代码。在虚拟机概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理等都依赖这个计数器来完成。

   由于Java虚拟机的多线程时通过现场轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。因此,我们称这列内存区域为线程私有的内存。

   如果线程正在执行一个Java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址。  

  如果正在执行的是Native方法,这个计数器值则为空(Undefined)。

  此内存区域(程序计数器)是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

(2)Java虚拟机栈(Java Virtual Machine Stacks)

      Java虚拟机栈(VM Stack)和程序计数器一样,也是线程私有的,因此它的生命周期也和线程相同。它存放的是Java方法执行时的数据,既描述的是Java方法执行的内存模型:每个方法开始执行的时候,都会创建一个栈帧(Stack Frame)用于储存局部变量表栈操作数动态链接方法出口等信息。每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程

     经常有人把Java内存分为堆内存(Heap)和栈内存(Stack)(实际远比这个复杂),这种是比较粗糙的分法,很大原因是大多数程序员最关注的,与对象内存分配最密切的区域就是堆和栈-----这里所指的就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表存放的是编译器可知的各种基本数据类型(boolean byteintlongcharshortfloatdouble)、对象引用

reference类型)和returnAddress类型(它指向了一条字节码指令的地址)。其中64bit长度的longdouble会占用两个局部变量空间(Slot),其余的数据类型只占用一个(即每个局部变量空间32)。局部变量表所需的内存空间是在编译时期确定的,在方法运行期间不会改变局部变量表的大小。

          Java虚拟机规范中,对这部分区域规定了两种异常:

         1、当一个线程的栈深度大于虚拟机所允许的深度的时候,将会抛出StackOverflowError异常;

         2、如果当创建一个新的线程时无法申请到足够的内存,则会抛出OutOfMemeryError异常。

(3)本地方法栈Native Method Stack

        本地方法栈与虚拟机栈所发挥的作用是非常相似,它们之间的区别不过是虚拟机栈为Java方法服务(也就是为字节码服务),而本地方法栈则为Native方法服务。在虚拟机规范中对本地方法栈中使用的语言以及使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(比如SunHotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

       与虚拟爱栈一样,本地方法栈区域也会抛出StackOverflowError和OutMemoryError异常。

(4)Java  Heap

   对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。

  Java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建,此虚拟机区域的唯一目的就是存放对象实例。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC”(GarageCollected-Heap---哈哈,别翻译成垃圾堆了)。

     从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、To Survivor 空间等。

    从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local AllocationBuffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的仍是对象实例。而进一步更为细致的划分的目的是为了更好地回收内存,或者更快地分配内存。



 Java虚拟机规范中规定:Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,就类似我们的磁盘空间一样。如果堆中不足以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

(5)方法区 Method Area

    方法区和Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机已经规范吧方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的就是把Java堆区别开来。

       对于HotSpot开发者来说,很多人称它为“永久代(Permanent Generation),但是两者并不等价,仅仅是因为HotSpot虚拟机设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以向管理堆一样管理这部分内存。但是因为永久代有“-XX:MaxPermSize的上限,使其更容易内存溢出。因此在JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出去了。

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

(6)运行时常量池RuntimeConstant Pool----------------即:常量池

    运行时常量池是方法分区的一部分。Class类文件中除了有类的版本、字段、方法、接口  等描述信息外,还有一项信息是常量池,用于存放编译器生成各种字面量和符号引用这部分内容将在类加载后进入方法区的运行时常量池中存放。(关于符号引用,可参考 http://blog.csdn.net/helongfu/article/details/47406609)

  运行时常量池相对于Class文件常量池的另外一个重要特征是具有动态性,运行期间也可能有新的常量池放入持重,比如String.intern()方法。

  运行时常量池属于方法区一部分,自然会抛出OutOfMemoryError异常

(7)直接内存Direct Memory

    直接内存不属于虚拟机中定义的 内存区域,而是堆外内存。

    直接内存不属于虚拟机运行时数据区域的一部分,也不属于Java虚拟机规范中定义的内存区域,但这部分内存也被频繁地使用,所以直接内存不足也会导致OutOfMemoryError异常出现。

   JDK1.4 中新加入了NIO(new Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这快内存的引用进行操作。这样能在一些场景中显著提高新能性能,因此避免了在Java堆和Native堆中来回复制数据。

      显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息。但是一直忽略直接内存也会使得各个内存区域总和大于物理内存限制,从而导致动态扩展时抛出OutOfMemoryError异常。

 

 

当然,就像上面所说,大多程序员最关注的就是堆和栈。栈:是每个线程私有的区域。堆:所有线程所共享的区域。

本文内容来自《深入理解Java虚拟机》-周志明著

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值