对象的创建过程
1、加载类
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。
如果没有,那必须先执行相应的类加载过程。
2、分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 对象所需内存的大小在类加载完成后便可完全确定。
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
分配方式:
1、指针碰撞。适用于连续内存,需要垃圾回收有整理功能。
2、空闲列表。不需要连续内存,可用于标记-清理的垃圾回收功能。
指针操作同步问题的解决方式,则也是一般的同步问题的解决方式:
1、CAS+失败重试来保证原子性
2、线程隔离,即为每个线程分配一块单独的区域,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
3、内存归零
不包括对象头
4、对象头设置
5、构造函数
虚拟机部分已经完成,开始对象自定义部分。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1、对象头
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
2、实例数据
存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配策略为longs/doubles、 ints、 shorts/chars、bytes/booleans、 oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段
总是被分配到一起。
3、对齐填充
对象的定位访问
Java程序需要通过栈上的reference数据来操作堆上的具体对象。 由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、 访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
目前主流的访问方式有使用句柄和直接指针两种。
1、句柄
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改
2、指针
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
HotSpot是使用第指针方式进行对象访问的
内存溢出
Java堆溢出
java.lang.OutOfMemoryError:Java heap space
通过查看引用链确定溢出原因:
内存合理的大:内存溢出 调大内存,或调小业务内存模型
内存不合理的大:内存泄漏 解决问题
虚拟机栈或本地方法栈溢出
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
线程过多,会导致内存溢出。
方法区和运行时常量池溢出
Exception in thread"main"java.lang.OutOfMemoryError:PermGen space
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因