HotSpot VM 在Java堆中对象的分配、布局和访问的全过程。
1.对象的创建
VM 在遇到new指令时,
① 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有必须先执行相应的类加载过程。
② 为新生对象分配内存(对象所需的内存大小在类加载万成后便可完全确定)
Java内存分配-指针撞针(Java堆内存时绝对规整的)
Java内存分配-空闲列表(Java堆内存并不是规整的)
选择哪种内存分配方式有Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
因此,在使用Serial、ParNew等待Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
③ 内存分配完后,VM需要将分配到的内存空间都初始化为零值(不包括对象头),
④ VM对对象进行必要的设置(对象头),例如:这个对象是哪个类的实例,如何才能找到类的元数据,对象的哈希码,对象的GC分代年龄等。
以上的从VM视角看一个对象已经产生了,但从Java程序视角看,才刚刚开始
⑤ 执行init方法初始化。
2. 对象的内存布局
对象在内存中存储的布局可以分为是三个区域: 、实例数据(Instance Data)、对齐填充(Padding)
-
对象头信息是与对象自身定义的数据无关的额外存储成本。
对象头分为两个部门:
① Mark Word 存储对象自身的运行时数据(哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等),考虑到VM空间效率,Mark Word被设计成一个非固定的数据结构,它会根据对象的状态复用自己的存储空间。
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向锁的时间戳。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:
1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。
3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。
4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。
② 类型指针,即对象指向它的类元数据的指针(并不是所有VM实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经历对象本身)
该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
③ 数组长度
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
-
实例数据(Instance Data)
Klass Word是对象真正存储的有效信息,就是程序代码中定义的各种类型的字段类容(父类继承的和自己本身)
-
对齐填充(Padding)
非必然存在,无特殊含义,占位符。 HotSpotVM的自动内存关系系统要求对象其实地址必须是8字节的整数倍
3. 对象的访问定位
两种方式;
① 句柄
Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类行书举个指的具体地址信息。 好处: reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中的实例数据指针。
② 直接指针
如果使用直接指针访问,那Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中存储的直接就是对象地址。 好处:直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpotVM使用直接指针
4.实战:OutOfMemoryError异常
-
java 堆溢出
① 判断是否内存泄漏 mat 查看
② 判断是否内存溢出 调整 -Xms -Xmx
-
虚拟机栈和本地方法栈溢出 -Xss虚拟机栈 -Xoss本地方法栈
① 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StactOverflowError异常
② 如果虚拟机在扩展栈时无法申请到足够的内促空寂那,抛出OutOfMemoryError异常
这两种情况存在着一些互相重叠的地方,当栈空间无法继续分配时,到底是内存太小还是已使用的栈空间太大,本质上只是堆同一件事情的两种描述而已。
实验结果表明: 在单线程下无论是由于帧太大还是虚拟机栈容量太小当内存无法分配的时候,虚拟机抛出StactOverflowError
多线程时,为每个线程的栈分配的内存越大,反而越容易产生内存溢出。
多线程导致内存溢出: 可能只能通过减少最大堆和减少栈容量来换取最多的线程。
-
方法区和运行时常量池溢出
(限制方法区大小参数 -XX:PermSize -XX:MaxPermSize)
运行时生成大量的类区填满方法区,直到溢出(可使用动态代理),借助cGLib直接操作字节码运行时生产大量的动态类
方法区溢出是一种常见的内存溢出异常,常见的有:大量jsp或动态产生jsp文件的应用、基于OSGi 的应用即使是同一个类文件,被不同加载器加载也会视为不同的类。
-
直接内存溢出 -XX:MaxDirectMemorySize,不指定默认与Java堆最大一致(-Xmx)
由于DirectMemory导致的内存溢出,有个明显的特征是在Heap Dump文件中不会出现明显的异常,如果发现OOM之后Dump文件很小,程序中直接或间接的使用了NIO,可能是这个原因