Java - Hotspot

104 阅读3分钟

1. 对象创建

  1. 接收到new指令时,先判断这个类是否被加载、解析、初始化过;如果没有先执行类的加载过程。
  2. 类加载检查通过后,为新生对象分配内存,如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式,如果是不连续规整的,采用“空闲列表”分配方式。内存是否规整取决于垃圾收集器是都带有压缩整理功能。
  3. Serial,ParNew等带有Compact过程的收集器,采用“指针碰撞”分配算法。CMS基于Mark-Sweep算法收集器,通常采用“空闲列表”分配方式。
  4. 创建对象涉及到分配内存和指针指向两个操作,不是原子性的,不是线程安全的。针对这个问题有两个解决办法:
  1. 采用CAS加上失败重试来保证操作的原子性。
  2. 采用TLAB策略(Thread Local Allocation Buffer),在Java堆中预先为每一个线程分配一块内存,称为TLAB,哪个线程要分配内存就在各自的TLAB上进行内存的分配,只有TLAB用完进行新的TLAB用完进行新的TLAB的分配时才需要同步锁定,虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB 
  1. 内存分配完成后,需要对对象头进行设置,包括这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码、对象的GC分代年龄等信息。
  2. 最后执行init方法,把对象按照程序员的意愿进行初始化。对象完成初始化。

2. 对象的内存分配

  1. 分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐补充(Padding)。
  2. 对象头,存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳、这部分数据的长度在32至64位虚拟机中分别为32bit和64bit。
    另一个部分是类型指针,虚拟机通过这个对象来确定这个对象哪个类的实例。

3. 对象的访问定位

  1. Java程序需要通过栈上的reference数据来操作堆中的具体对象,具体实现有两种方式;使用句柄和直接指针两种。
  2. 句柄:Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象的实例数据和类型数据各自的地址信息。最大好处是当对象修改时,reference本身不需要修改,因为reference中存储的是稳定的句柄地址。

image.jpeg

  1. 接指针:reference中存储的直接就是堆中的对象地址,堆对象的布局中需要考虑如何防止访问类型数据的相关信息。最大好处是速度更快,节省了一次指针定位的开销,HotSpot采用直接指针方式。

image.jpeg

4. OutOfMemoryError

  1. 堆溢出:不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制就就产生内存溢出。
  2. -Xms(20m)堆最小值;-Xmx(20m)堆最大值; -XX:+HeapDumpOnOutOfMemoryError 内存溢出异常时Dump出当前的内存堆转存储快照以便日后分析。

虚拟机栈和本地方法溢出

  -Xss栈容量 

方法区和运行常量池溢出

  多次调用String.intern()方法可以产生内存溢出异常。
  JDK1.6之间,可以通过 -XX:PermSize 和 -XX:MaxPermSize 限定永久代大小,从而达到限制方法区大小的目的。

本地直接内存溢出

  通过 -XX:MaxDirectMemorySize 指定。如果不指定则默认和Java堆最大值(-Xmx指定)一样。