AmmoMercy的技术博客

Stay hungry, stay foolish.

0%

深入理解JVM2 JAVA内存区域

深入理解JVM笔记2

主要内存区域

程序计数器PC

与计算机硬件中的PC类似用于指示当前线程运行到的字节码指令的地址,当运行本地方法时,pc为undefined。

唯一一个没有规定OutOfMemoryError的区域。

虚拟机栈

线程私有,用于存放当前线程的方法栈帧。 栈帧是由局部变量表、操作数栈、动态链接以及方法出口等组成。

其中局部变量表不仅包含局部变量(基本类型直接存值,对象类型存引用)还包括了returnAddress(用于指示该方法完成后返回的字节码指令地址)。局部变量表的大小在编译之后就确定了,在方法的运行期间是不会改变的。比如一个int变量只可能存放int,对象的引用只能存放该对象类型的引用,自然不会变化。

本地方法栈

与虚拟机栈类似,只不过存放的是本地方法。所以,HotSpot虚拟机将其与虚拟机栈直接合二为一。

JAVA堆

又叫GC堆,几乎所有对象分配的场所,是JVM中最大的一块内存。之所以说是几乎所有,是因为当前JIT和逃逸分析技术的成熟,比如栈上分配和标量替换优化技术,使得对象不一定在堆上分配。堆由GC管理(见下一章GC)。

线程私有,但可以为了快速释放和分配内存而为每个线程分配私有的缓冲区(Thread Local Allocattion Buffer, TLAB)。

分为新生代、老年代。

新生代:有from survivor、to survivor、 eden。两个survivor用于存放young gc之后存活的对象,eden用于存放新生成的对象。因为大多数对象存活时间都很短,即所谓的“朝生夕死”,所以eden的容量较大,survivor空间较小。默认配置下from to各占1/10,eden占8/10。

堆的空间一般都是自动扩展的,通过-Xms和-Xmx两个参数来控制堆的最小和最大容量,当最大和最小容量设为相同值时,就变成了固定大小。

方法区

存放类的信息,逻辑上是堆的一部分。堆上存放对象,对象来自于类,两者十分相似,所以将方法区描述为堆的一部分是很自然的。在HotSpot上,对方法区的回收机制与堆类似,故其又称为永久代。不过在JDK1.8中,方法区被从堆中移到了直接内存里,永久代也不复存在了,取而代之的是元空间。

运行时常量池

方法区的一部分,存放编译期生成的各种常量。jdk1.8在物理实现上将其从方法区移出,存放在堆上单独开辟的空间,但逻辑上仍然是方法区的一部分。

直接内存

NIO利用该区域进行I/O,其在堆中有一个DirectByteBuffer对象,在该对象中存有直接内存的引用。使用直接内存的好处是可以避免在JAVA堆和Native堆中来回复制。

小结

上述所有区域中,只有pc没有OutOfMemory异常(毕竟只是一个记录执行位置的指针),其他区域都有可能产尘OutOfMemory异常。同时,对于虚拟机栈和本地方法栈来说,两个区域还会产生StackOverflow异常

线程私有 线程共享
pc、栈 堆、方法区

对象的创建

检查类是否被加载

对象的创建依赖于类的信息,所以在创建一个对象之前应该先确定该对象的类是否已经被加载到jvm。具体实现是检查常量池中是否包含类的引用。

分配内存

在堆上的空闲区为一个对象开辟一块区域。所以就必须要有空闲区进行管理的方法。一种方法是将已分配的空间集中在一侧,空闲区集中在另一侧(这种情况称为规整的),这样每次分配空间的时候将空闲区靠近分界的区域分配给新创建的内存即可。同时,利用一个指针来指示两个区域的分界,这种方法称为指针碰撞法。如果空闲内存和已使用的内存是交错的(即不规整),那么要维护一个列表来记录每个空闲块的地址和大小等信息,这种方法称为空闲链表法。

线程安全

对象的创建是十分频繁的,那么就需要进行一些操作来确保对象的分配是线程安全的。一是使用CAS加上失败重试来进行同步。另一种方法是使用TLAB,每个线程均在自己的TLAB中新建对象, 只有TLAB满了之后申请新的TLAB时,才需要同步锁定。

初始化

将分配到的内存空间设为零值(int是0,对象是null等),如果使用TLAB也可提前至TLAB进行。

一些设置

将一些必要的信息写入对象,如hashcode、分代年龄、类信息等。

对象的内存布局

对象头

两部分组成。第一部分用于存储对象本身的信息,如hashcode、分代年龄、与锁相关的一些信息等,也叫Mark Word。第二部分是只想该对象所属的类的指针。

对象字段

对象中定义的字段等。

对齐填充

jvm规定对象的长度必须是8字节的整数倍,如果前两项大小加起来不是8字节整数倍就需要进行填充。

其他

对象的访问

使用对象时需获取它的类型信息,那么如何获取呢?有两种方法:一种是利用在前面说过的,对象头的第二部分指向了对象所属的类,直接利用这个指针就可以,这被成为直接指针方法。另一种是维护一个句柄,句柄中有两项内容,一项是指向对象的指针,另一项是指向对象所对应的类的指针。

在栈帧中的局部变量表中,如果使用第一种方法,那么引用是直接指向对象的指针,而第二种方法的则指向句柄。