深入理解java虚拟机 第二章 java内存区域与内存溢出异常
java虚拟机在执行java程序中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间
程序计数器
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,
每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”的内存
java虚拟机栈
线程私有,生命周期和线程相同
虚拟机栈描述的是java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
局部变量表:
存放了编译器可知的各种基本数据类型、对象引用和returnAddress
本地方法栈
为虚拟机使用到的native方法服务
java堆
被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存
java堆是垃圾收集器管理的主要区域
java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
方法区
各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池:
class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池
用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
直接内存
并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域
JDK1.4新加入了NIO类,引入了一种基于通道与缓冲区的IO方式,可以使用Native函数库直接分配对外内存,
然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作
HotSpot虚拟机对象探秘
对象创建
1 虚拟机遇到一条new指令,
2 检查这个指令的参数是否能在常量池中定位到一个类的符号引用
3 检查这个符号引用代表的类是否已被加载、解析和初始化过
如果没有先执行相应的类加载过程
4 分配内存,对象所需内存的大小在类加载完成后便可完全确定,
两种方式:
一 指针碰撞:假设java堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放一边,中间一个指针作为分界点
那分配内存就是把指针向空闲空间挪动一段与对象大小相等的距离
二 空闲列表:java堆内存并不是规整的,已使用的内存和空闲的内存相互交错,虚拟机必须维护一个列表,
记录上哪些内存块是可用的,在分配时从列表中找到一块足够大的空间划分给对象,并更新记录
在并发情况下对象的创建不是线程安全的,两个解决方案:
一 对分配内存空间的动作进行同步处理
二 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,
称本地线程分配缓冲(TLAB),只有TLAB分配完时,才需要同步锁定
5 将分配的内存空间都初始化为零,这一步保证了对象的实例字段在java代码中可以不赋初始值就直接使用
6 虚拟机对对象进行必要的设置,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代等
这些信息存放在对象的对象头中
7 构造函数
对象的内存布局
对象在内存中存储的布局可以分3块区域:对象头、实例数据和对齐填充
对象头两部分信息:
1 存储对象自身的运行时数据,哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID
2 类型指针,即对象指向它的类元数据的指针
实例数据是对象真正存储的有效信息
对齐填充不是必然存在的,也没有特殊含义,仅仅是占位符的作用
对象的访问定位
java程序需要通过栈上的reference数据来操作堆上的具体对象
具体的对象访问方式也是取决于虚拟机实现的,目前有两种主流方式:
1 使用句柄:java堆中将会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址
句柄中包含了对象实例数据与类型数据各自的具体地址信息
好处是在对象被移动时只会改变句柄中实例数据指针,而reference本身不需要修改
2 直接指针:java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference存储的就是对象地址
好处是速度快

浙公网安备 33010602011771号