根据《Java 虚拟机规范(Java SE 7版)》规定,Java虚拟机所管理的内存,将会包括以下几个运行时数据区域:
注:
1、由所有线程共享的数据区: 对应 java内存模型的主内存,各线程共有;
2、线程隔离的数据区:对应 java内存模型的工作内存,各自线程持有;
1.程序计数器(线程私有)
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解释器工作时,就是通过改变“这个计数器的值”,来选取下一条执行字节码指令。
每条线程都有一个独立的程序计数器。
如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈(线程私有)
同样是线程私有,描述Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
1)、一个方法对应一个栈帧。
2)、局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。
规定的异常情况有两种:
1.线程请求的栈的深度,大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。
3.本地方法栈(线程私有)
和Java虚拟机栈很类似,不同的是:本地方法栈为Native方法服务。
4.Java堆(线程共享)
是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。
堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。
堆无法扩展时,抛出OutOfMemoryError异常
5.方法区(线程共享)
所有线程共享,存储:已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
6.运行时常量池(线程共享)
它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Const Pool Table),用于存放编译期生成的各种字面量和符号引用。
并非预置入Class文件中常量池的内容,才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
7.直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库,直接分配“堆外内存”,然后通过一个存储在Java堆中的DirectByteBuffer对象,作为这块内存的引用进行操作。
因为避免了在Java堆和Native堆中来回复制数据,提高了性能。
当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。
===============================
二: 对象的创建
2.1 当虚拟机遇到一条New指令时:会进行如下步骤
- 检查指令的参数(即工作中我们New的对象),能否在常量池中找到它的符号引用。
- 如果存在,检查符号引用代表的类是否被加载、解析、初始化过。
- 加载通过后,虚拟机将为新生对象分配内存。(所需内存大小在类加载完成后便可确定)
2.2 两种内存分配的方式:
指针碰撞:假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边。中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲空间那边挪动一段与对象大小相等的距离。这种方式则属于指针碰撞。
空闲列表:如果堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,显然无法使用指针碰撞。虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新记录表上的数据。这种方式属于空闲列表。
具体选择哪种分配方式由Java堆决定,而Java堆是否规整,则有GC收集器决定。因此使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞。而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的空闲列表。
2.3如何保证分配内存时线程的安全性?
- 对分配内存的动作进行同步处理(实际上虚拟机采用CAS配上失败重试的机制保证了更新操作的原子性)
- 把分配内存的动作按照线程划分在不同的空间之中进行(即每个线程在Java堆中预先分配一小块内存(称为本地线程分配缓冲))。
三:对象的内存布局
在HotSpot虚拟机中对象的内存布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头包括两部分信息:
- 存储对象自身的运行时数据(如:哈希码、GC分代年龄、锁 等)
- 类型指针(即对象指向他的类元数据的指针,虚拟机根据此指针来确认对象属于哪个类的实例)
实例数据:
实例数据才是对象真正存贮的有效信息(即程序中所定义的各种类型的字段内容)。
对齐填充:
不是必然存在的,仅仅起到占位符的作用。
四:对象的访问定位
创建对象就是为了在程序中使用,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。
对象的访问方式:
句柄访问:Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优点:reference中存储句柄地址是稳定的。在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
句柄访问图示:
指针访问:reference中存储的直接就是对象地址。
优点:速度快,节省了指针定位的时间成本。
指针访问图示: