1、运行时数据区域
java的运行时区域包括公共部分的方法区、堆区 以及线程私有的虚拟机栈、本地方法栈、程序计数器。
1.1 程序计数器
程序计数器是一块较小的内存空间,它是当前线程所执行字节码行号的指示器。每条线程的计数器都是独立的,互不干扰
如果线程执行的是java方法,则计数器记录的是正在执行的字节码指令地址。如果是native方法,则计数器的值为空
1.2 java栈
java栈是线程私有的,生命周期和线程相同。
java栈描述了方法执行时的内存模型:
一个方法运行的时候会创建一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口等。
每个方法的调用即入栈操作、执行完成即出栈操作。
局部变量表存放编译期可知的基本数据类型、引用类型、returnAddress类型。局部变量表所占内存编译期已经固定,运行时不会改变
如果栈的调用深度大于虚拟机规定的深度,会抛StackOverflowError。
如果栈的调用深度过大,会导致无法申请到内存,会抛出OOM
1.3 native栈
与java栈类似,native栈为native方法服务
1.4 java堆
堆被所有线程共享,虚拟机启动时便创建堆,堆的唯一目的就是存放对象实例和数组。也是垃圾回收的主要区域。
java堆可以物理内存不连续,但是逻辑内存必须连续
1.5 方法区
方法区被所有线程共享,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后编译的代码等。
有些人将方法区叫做永久代并不准确,hotspot只是将GC扩展到方法区。其他虚拟机不存在永久代的说法。
1.6 运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
1.7 直接内存
直接内存不是虚拟机运行时的一部分,但是也会受到本机内存的限制而导致OOM。
如NIO的DirectByteBuffer直接操作内存,避免了java堆和native堆之间的复制操作,提高了效率。但是native堆也受到本机限制。
2、hotspot虚拟机的对象
2.1 对象的创建
虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,就必须先执行相应的类加载。
在加载检查通过后,虚拟机将为对象分配内存。内存的大小在类加载完成后便可以确定。
2.1.1 为空间分配内存
如果java堆是规整的,即用过的内存放在一边,空闲的内存放在另一边,中间有一个指针作为分界点的指示器。如果分配空间,将指示器想空闲部分移动即可。这种方式叫做指针碰撞。
如果不是规整的,即用过的和空闲的堆内存相互交错。虚拟机就必须维护一个列表,记录哪些内存可用,叫做空闲列表。分配对象内存时,在空闲列表中找到合适的大小,并更新空闲列表。
2.1.2 线程安全
虚拟机创建对象非常频繁,并发下分配内存也并不是线程安全的。解决方法有两种。
1、对分配空间的动作进行同步。虚拟机采用CAS配上失败重试保证更新操作的原子性
2、每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。线程的TLAB用完分配新TLAB时,再进行同步
分配完内存,接下来就是对这块内存初始化。如对象是哪个类的实例,对象的GC分代年龄等
2.2 对象的内存分布
在Hotspot中,对象的内存分为三块:对象头、实例数据、对齐填充
2.2.1 对象头
对象头分为两部分,第一部分存储对象自身的运行时数据,如哈希码、GC分代,偏向线程ID等。这部分在32位和64位系统中分别占用32bit和64bit。官方称为Mark Work。
考虑到虚拟机的空间效率,Mark Work被设计成复用的存储空间,在不同的状态,会存储不同的内容。
对象头的另一部分是类型指针,虚拟机通过这个指针确定对象属于哪个类。并不是每个虚拟机都有类型指针。如果对象是数组,对象头还必须有一块记录数组长度的数据
2.2.2 实例数据
实例数据是真正的有效部分,存储了代码中定义的各种类型的内容。
2.2.3 对齐填充
对齐填充没有实际的意义,仅仅起到占位的作用。对象是以8字节对齐的,如果没有对齐,需要占位
3、对象的访问
java通过栈上的reference来操作堆中对象,访问堆中的对象有两种主流的方式
3.1 使用句柄
在java堆中分配一块内存作为句柄池,reference存储句柄池的对象的句柄地址,句柄包含实例对象数据和类型数据的地址信息
3.2 直接指针
reference直接存储对象的地址,效率更高
主要说明运行时各内存区域特点及对象创建