对象的创建
过程如下:
- JVM遇到new指令,检查这个指令的参数是否能在常量池定位到一个类的符号引用,检查这个符号代表的类是否被加载、解析和初始化过(否则执行类加载)。
- 检查通过,新生对象分配内存。对象所需内存大小在类加载完成后便可完全确定(每个变量+对象头引用等等)。
- 不同的JV由于GC策略不同,分配内存结果不同。绝对规整的JVM Heap,有一个指针作为空闲内存和已用内存的分界点,分配时挪动指针一段与对象大小相等的距离,这种方式叫指针碰撞。如果不规整,VM会有一个列表标记内存的可用,从列表找到一块足够大的空间分配给对象实例,这种方式叫空闲列表。
- 考虑到并发性,有两种解决方案,一种是对分配内存空间的同步进行同步处理,第二种是把内存分配的动作按照线程划分为不同的空间进行,叫做本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。分配完后,内存空间初始化为零值。
- 一般执行完new指令,然后执行init方法。
对象的内存布局
对象内存中包括三部分:对象头(Header)、实例数据(Instance Data)和对齐数据(Padding), 这些都在堆上
对象头
Header中包括两部分,一部分是存储对象自身的运行时数据:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID、偏向时间戳等,这部分数据长度在32位和64位虚拟机分别为32bit和64bit(未开启压缩指针)。这一部分数据叫Mark Word.会尽量采用了位复用,保存这部分数据,不同状态下存储的内容不一样。
所以一个引用的长度在32位jvm和Davlik虚拟机是32位,在未开启指针压缩的64bit jvm是64位的(8个字节),这是在栈上的
对象头另一部分就是类型指针,即对象指向它的类元数据的指针,但不是所有的虚拟机都必须在对象数据上保留类型指针,比如Java数组,对象头还包括一块记录数据长度的数据。因为数据的元数据中无法确定数组的大小。所以这一部分不是必须的。
实例数据
实例数据部分是对象真正存储的有效信息。无论是父类继承还是子类定义的,都会记录下来。存储顺序会受到虚拟机分配策略参数和字段在java源码中定义的顺序有关,默认分配策略是longs/doubles, ints, shorts/chars, bytes/boolean, oops,相同宽度的字段总是被分配到一起。
对齐填充
对齐填充不是必然存在的,也没有特别意义。起到占位符的作用,HotSpot虚拟机自动内存管理系统要求对象起始地址必须是8字节的整数倍。
对象的访问定位
两种,一种是使用句柄,另一种是直接指针。
句柄
java堆里将会华为一块内存来作为句柄池,引用里存的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。好处是reference里存放的是稳定的句柄地址,不需要修改。
直接指针
reference里存储的直接就是对象地址,好处是速度快,节省了一次指针定位的时间开销。这种方式虚拟机用的更多一些。比如Hotspot虚拟机
Eclipse里模拟OOM
设置eclipse run的jvm参数如下
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
//加入heapDumpOnOutOfMemoryError,虚拟机会在出现内存溢出时dump当前的内存堆转储快照,以便进行分析
-XX:+HeapDumpOnOutOfMemoryError
使用Eclipse Memory Analyzer可以进行内存分析。