JVM对象创建和内存分配
1.对象创建
1.1 类加载检查
new 对象会先去检查new的类是否被加载了。
1.2 加载类
通过类加载器加载类。
1.3 分配内存
对象所需内存的大小,在类加载完成后便能确定,在堆空间中分配出一块与对象大小相等的空间(常规情况是这样,因为有在栈上分配的情况)。
1.3.1 内存划分方法
指针碰撞(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,未使用的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存把指针向未使用空间那边挪动一段距离得到与对象大小相等的空间。如图所示
就像图所示这样,临界点有个标记,当需要分配时移动标记直到拿出与对象大小相等的空间。
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
如图所示
会有一个列表来记录这些未使用的内存块,当有对象需要分配内存时会从列表中找出一块大小大于等于内存大小
1.3.2 解决并发
CAS
采用CAS加上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲
每个线程在堆中预先分配一小块内存。通过-XX:+/-UseTLAB来控制是否启用,默认-XX:+UseTLAB启用,-XX:TLABSize 指定大小。没开启或者大小不够用会采用CAS的方式。
1.4 初始化
为属性设置默认值。
1.5 设置对象头
对象头包含两部分信息;第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分 是类型指针,即对象指向它的类元数据的指针。synchronized关键字的实现就利用了对象头。
(图片来源于网络)
对象分代年龄只有4位既分代年龄最大值为15,不可能超过15。
1.6 执行init方法
执行init方法(c++层面上的),即对象按照程序员的意愿进行初始化。就是为属性赋值和执行构造方法。
2. 对象内存分配
2.1 栈上分配
JVM通过逃逸分析确定该对象不会被外部访问。如果不会被外部访问可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁减轻了垃圾回收的压力。
逃逸分析:
分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为参数传递到其它方法中。简单来说就是方法内定义对象只在该方法内使用未离开方法就是未逃逸。
public void test1(){Father f=new Father();f.setName("122");test(f);}public void test2(){Father f=new Father();f.setName("122");}
test1方法Father对象逃逸了,test方法未逃逸
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)。
标量替换:
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解成若干个被这个方法使用的成员变量来代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
标量与聚合量:
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
2.2 对象在Eden区分配
多数情况下,对象在年轻代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
Minor GC/Young GC:
指发生年轻代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:
一般会回收老年代,年轻代,方法区的垃圾,Full GC的速度一般会比Young GC的慢10倍以上。
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,绝大多数对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为年轻代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可, JVM默认参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动调整,如果不想这个比例不自动调整可以设置参数-XX:-UseAdaptiveSizePolicy
当survivor区没有足够的空间存放对象时会导致对象提前进入老年代。
2.3 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
避免为大对象分配内存时的复制操作(会降低效率)。
2.4 长期存活的对象进入老年代
对象在 Survivor 每经历过一次 MinorGC,分代年龄就加1,当分代年龄达到阈值就会移到老年代。其阈值不同的垃圾收集器有所差异但是最大不能超过15,前面对象头已经说明了。
2.5 对象动态年龄判断
当前存放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象分代年龄最大值的对象,就可以直接进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
2.6 老年代空间分配担保机制
在发生 Minor GC 之前,JVM 先检查老年代的最大可用连续空间是否大于新生代所有对象的总大小(或者历次晋升的平均大小)。如果是,则确保这次 Minor GC 是“安全”的,即新生代中所有存活对象全部晋升到老年代,老年代也能容纳得下,从而放心地进行 Minor GC。如果检查失败,JVM 则直接进行 Full GC来避免在 Minor GC 后因老年代空间不足而引发OOM。