Java内存管理机制

Java程序由JVM自动管理内存,不需要为new操作去写配对的delete、free代码,不容易出现内存泄露和内存溢出等问题。也正是因为Java把内存管理的权利交给了JVM,一旦出现内存泄露和内存溢出,若要排查相关的问题,必须对Java内存模型以及内存管理机制有所了解。本文将介绍Java内存的每一个部分的服务对象、作用以及可能产生的问题。

Java程序运行时的数据区域

Java虚拟机在运行Java程序时会把管理的内存划分为若干个不同的数据区域,这些区域有各自不同的用途。根据Java虚拟机规范的规定,Java虚拟机管理的内存区域将包括以下几个区域:
这里写图片描述

程序计数器(Program Counter Register)

程序计数器是一块比较小的内存区域,每一个线程都有一个程序计数器,它可以看做是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里,字节码解释器通过改变程序计数器的值,来选择下一条需要执行的指令,分支、循环、跳转、异常处理、线程恢复等都需要程序计数器来完成。
如果线程正在执行的是Java方法,那么程序计数器记录的是正在执行的虚拟机的字节码指令的地址,如果正在执行的是native方法,那么计数器的值为Undefined。

虚拟机栈

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

局部变量表

局部变量表存储了编译期可以预知的各种各种基本数据类型、对象引用、return Address类型(指向了一条字节码指令的地址)。局部变量表所需要的内存空间在编译期完成分配,当一个方法进入时,这个方法所需要的局部变量的空间是确定,在运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机所允许的栈深度,将会抛出StackOverflowError异常,如果虚拟机栈在扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。

本地方法栈(Native Method Stack)

本地方法栈和虚拟机栈所发挥的作用是相似的,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机用到的native方法服务。虚拟机规范中对本地方法所使用的语言、数据结构没有强制规定,各种虚拟机可以自由的实现。甚至有的虚拟机将本地方法栈和虚拟机栈两者合二为一。同样,本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆

Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存的唯一目的就是存放对象,几乎所有的对象都在Java堆中分配内存。
根据Java虚拟机的规定,Java堆可以是物理上不连续的内存空间,只要逻辑上连续即可,在实现时既可以是固定大小的,也可以是可以扩展的。如果在堆中没有内存完成内存分配,并且无法再扩展,将抛出OutOfMemoryError。

方法区

方法区与Java堆一样,也是所有线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等信息。Java虚拟机规范将方法区描述为堆的逻辑单元,但是它也有一个别名叫做“非堆”,目的是与Java堆区分开来。
Java虚拟机规范对于方法区的限制相当放松,除了和Java堆一样不需要连续的内存和可以固定大小或者可以扩展外,还可以选择不实现垃圾收集。根据Java虚拟机规范的约定,当方法区无法满足内存分配的要求时,将抛出OutOfMemoryError异常。

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等信息外,还有一项是常量池,用于存放编译时声称的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一般来说,除了class文件描述的符号引用外,还会把翻译出来的直接引用也存储在常量池中。因此运行时常量池相对于class文件的常量池一个重要特征是动态性,运行时也可以将新的常量加入常量池中。
运行时常量池既然是方法区的一部分,自然也就收到方法区内存的限制,满足内存分配的要求时,将抛出OutOfMemoryError异常。

hotspot虚拟机的对象

hotspot虚拟机是使用最广的虚拟机,以下将基于Java使用最广的hotspot虚拟机来看Java对象的内存分配、布局以及访问过程。

对象的创建

  1. 虚拟机遇到一条new指令时,首先检查能否在常量池找到找到该类的符号引用,并且检查这个符号代表的类是否已经被加载、解析、初始化。若没有先执行类的加载过程。
  2. 为新建对象分配内存,对象所需要的内存大小在类加载的时候就已经确定,为对象分配内存的任务等同于把一块确定大小的内存从Java堆中划分出来。若内存连续,可以使用指针碰撞方式分配内存,若内存空间不连续,则使用空闲列表进行分配。为保证分配内存的安全性,可以对分配内存空间的动作进行同步处理,或者为每一个线程分配该线程私有的本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程需要分配内存,就在哪个本地线程分配缓冲中进行。
  3. 分配内存完成后,虚拟机将需要分配到的内存空间都初始化为零值(不包括对象头)。
  4. 虚拟机对对象进行必要的设置,将对象的基本信息存放在对象头中。
  5. 对对象进行初始化操作。

对象的内存布局

对象在内存中存储的布局可以分为3个部分,对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

对象头

hotspot的对象头分为两部分,第一部分用于存储对象自身运行数据(哈希吗、GC分代年龄、锁状态标识等),官方称其为“Mark Word”,另外一部分数据是类型指针,即对象指向它的类元素的指针,JVM通过这个指针确定创建的是哪一类型的指针。

实例数据

实例数据是对象存储的真正有效的信息,是程序中所定义的各种类型的字段值

对齐填充

对齐填充并不是必然存在的,也么有特别的含义,仅仅起着占位符的作用。

对象的访问定位

Java对象通过栈中的reference数据来操作堆上的具体数据,目前主流的访问方式有句柄和直接指针两种。

通过句柄访问对象

Java堆中会划分出一块内存,作为句柄池,reference中存放的就是句柄地址,而句柄中包含对象实例数据和类型数据各自的地址信息。
这里写图片描述
使用句柄的好处是reference中存储的是稳定的地址,在对象的移动中只会改变句柄中的实例数据指针,reference本身不需要改变。
###通过直接指针访问对象
通过直接指针访问对象速度快,节省了指针定位的时间开销。
这里写图片描述
本文主要介绍了JVM的内存划分,这对解决程序的内存溢出很有帮助。
#参考文献

1.周志明 《深入理解Java虚拟机》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值