hostpot虚拟机对象探究
jvm虚拟机创建对象的流程
ava虚拟机(JVM)创建对象的过程包括以下步骤:
- 类加载: 首先,JVM会检查对象的类是否已经被加载。如果该类还没有被加载,JVM会通过类加载器加载该类的字节码文件。
- 内存分配: 一旦类加载完成,JVM会在堆内存中为对象分配内存空间。Java的对象通常存储在堆中,堆是Java虚拟机管理的一块内存区域。
- 初始化零值: 在分配内存后,JVM会对对象进行初始化。这包括将对象的实例变量设置为默认的零值(数值类型为0,布尔类型为false,引用类型为null)。
- 构造方法调用: 接着,JVM会调用对象的构造方法(如果有的话)。构造方法负责完成对象的初始化工作,可以对实例变量进行赋值,执行其他必要的初始化操作。
- 对象引用: 一旦对象成功初始化,JVM将返回一个指向该对象的引用。这个引用可以被赋给类变量、实例变量、局部变量等,使得程序可以通过引用访问和操作这个对象。
需要注意的是,对象的内存分配和初始化是原子操作的,这意味着在对象初始化完成之前,其他线程无法看到或访问到这个对象。这有助于确保多线程环境
此外,Java虚拟机可能会对对象的内存分配和回收采取一些优化措施,如对象池、分代垃圾收集等,以提高内存利用率和程序性能。
java对象的内存分布
-
Java 对象头包括两个部分:Mark Word 和 Class Pointer,对于数组对象对象头还包括数组长度(Length),下面具体看一下每个部分:
对象头
Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特。
包含两部分类信息:
- 存储对象自身运行时数据,哈希码,Gc分代年龄,锁状标志,线程持有的锁,偏向线程ID,偏向时间戳
- 实例数据(Instance Data)
- 对齐填充
Class Pointer
这部分是一个类型指针,即对象指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。
并不是所有的虚拟机实现对象头都具有类型指针,这和对象的访问定位方式有关,主流的访问方式主要有使用句柄和直接指针两种:
使用句柄的方式:Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
直接指针的方式:Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问 的开销(HotSpot 虚拟机采用该方式,所以对象头中有类型指针用于存放对象结构的引用)
length
如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
实例数据
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在 Java 源码中定义顺序的影响。 HotSpot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 HotSpot 虚拟机的 +XX:CompactFields 参数值为 true(默认就为 true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
这里指的是子类在继承父类的字段后,把原本填充的空间放上小的字段类型数据
对齐填充
对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。对象头部分已经被精心设计成正好是 8 字节的倍数(1 倍或者 2 倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
垃圾收集(GC)器和内存分配策略
-
什么是GC?
GC 是 garbage collection 的缩写,意思是垃圾回收——把内存(特别是堆内存)中不再使用的空间释放掉;清理不再使用的对象。
-
为什么要GC?
堆内存是各个线程共享的空间,不能无节制的使用。服务器运行的时间通常都很长。累积的对象也会非常多。这些对象如果不做任何清理,任由它们数量不断累加,内存很快就会耗尽。所以GC就是要把不使用的对象都清理掉,把内存空间空出来,让项目可以持续运行下去。
-
什么样的对象是垃圾对象?
不再使用或获取不到的对象是垃圾对象。
-
如何把垃圾对象找出来?
办法1:引用计数法(不采用,不能解决循环引用问题)
办法2:可达性分析(从GC Roots对象出发,不可达的对象就是要清理的对象)
-
找到垃圾对象如何执行清理?
具体的GC算法
对象已死:
在回收对象的时候 要确认对象不可能再被任何途径使用的对象(以下方法)
1.程序计数法
- 引用计数法是在对象每一次被引用时,都给这个对象专属的『引用计数器』+1。
- 当前引用被取消时,就给这个『引用计数器』-1。
- 当前『引用计数器』为零时,表示这个对象不再被引用了,需要让GC回收。
- 可是当对象之间存在交叉引用的时候,对象即使处于应该被回收的状态,也没法让『引用计数器』归零。
2.可达性分析法
核心原理:判断一个对象,是否存在从『堆外』到『堆内』的引用。
为了解决引用计数法的循环引用问题,Java 采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
引用
jdk1.2之前:如何reference类型的数据中存储的数值代表的是另一块内存的其实地址,就称该reference数据是 代表某块内存,某个对象的引用
jdk1.2后:
- 强引用:最传统的引用定义,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用:用来描述还有用,但是非必须的对象,只被弱引用关联的对象,在系统将要内存溢出前,会把这些对象列入回收范围之中,进行二次回收.如果这次回收还没有呢足够的内存,才会抛出内存溢出异常
- 弱引用:用来描述那些非必要对象,比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉被弱引用关联的对象
- 虚引用:也称"幽灵引用"或者"幻影引用",它是最弱的一种引用关系作用是:在这个对象被回收掉 的时候收到一个系统通知
生存还是死亡
在可达性分析算法中判定为不可达的对象,也不是‘非死不可‘在清理之前会进行两次标记,如过没有发现可达链,则会被标记一次
随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法
回收方法区
主要是对常量池的回收和对类的卸载。
在大量使用反射、动态代理、CGLib 、动态生成 JSP 以及 OSGi 场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件:
- 堆中不含有该类的任何实例
- 该类的classloader被回收
- 该类对应的class对象没有被任何地方所引用,即没有通过反射访问该类方法
不过满足也不一定被卸载,可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
垃圾回收算法
- 引用计数式垃圾收集
- 追踪式垃圾收集
1.标记-清除
将存活的对象标记,然后清除未标记的对象。
缺点:标记、清除效率不高,且会产生不连续的内存碎片,导致无法给大对象分配内存
2.标记-整理
所有存活对象都向一端移动,然后直接清理掉端边界外的对象。实现了内存连续
3.复制
将内存划分为两块,每次只使用其中一块,内存用完将存活的对象复制到另外上,然后进行清理。
HotSpot虚拟机中Eden和S区比例是:8:1:1,内存的利用率达到90%,如果每次回收有多于10%的存活对象,此时要借用老年代来存储放不下的对象
4分代收集
根据对象存活周期将内存划分为几块,不同区域采用不同算法。
一般堆分为新生代和老年代:新生代使用复制算法;老年代使用标记-清除或标记-整理算法。