对象的创建
对象创建的主要流程:
类加载检查
虚拟机遇到new指令时,会先检查对应类是否已加载,解析和初始化过。
分配内存
- 指针碰撞:默认的,内存绝对规整,用过的内存放在一边上,没用过的放在另一边。
- 空闲列表:内存不规整,虚拟机维护一个列表,记录哪些内存可用。 注:当存在并发问题时,会采用CAS或TLAB
初始化
在分配内存之后,JVM会自动将所有实例变量初始化为其数据类型的默认值(即所谓的“零值”或“空白值”)。例如,数值类型(如int、float、double)的默认值是0,布尔类型的默认值是false,引用类型的默认值是null。
设置对象头
在HotSpot虚拟机中,对象在内存中存储布局可分为三块区域:对象头、实例数据和对齐填充。
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另一部分是类型指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
指针压缩:适用于堆内存在4g-32g之间。
执行init方法
给对象赋值并执行构造方法。
对象分配
对象内存分配
对象内存分配的流程图:
对象逃逸分析
JDK7之后默认开启。分析对象动态作用域,当一个对象在方法中被定义后,可能被外部方法引用。JVM通过逃逸分析确定该对象不被外部访问,则可能会在栈上分配,减轻垃圾回收压力。
标量替换
JVM确定一个对象只包含标量字段,并且这些字段在程序执行期间不会被外部代码访问时。在这种情况下,JVM可能会选择不实际创建该对象,而是直接在栈上分配这些标量字段。这可以减少堆内存的使用和垃圾收集的开销,因为对象不需要在堆上进行分配和回收。
标量和聚合量
标量即不可进一步分解的量,如JAVA基本数据类型。而对象是聚合量可分解。
对象在Eden区分配
大多数情况,对象在新生代中Eden区分配。当Eden区没有充足空间进行分配时会触发Minor GC
对象在老年代分配
大对象直接进入老年代
当JVM判断为大对象时,该对象直接进入老年代,而不是年轻代。(JVM可设置对象大小,且这个参数只在Serial和ParNew两个收集器下有效) 可以避免为大对象分配内存时的复制操作而降低效率。
长期存活对象进入老年代
分代年龄超过阈值进入老年代
对象动态年龄判断
年龄1+年龄2+...+年龄n多个对象总和超过S区50%,此时就会把年龄n(包含)放入老年代,一般在Minor GC后触发,目的是让长期存活对象尽早进入老年代。
老年代空间分配担保机制
对象内存回收
引用计数法
给对象添加一个引用计数器,每当有一个引用引用它则+1,弊端是循环依赖问题很难解决。
可达性分析算法
将GC Roots为起点,从这些节点向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余就是垃圾对象。
常见的引用类型
Java的引用类型一般分为4种:强引用、软引用、弱引用、虚引用
强引用:普通的变量引用。
软引用:将对象用SoftReference软引用类型的对象包裹,一般不被回收,当GC后发现没有新空间存放对象时,则会将这些软引用的对象回收掉。
弱引用:GC后直接回收掉,一般很少用。
虚引用:最弱的引用关系,一般不用。
其他问题
finalize()方法最终判定对象是否存活
- 第一次标记并进行一次筛选:筛选条件是此对象是否覆盖finalize方法,没有直接回收
- 第二次标记:执行finalize方法(只执行一次),只要重新与引用链上任意对象建立关联可复活,一般不用。
如何判断一个类是无用的类
- 该类所有实例已被回收
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有引用,无法通过反射访问该类方法
注:满足上述条件在full gc时会回收方法区的类信息,其中的条件2很苛刻,一般在自定义类加载器中ClassLoader会有回收的情况(ex:jsp热加载)。