深入理解Java虚拟机第三版读书笔记第二章01:Java中的内存区域
Java中的内存区域
这是我关于深入理解Java虚拟机第三版的读书笔记
引用书中一句话:
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
这句话真的太经典了,网上查阅资料后我的理解:
C++支持编写一些非常底层的程序,从而能够操作计算机硬件,并操纵实际内存地址。但这些是以牺牲可移植性为代价的,因为这时每个程序都是针对某种具体硬件环境的。而Java基于Java虚拟机(虚拟机的底层也是C++/C进行操作的)实现了跨平台性,Java开发无需关注底层,JAVA虚拟机为了我们解决了这些问题,将Java的开发注重与功能上。
下面会注意介绍这些功能
程序计数器
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
Java虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型。
它的生命周期与线程相同。
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存放局部变量表
、操作数栈
、动态连接
、方法出口
。
笼统的将Java内存区域划分为:堆
和栈
(继承自传统的C/C++程序中的内存划分)
这样的划分在Java语言中显得有些粗糙,实际的情况要远比这个复杂。
不过这样的划分也间接体现了内存划分中的两个重要区域:堆
和栈
。
这里的堆通常情况下来将,指的是虚拟机栈中的局部变量表。
局部变量表:
- 八大基本的数据类型
- 对象引用类型
- ReturnAddress类型
本地方法栈
与虚拟机栈发挥的作用非常相似。
区别:
- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
- 本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
Java堆
堆是虚拟机内存管理中最大的一块。
Java堆是被所有线程共享的一块区域,几乎几乎几乎
所有的对象都在这里分配内存。
随着技术的进步已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
在《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在堆上分配
。
如果从分配内的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。
无论怎么分配内存,都不会改变Java堆中存储内容的共享性,将Java堆细分的目的只是为了更好的回收内存,或者更快的分配内存。
线程共享的Java堆中可以划分出多个线程私有的分配缓冲区TLAB
。
所以说堆是完完全全线程共享的吗? 这也不太对。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作GC堆
。
因为现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现 新生代
、老年代
、永久代
、Eden空间
、 From Survivor空间
、To Survivor空间
等名词。
这些概念在本书后续章节中还会反复登场亮相,在这里笔者想先说明的是这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。
方法区
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息
、常量
、静态变量
、即时编译器编译的代码缓存
等数据。
《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作非堆
,目的是与Java堆区分开来。
永生代的概念:
在JDK8以前,许多Java程序程序员习惯于在HotSpot虚拟机上开发,很多人愿意把方法区称做
永生代
常量池
常量池是方法区的一部分,Class文件中除了有类的版本
,字段
,方法
,接口
等描述信息外,还有一项就是常量池
。
常量池用于存放即时编译时期生成的字面量与符号引用,这部分内容会在类被加载后 存放到方法区中的常量池中。
对象的创建
在Java语言层面创建对象的方式通常为:
- new关键字
- 克隆
- 反序列化
而我们最常用的莫过于new关键字
在Java虚拟机中通过new 关键字创建对象的过程:
- 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用。
- 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。(对象内存大小在类被加载解析的过程中被确定下来)
为对象分配空间的实际任务就是把一块确定大小的内存块从Java堆中取出,用于存放对象。
在划分的过程中有两个主要的思想:
在内存绝对规整排列的情况下:
将所有使用过的内存放在一边,未使用过的内存放在另一边,定义一个二者的分界点指针。
分配的过程就可以理解为:把指针向未使用过的内存区域划分一个与对象内存大小相同的区域,这样的方法称为指针碰撞
。
书的描述:
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”
在内存不规则排列的情况下:
如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了。
虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
问题:在多线程环境下可能会出现问题,无法保证原子性。
两种解决方案:
- 经典的CAS + 自旋操作实现线程同步,确保原子性。
- 没太看懂-> 是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
针对第二条看了别人写的博客给出的理解:
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值。
这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
这些信息存放在对象的对象头之中。
根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
接下来书中用了一段 HotSport的解释器代码(C++写的)来说明对象的创建过程。。
具体可以概括为:
- 类加载机制检查:JVM首先检查一个new指令的参数是否能在常量池中定位到一个符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过
- 分配内存:把一块儿确定大小的内存从Java堆中划分出来
- 初始化零值:对象的实例字段不需要赋初始值也可以直接使用其默认零值,就是这里起得作用
- 设置对象头:存储对象自身的运行时数据,类型指针
- 执行:为对象的字段赋值
对象的内存布局
在虚拟机中,对象在内存的布局可以划分为三个部分:对象头
、实例数据
、对其填充
。
由于书中写是对于32位的虚拟机所写的,64位的与32位的大不相同,所以先暂时放一下。
对象的访问定位
在我们成功创建对象之后,新的问题是如何访问对象?
我们所写的Java程序会通过对象的引用&reference
(位于虚拟机栈中)来操作位于堆上的具体对象。
现在又有研究的了,既然你是通过引用去访问的具体对象,那引用是怎么关联、定位到堆中的对象的呢?
主流的方式有:句柄
和直接指针
。
句柄:
Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
直接指针:
直接指针访问,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
两者的区别:
- 句柄的好处在于,reference指向的是稳定的句柄地址,在垃圾收集数据进行移动时,只需要改变实例对象指针就行,无需改变reference。
- 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。HotSpot使用的就是这种方式
参考:http://www.hollischuang.com/archives/3875