Java的内存分配(运行时数据区)

    根据《Java虚拟机规范》的规定,运行时数据区通常包括:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。由于JVM规范中没有规定具体如何实现这些区域,所以不同的虚拟机厂商可以有不同的实现方式。

1、程序计数器

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

    多线程情况下,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,互不干扰。因此,程序计数器是线程私有的。

    如果线程执行Java方法,程序计数器中记录正在执行的虚拟机字节码指令的地址;如果线程执行native方法,则程序计数器中的值为空,即undefined。

    由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。程序计数器区域是Java虚拟机中唯一没有定义OutOfMemory异常的区域。

2、虚拟机栈

    Java栈也叫虚拟机栈(Java Vitual Machine Stack),是线程私有的。

    Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。可以说Java栈描述的是Java方法执行的内存模型:每个方法执行时都会创建一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。

    当线程执行一个方法,就会随之创建一个对应的栈帧,并将栈帧压栈;当方法执行完毕之后,便会将栈帧出栈。所以,使用递归方法容易导致栈内存溢出。

    线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。而在活动线程中,也只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。

    编译阶段,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。

    Java虚拟机栈可能出现的两种异常:

        1)线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError;

        2)虚拟机栈空间可以动态扩展,当动态扩展无法申请到足够的空间时,抛出OutOfMemory异常。

2.1 局部变量表

    局部变量表就是我们常说的“栈”,用来存储方法中的局部变量,包括在方法中声明的非静态变量以及函数形参。对于基本数据类型的变量,直接存储它的值;对于引用类型的变量,存储指向对象的引用;还可能存储returnAddress类型(指向一条字节码指令的地址)。

    局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性的说,每个slot可以存放一个boolean、byte、char、short、int、float、reference或returnAddress这8种类型的数据,而这8种类型都可以使用32位甚至更小的物理内存来存放。这种说法允许slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。而对于64位的long和double数据类型,虚拟机会以高位对齐的方式为其分配两个连续的slot空间。

2.2 操作数栈

    操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。操作数栈的最大深度也在编译阶段写入到Code属性的max_stacks数据项中。

    操作数栈的每一个元素可以是任意Java数据类型。32位数据类型所占的栈容量是1,64位数据类型所占的栈容量是2。

    当一个方法开始执行的时候,它的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。

    在概念模型中,两个栈帧是完全相互独立的;但是虚拟机的实际实现中,都会做一下优化,让两个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈和上面栈帧的部分局部变量表重叠,这样在方法调用的时候可以共享一些数据,无需进行额外的参数复制传递。

    栈最典型的一个应用就是用来对表达式求值。一个线程执行方法的过程,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

2.3 动态链接

    每个栈帧都包括一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

    Class文件的常量池中存在大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态引用;另一部分将会在每次运行期间转化为直接引用,成为动态连接。

2.4 方法返回地址

    方法开始执行后,有两种方法可以退出这个方法的执行,通过正常完成出口,以及异常完成出口。方法使用异常完成出口的方式退出,不会返回任何值。

    无论哪种退出方式,方法退出后都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上次方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧很可能会保存这个数据;而方法异常退出时,返回地址要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。

   方法退出过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

2.5 附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范中没有的信息到栈帧,比如调试相关的信息。这部分信息完全取决于具体的虚拟机实现。

    在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归到一类,称为栈帧信息。

3、本地方法栈

    本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈为执行Java代码(字节码)服务,而本地方法栈为执行本地方法(Native Method)服务。

    本地方法栈也会抛出StackOverflowError和OutOfMemory异常。

    在JVM规范中,并没有对本地方法栈的具体实现方法和数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

4、Java堆

    一般来说,堆是Java虚拟机所管理的内存中最大的一块。堆是所有线程共享的一块区域,在虚拟机启动时创建。Java堆唯一目的就是用来存放对象实例。

    堆是Java垃圾收集器管理的主要区域,所以也称为“GC堆”。由于现在的垃圾收集器基本上都是采用分代收集算法,所以Java堆还可细分为:新生代和老生代。再细致一点新生代还可分为Eden空间,From Survivor空间,To Survivor空间。

    如果从内存分配的角度看,线程共享的Java堆可划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,都是用来存放对象实例。细分的目的是为了更好的回收内存或者更快的分配内存。

    Java堆可以是物理上不连续的空间,只要逻辑上连续即可。在实现时,既可以实现成固定大小,又可以是可扩展的,主流的虚拟机都是按照可扩展的方式来实现的。如果当前对中没有内存完成对象实例的创建,并且不能在进行内存扩展,则会抛出OutOfMemory异常。

新生代:

    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

老生代:

    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代:

    持久区不在堆中,属于方法区。用于存放静态文件,如Java类、常量、方法描述等。对持久代的回收主要回收两部分内容:废弃常量和无用的类。

5、方法区

    方法区是JVM中非常重要的区域,和堆一样,也被线程共享。

    在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

5.1 运行时常量池

    运行时常量池是方法区的一部分。在Class文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的各种字面量和符号引用。常量池在类加载后存放到方法区的运行时常量池中。

    JVM虽然对Class文件每一部分都有严格规定,但是对运行时常量池没有任何细节要求,提供商可以自行实现这个内存区域。一般来说,还会把翻译出来的直接引用也存储在运行时常量池中。

创建时机:运行时常量池是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

动态性:运行时常量池相对于Class文件常量池的一个重要特征是动态性。JVM不要求常量只能在编译期产生,也就是说并非预置入Class文件常量池的内容才能进入方法区的运行时常量池中,运行期间也可将新的常量放入常量池中,比如String的intern方法。

5.2 垃圾回收

    JVM规范中,没有强制要求方法区必须实现垃圾回收。HotSpot以永久代来实现方法区,从而GC可以像管理堆区一样管理这部分区域。不过自从JDK7,Hotspot虚拟机便将运行时常量池从永久代移除了。

    由于垃圾收集行为在这个区域是比较少出现,所以常量和静态变量的定义要多注意。当然方法区的内存收集还是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。

    一般来说方法区的内存回收比较难以令人满意。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。

6、直接内存(附加内容)

    直接内存(Direct Memory)不是运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。但是这部分内存也被频繁使用,也可能导致OutOfMemory。

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

    本机的直接内存不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址能力的限制。在配置虚拟机参数时,如果不注意直接内存,就可能导致各个内存区域总和大于物理内存限制,导致动态扩展时出现OutOfMemory。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值