Java的内存区域
Java的内存区域
多弗哥 发表于6个月前
Java的内存区域
  • 发表于 6个月前
  • 阅读 0
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

    根据《Java虚拟机规范(第2版)》的规定,Java虚拟机在执行Java程序时会把它所管理的内存划分为若干个不同的数据区域。虚拟机所管理得内存将会包括以下几个运行时数据区域:

1.  程序计数器

    程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以视作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    由于Java虚拟机得多线程是通过线程切换并分配时间片的方式来实现得,在任意一个时间点,一个内核处理器都只会执行一条线程中的指令。因此,为了保证线程切换后能恢复到正确的执行位置,每条线程都会有一个独立的程序计数器,且互不影响。我们称这类内存区域是线程隔离的,为“线程私有”的内存。

    该内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

 

2.  虚拟机栈

    虚拟机栈(Virtual Machine Stacks) 也是线程私有的,它的生命周期与线程相同。

    它描述的是Java方法执行时的内存模型:每个方法在执行同时都会创建一个栈帧(Stack Frame,Java方法运行时的基础数据结构),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈至出栈的过程。我们平常所说的比较粗的“Java内存分为堆内存和栈内存”,其中“栈内存”就是现在讲得虚拟机栈,或者是虚拟机栈中的局部变量表。

    局部变量表存放了编译器可知的各种基本数据类型、对象引用类型(reference)和返回地址类型(returnAddress ①)。局部变量表所需得内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是确定的,且在方法运行期间不会改变。所以,在Java虚拟机规范中,对这个区域规定了两种异常情况:

  • 如果线程请求的栈深度大于虚拟机所允许的最大值(即调用链过长),则抛出StackOverflowError异常 ②;
  • 如果虚拟机栈动态扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常;

 

3.  本地方法栈

    本地方法栈(Native Method Stack) 与虚拟机栈的作用是相似的,它们之间的区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

    在Java虚拟机规范中并没有强制规定本地方法栈所使用的语言、实现方式和数据结构,具体的虚拟机可以自由实现它,事实上,OpenJDK和SunJDK自带的HotSpot虚拟机就是直接将虚拟机栈和本地方法栈合二为一的。

    本地方法栈区域也会抛出StackOverflowError异常和OutOfMemoryError异常。

 

4.  Java堆

    对大多数应用来说,Java堆(Java Heap) 是Java虚拟机管理的最大一块内存。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存 ③。

    Java堆是GC(垃圾收集器)管理的主要区域,所以很多时候也称作“GC堆(Garbage Collected Heap)”。从内存回收的角度来看,由于现在垃圾收集器基本都采用的分代收集算法,所以Java堆可以再分为:新生代、老年代和永久代;新生代还可以再细分为:Eden空间、From Survivor空间、To Survivor空间等。

    从内存分配的角度来看,线程共享的Java堆中可能会划分出多个线程私有的本地线程分配缓冲区(TLAB,Thread Local Allocation Buffer)。

    根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。当前主流的虚拟机都可以按照JVM参数(-Xmx和-Xms)来扩展Java堆,在对象实例分配时,如果在堆中已经没有内存可以完成分配,且堆也无法再进行扩展时,将会抛出OutOfMemoryError异常。

 

5.  方法区

    与Java堆一样,方法区(Method Area) 也是线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器输出的代码等数据。虽然Java虚拟机规范将方法区描述为堆的逻辑部分,但是它却被称为非堆(Non-Heap),目的是与Java堆区分开来。

    很多人都愿意把方法区称为“永久代(PermGen)”,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区GC而已,对于其他虚拟机(比如JRockit,J9等)是不存在永久代这个概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易触碰到可用内存的上限(永久代有-XX:MaxPermSize的上限),而遇到内存溢出的问题。值得注意的是,从JKD1.7开始永久代PermGen逐渐被移除(移出原本放在永久代中的字符串常量池),最新的JDK1.8中已使用元空间(MetaSpace)代替永久代。

    相对而言,垃圾收集行为在这个区域是较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收成绩难以令人满意,尤其是类型的卸载,但是这个区域的回收确实是必要的。在Sun公司的BUG列表中,曾出现过若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

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

 

6.  运行时常量池

    运行时常量池(Runtime Constant Pool) 是方法区的一部分。Class文件中除了类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Constant Pool Table),它用于存放编译期生成的各种字面量、符号引用,final变量值、类名和方法名常量,这部分内容将在类加载后存放到方法区的运行时常量池中。它们以数组形式访问,是调用方法与类联系及类的对象化的桥梁。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

    运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能,也就是说并非预置入Class文件常量池的内容才能进入方法区的运行时常量池,程序运行期间生成的新常量也可放入池中,这种特性用的比较多的是String类的internd()方法。String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。不过JDK7的String#intern()方法的实现有所不同,当常量池中没有该字符串时,不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录堆中首次出现的该字符串的引用,并返回该引用。所以,在JDK1.7之前运行时常量池是方法区的一部分,JDK1.7及之后版本已经将运行时常量池从方法区中移了出来,在Java堆(Java Heap)中开辟了一块内存区域存放运行时常量池。

 

7.  直接内存

    直接内存(Direct memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现。

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

    显然,本机直接内存分配不会受到Java堆大小的限制,但是,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)的大小以及处理器寻址空间的限制,如果各个内存区域总和大于物理内存限制,将会导致动态扩展时遇到OutOfMemoryError异常。

 ∷  总结一下,Java类和对象在运行时的内存里是怎么样的呢

  • 程序运行时,类信息、方法字节码在方法区,对象实例及数组在堆里面;
  • 静态变量在方法区,字符串常量等常量在运行时常量池;
  • 线程调用方法执行时创建栈帧并压栈,方法的参数、局部变量和返回地址在栈帧的局部变量表;
  • 对象的实例变量和对象一起放在堆里,各个线程可以共享访问对象的实例数据;
  • 类信息、常量、静态数据一起放在方法区,各线程可以共享访问;
  • 程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭;

 

注:

    ① returnAddress类型:与那些数值类的原始类型不同,returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。

    ② 一个线程中的方法可能还会调用其他方法,这样就会构成方法调用链。

    ③ 为什么说几乎呢?随着JIT编译器(Just In Time)和内存逃逸分析技术的发展,栈上分配、标量替换优化技术有可能会在栈上分配对象实例。

    ④ 零拷贝(Zero-Copy)机制:它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。在Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()方法、Java NIO中的FileChannel.transferTo()方法、Netty中的FileRegion都实现了零拷贝的功能。

 

 

参考自:

  • 《深入理解Java虚拟机》;
  • 《JVM高级特性与最佳实践》 ;
标签: JVM
共有 人打赏支持
粉丝 6
博文 29
码字总数 106691
×
多弗哥
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: