一、内存模型
运行时数据区域
线程私有部分:
a) 程序计数器【没有OutOfMemoryError】:当前线程所执行的字节码的行号指示器。 如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;若执行的是一个Native方法,这个计数器值则为空)
b) 虚拟机栈【OutOfMemoryError StackOverflowError】:Java方法执行的内存模型,存放局部变量(基本数据类型、以及引用)、操作数栈、动态链接、方法出口等信息。
c) 本地方法栈:Native方法同(b)
线程共享部分:
a) 堆【OutOfMemoryError】:对象以及数组。
划分:a.新生代、老年代
b.Eden\ From Survivor\To Survivor
c.多个线程私有的分配缓冲区(TLAB,Thread LocalAllocation Buffer)
b) 方法区【OutOfMemoryError】:类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池:编译器生成的各种字面量和符号引用
动态性:并非预置入Class文件中常量池的内容才能进入方法区运行常量池,运行期间也可能将新常量放入池中(String.intern())
非运行时数据区域
直接内存【OutOfMemoryError】:受到本机本内存和处理器寻址空间的限制(不属于Java运行时数据区,也不属于Java虚拟机规范中的内存区域)。若不考虑直接内存配置虚拟机参数,会导致各个内存区域综合大于物理内存限制,导致动态扩展时出现内存溢出情况
直接内存的使用:JDK1.4中引入NIO类,引入了基于通道与缓冲区的I/O方式,使用Native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
二、对象或数组(Java堆)
对象的创建过程
收到new指令
常量池中定位符号引用
符号引用代表的类是否被加载、解析和初始化过(若没有,就开始加载,双亲委派模式)
新生对象内存分配(对象所需的内存大小在类加载的时候就能确定)
1. 划分空间:垃圾收集器是否带压缩整理功能决定内存是否规整
内存绝对规整:指针碰撞(Serial、ParNew) 内存不规整:空闲列表(CMS)
2. 线程安全:并发情况下可能发生正在给对象A分配内存,指针还未修改,对象B又使用了原来的内存进行分配。
对分配内存空间的动作进行同步处理:CAS配上失败重试的方法保证更新的原子性 内存分配的动作按照线程划分在不同的空间之中进行,即对每个线程在Java中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。当前线程在其对应的TLAB上分配内存,只有TLAB用完病分配新的TLAB时才需要同步锁定。
虚拟机对分配到的内存空间都初始化为零值(不包括对象头);若使用了TLAB可以直接在分配TLAB时进行,保证对象的实例字段在Java代码中可以不赋初值就可以直接使用。
【注意:类的成员变量可以不显示地初始化(Java虚拟机都会先自动给它初始化为默认值)。方法中的局部变量如果只负责接收一个表达式的值,可以不初始化,但是参与运算和直接输出等其它情况的局部变量需要初始化。】
设置对象头:例如对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分带年龄等信息、根据虚拟机当前的运行状态的不同是否启用偏向锁等等。
虚拟机的初始化工作完成,根据字节码中是否有invokespecial指令,决定new之后接着执行<init>方法,按照程序员的意愿初始化
对象的内存布局
1) 对象头:存储的信息与对象自身定义的数据无关的额外存储成本
l 运行时数据MarkWord:例如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
(Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间,即根据状态和标志位其存储内容也会不同)
l 类型指针:对象指向它的类元数据的指针(虚拟机通过这个指针确定是哪个类的实例,但并不是所有虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身)【对象访问的直接访问】
l 对象为数组,对象头中还有记录数组长度的数据(普通对象的元数据信息确定Java对象的大小)
2) 实例数据:对象真正存储的有效信息(父类继承+子类中定义),存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
3) 对齐填充:占位符作用
HotSpot VM的自动内存管理系统要求对象其实地址必须是8字节的整数倍,即对象大小是8字节的整数倍,对象头满足了要求,主要就是对象实例数据部分需要对其填充。
对象的访问定位
通过栈上的reference数据来操作堆的具体对象。
1) 句柄:
Java堆中划分一块内存作为句柄池,refenrence中存储的对象的句柄地址;而句柄中包含了对象实例数据(堆中)与类型数据(方法区)各自的具体地址信息。
2) 直接指针访问
refenrence存储的直接就是对象地址(堆中),Java堆对象的布局中就必须考虑如何放置访问类型数据(方法区)的相关信息。
比较:
1) 句柄访问:(从垃圾回收角度)
优点:refenrence中存储的是句柄地址,对象被移动时只会改变句柄中的实例数据的指针(对象的移动在GC中非常频繁),而reference本身不需要修改;
2) 直接访问:(从对象访问角度)
优点:速度更快,节省了一次指针定位的时间开销(由于对象访问在Java中非常频繁)