1. 对象创建
- 接收到new指令时,先判断这个类是否被加载、解析、初始化过;如果没有先执行类的加载过程。
- 类加载检查通过后,为新生对象分配内存,如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式,如果是不连续规整的,采用“空闲列表”分配方式。内存是否规整取决于垃圾收集器是都带有压缩整理功能。
- Serial,ParNew等带有Compact过程的收集器,采用“指针碰撞”分配算法。CMS基于Mark-Sweep算法收集器,通常采用“空闲列表”分配方式。
- 创建对象涉及到分配内存和指针指向两个操作,不是原子性的,不是线程安全的。针对这个问题有两个解决办法:
- 采用CAS加上失败重试来保证操作的原子性。
- 采用TLAB策略(Thread Local Allocation Buffer),在Java堆中预先为每一个线程分配一块内存,称为TLAB,哪个线程要分配内存就在各自的TLAB上进行内存的分配,只有TLAB用完进行新的TLAB用完进行新的TLAB的分配时才需要同步锁定,虚拟机是否使用TLAB,可以通过
-XX:+/-UseTLAB
- 内存分配完成后,需要对对象头进行设置,包括这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码、对象的GC分代年龄等信息。
- 最后执行init方法,把对象按照程序员的意愿进行初始化。对象完成初始化。
2. 对象的内存分配
- 分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。
- 对象头,存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳、这部分数据的长度在32至64位虚拟机中分别为32bit和64bit。
另一个部分是类型指针,虚拟机通过这个对象来确定这个对象哪个类的实例。
3. 对象的访问定位
- Java程序需要通过栈上的reference数据来操作堆中的具体对象,具体实现有两种方式;使用句柄和直接指针两种。
- 句柄:Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象的实例数据和类型数据各自的地址信息。最大好处是当对象修改时,reference本身不需要修改,因为reference中存储的是稳定的句柄地址。
- 接指针:reference中存储的直接就是堆中的对象地址,堆对象的布局中需要考虑如何防止访问类型数据的相关信息。最大好处是速度更快,节省了一次指针定位的开销,HotSpot采用直接指针方式。
4. OutOfMemoryError
- 堆溢出:不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制就就产生内存溢出。
- -Xms(20m)堆最小值;-Xmx(20m)堆最大值;
-XX:+HeapDumpOnOutOfMemoryError内存溢出异常时Dump出当前的内存堆转存储快照以便日后分析。
虚拟机栈和本地方法溢出
-Xss栈容量
方法区和运行常量池溢出
多次调用String.intern()方法可以产生内存溢出异常。
JDK1.6之间,可以通过 -XX:PermSize 和 -XX:MaxPermSize 限定永久代大小,从而达到限制方法区大小的目的。
本地直接内存溢出
通过 -XX:MaxDirectMemorySize 指定。如果不指定则默认和Java堆最大值(-Xmx指定)一样。