1.Java普通对象的分配过程(Class对象和数组对象除外)
1.1 类加载检查
当JVM执行到new
执行时(new、克隆、反序列化等),先去常量池中是否能定位到该类的符号引用
,并检查该符号引用对应的类是否已经加载并解析完成,如果没有则进入类加载流程。因为如果类还没有加载,那么此时分配类对应的对象是没有任何意义的,对象的所有信息都要来自于Class。
1.2 分配内存空间
因为对象的实际内存大小在类加载阶段早已确定,所以分配内存空间这个流程就是在堆内存中开辟一个确定好大小的地址空间;但是分配内存这个阶段有2个问题需要解决:如何分配?并发安全问题如何保障?
1.2.1 分配策略一:指针碰撞(默认) bump the pointer
如果堆内存时绝对整齐的,所有的使用和未使用的内存是划分开来,将一个地址指针作为指示器,分配内存就是移动指针;内存的规整问题取决于所使用的的垃圾收集器。
1.2.2 分配策略二:空闲列表 Free List
如果堆内存是不整齐的,JVM会维护一个表,记录哪些内存可用哪些不可用,在分配的时候再列表中寻找合适大小的地址分配给对象,并更新这个空闲列表记录。
1.2.3 并发问题方案一:CAS无锁机制
Java普通内存对象分配的并发安全问题可以用CAS
无锁机制Cmpare and Swap
完成,CAS是通过比较期望值和内存值进行原子操作,JVM通过CAS原子操作不断重试来分配内存。
1.2.4 并发问题方案二:TLAB 本地线程分配缓冲
Thread Local Allocation Buffer
本地线程分配缓冲,没给现场在Java堆空间中预先分配一小块线程私有的内存空间,每个线程创建对象时可以在自己的Buffer上分配,这样大大提高了效率,当Buffer不够时再重新申请。可以通过-XX:+UseTLAB
来设置JVM是否启用TLAB,默认是开启的。-XX:TLABSize
可以设置TLAB的Buffer初始大小。
1.3 初始化
JVM在为对象分配了内存地址后,JVM将对象的成员属性都设置为初始值,引用类型为Null;如果是使用TLAB方式,这个过程也可以提前到TLAB分配时进行。这个阶段可以让未赋值的对象暴露引用,程序是能够访问对象的初始属性,例如单例模式中的双重校验锁问题。
1.4 设置对象头
JVM对对象进行元数据的设置,例如这个对象的类信息、hashCode(第一次使用时)、GC分代年龄等信息,这些信息保存在对象头中。
1.5 对象初始化
对象执行构造方法进行成员属性的赋值或其他程序操作。
2.对象的内存布局
对象在JVM堆内存中的布局绝不是我们看到的class描述那样简单:
MarkWord:
2.1 指针压缩
JDK从1.6开始在64位操作系统中支持指针压缩
,JVM通过-XX:+UseCompressedOops
参数来控制,模式开启指针压缩。使用指针压缩有如下特点:
1.使用指针压缩技术可以节省内存,减少指针在主存和高速缓存的总线带宽压力。
2.堆内存<4G时,不需要启用指针压缩,JVM会去除高32位地址,使用低位的虚拟地址空间。
3.堆内存>32G时,压缩指针会失效,强制使用64位。
2.2 对象的访问定位方式
2.2.1 句柄
2.2.2 直接引用
HotSpot使用第二种直接引用的方式进行对象的访问,但是有例外(如果使用了Shenandoah垃圾收集器,则有一次额外的转发定位)。
3.对象的分配策略/方式
3.1 栈上分配
如果JVM通过逃逸分析
推断当前对象逃不出当前方法且达成JIT和标量替换条件,则该对象会在栈上分配内存。JDK7后默认开启逃逸分析,可以通过-XX:-DoEscapeAnalysis
来关闭。
3.2 优先在年轻代Eden区分配
绝大数情况下,新的对象优先在年轻代Eden区分配,当Eden区没有足够的内存空间时,JVM将发起一次Minor GC
,Minor GC相比于Full GC频率很快,速度也快,Minor GC只回收年轻代。年轻代里的Eden区和幸存0和幸存1区比例默认是8:1:1,可通过-XX:+UseAdaptiveSizePolicy
开启比例自适应变化,也可以同样参数关闭自动变化。
3.3 大对象直接进入老年代
需要连续内存空间的大的对象比如数组,如果对象大小超过阈值则直接进入老年代,可以通过-XX:PretenureSizeThreshold
参数来设置阈值,但是这个参数只对Serial和ParNew收集器有效。大对象直接进入老年代是为了降低大对象在年轻代频繁复制带来的性能消耗。
3.4 长期存活的对象进入老年代
对象在幸存0和幸存1区频繁的复制迁移,在迁移过程中在对象头中会记录对象的分代年龄,当年龄>=15就会进入老年代(不同的垃圾收集器阈值不一定),而且这个阈值可以通过-XX:MaxTenuringThreshold
来设置,但是不能超过15,因为对象头中只有4位二进制记录对象头大小。
3.5 对象动态年龄判断
当一批对象的总大小大于幸存区(其中一个)大小的50%(-XX:TargetSurvivorRatio)时,大于等于这批对象年龄最大的对象就可以直接进入老年代。动态年龄判断不是实时进行,一般是在Minor GC之后发生。
3.6 空间分配担保机制
年轻代买一次Minor GC之后都会计算一下老年代剩余的可用大小,如果这个空闲大小<年轻代里所有对象大小总和(包括垃圾)并且允许空间分配担保失败-XX:HandlePromotionFailure
,如果允许,就会检查老年代的可用大小是否大于之前每一次Minor GC进入老年代的对象的平均大小,如果大于则进行一次Minor GC,如果担保失败则进行Full GC。如果分配担保参数不允许或者没设置,则也是Full GC。
4.对象的存活判断
4.1 引用计数法
给对象添加一个引用的计数器,每当有一个引用指向他就+1,当引用断开就-1。当计数器为0时则标记为垃圾。这个算法十分高效和简单,但是主流的垃圾收集器并没有采纳这种算法,因为它很难解决循环引用的问题。
4.2 可达性分析算法
可达性分析算法
就是通过一系列的GC Roots
为对象作为起点,从根节点往下搜索,当一个对象到GC Roots没有任何引用链时,说明对象不可用会被回收,能作为GC Roots的对象有:
- 栈(本地变量表)中引用的对象。
- 静态遍变量引用的对象。
- 方法区常量引用的对象。
- 本地方法栈中JNI引用的对象。
- Class对象、异常对象、类加载器等等。
- synchronized锁持有的对象。
- JVM内部的特殊对象,例如JMXBean、本地代码缓存等。
- JVM中临时性对象,例如跨带引用的对象。
注:Class对象回收条件比较苛刻:1.该类所有实例都已回收。2.加载该类的ClassLoader已被回 收。3.Class对象 没有被任何地方引用,无法通过反射访问该类。4.JVM参数控制。
4.3 对象的finalize方法作用
在可达性分析算法中不可达的对象也并不是一定会回收,要真正标记一个对象的死亡,需要经历再次标记过程,而这个过程可以救自己一命。
1.第一次标记并进行筛选,当对象没有覆盖finalize()方法,对象直接被回收。
2.如果对象覆盖了finalize()方法,在方法里将自己与引用链任意对象建立关联即可。在第二次标记时,它将移出待回收集合,而且对象的finalize()方法只能执行一次。
5.对象的各种引用
5.1 强引用
一般的User user = new User()
模式都是属于强引用,这种引用只要引用还在,垃圾收集器就不会回收该对象。
5.2 软引用 SoftReference
SoftReference<User> user = new SoftReference<>(new User())
,这种模式就是软引用;JVM将要发生OOM之前,会对这些对象回收,如果这次回收仍然内存不够,才会抛出OOM错误。
5.3 弱引用 WeakReference
WeakReference<User> user = new WeakReference<>(new User())
,这种模式就是弱引用,弱引用的对象只能存活到下一次垃圾回收之前,GC发生时不管内存够不够都会被回收。JDK中ThreadLocal
中就有软引用的场景
5.4 虚引用 PhantomReference
基本不用的一种引用,在垃圾回收阶段有一定作用。