JVM(二) -- 对象详解

273 阅读6分钟

一:基础概述

JVM(一) 内存分配文章中谈了虚拟机运行时内存区域的划分,其中堆作为最大的内存区域存储几乎所有对象实例。本文将从对象结构、内存分配、对象访问、死亡判断、引用类型等几方面针对对象进行系列阐述,更深层次认识面向对象到底面向的是什么

二:对象结构

在这里插入图片描述

2.1 对象头区域

划分为三块区域,其中类型指针与数组长度都比较特殊,有可能并不存在该内存区域

  • 哈希码、GC年龄、偏向锁标志等信息为对象自身运行数据信息
  • 类型指针区域并不一定存在,若采用句柄方式访问则无该内存区域
  • 数组长度通过数组元数据并不能判断,所以数组对象会在对象头维护数组长度数据
2.2 实例数据、对齐填充

实例数据区域存储对象有用信息,如定义的对象属性值等信息。至于对齐填充区域也是不一定存在的内存,因为HotSpot虚拟机要求对象大小必须是8整数倍,所以当对象头与实例数据不满足时需要这块区域补充

三:内存分配

对象创建后需要在堆中分配内存,内存分配方式是什么?内存分配会不会有线程安全问题?如何解决线程安全问题。对象分配位置在哪?所有新生对象都分配到新生代?新生代对象如何进入老年代等等问题

3.1 指针碰撞

在这里插入图片描述
假设堆内存区域规则,当某对象需要分配内存时只需要移动标记指针分配相对大小内存区域即可。当然前提是垃圾收集器具备空间整理功能,如Serial、ParNew等

3.2 空闲列表

针对内存区域不规则采用的方案是使用空闲列表,维护一个空闲列表记录空闲内存区域,当某对象需要分配内存时检索表中合适区域进行分配即可

3.3 线程安全

指针碰撞亦或是空闲列表修改对应内存使用状态都需要时间,有可能在高并发状况下导致线程不安全问题。HotSpot虚拟机中采用CAS重试机制保证线程安全,还有一种方案就是利用栈空间的线程私有特性。提前为每个线程栈划分一块内存专门使用,当划分内存区域使用完毕申请新内存区域时再针对这部分操作加同步锁即可

3.4 限制最大对象

在这里插入图片描述
首先明确一点就是新生对象默认都会分配到新生代Eden,但是通过参数-XX:PretnureSizeThreshold可以设置当对象超过该数值时会分配都老年代。但是注意该参数在Parallel Scavenge 收集器中无效

3.5 长期存活对象

在这里插入图片描述
新生代中采用复制算法进行垃圾回收,当某些对象存活时间较长一直在新生代中复制移动也会导致性能的损耗。所以,HotSpot中可以使用参数-XX:MaxTenuringThreshold限制当某个对象存活多少轮GC后当GC再次发生时直接进入老年代。默认值15

3.6 动态检测

动态年龄晋升依靠参数-XX:TargetSurvivorRatio 控制,默认百分之五十。Survivor区域对象从年龄小 --> 大开始累加,直到某个年龄的对象加入后 总内存 > Survivor内存 * TargetSurvivorRatio,则将大于等于该年龄的所有对象加入老年代

四:对象访问

栈空间局部变量表中记录引用类型对象都是维护引用地址,那么如何通过这个引用获取对象实例?两种方法分别是句柄访问、直接引用

4.1 句柄访问

在这里插入图片描述
句柄访问方式就是在堆空间中开辟一块内存用于存放句柄,引用对象通过句柄连接访问获取到对象类型以及对象实例

4.2 直接访问

在这里插入图片描述
前面讲对象结构时提到过对象头中可能存在类型指针区域,该区域用于标记对象类型。直接指针的方式相对于句柄池访问少了一次指针引用,在对象访问频繁的基础下可以节约不少性能消耗。但是当对象内存地址改变时直接指针需要修改reference引用,句柄访问只需要修改句柄即可。HotSpot中采用直接访问的方式访问对象

五:死亡判断

对象创建后自然需要销毁释放内存空间,对象销毁的过程就是GC操作。虚拟机如何判断某个对象是否需要进行垃圾回收,当然判断方式有引用计数算法和可达性算法分析两种。直接引用就是通过维护计数器实现,并不能解决对应循环引用的问题。HotSpot采用可达性算法分析

5.1 可达性算法分析

根据GCRoots搜索存活拥有引用关系的对象,其余对象则为可回收对象。形象理解就是A为GCRoot,A引用B,B引用C/D。那么A、B、C、D都是存活对象,如下图所示

在这里插入图片描述

5.2 GCRoots

前面提到GCRoots概念,每个GCRoot本质上都是一个Java对象,虚拟机将哪些对象作为GCRoot?个人理解就是虚拟机确定一定活跃的对象类型,如下所示:

  • 虚拟机栈引用对象
  • 方法区静态变量、常量引用对象
  • 本地方法栈引用对象
5.3 可达性算法证明

在这里插入图片描述

六:引用类型

直接通过new创建对象实例则称之为强引用,该引用类型的对象只有在不可达时才能进行回收。但是某些对象不是很重要的情况下当内存空间不足GC即可回收就需要定义为其它引用类型。总结就是对象引用类型可以影响GC回收行为

6.1 强引用

在这里插入图片描述
垃圾回收也不能释放强引用占用内存,这时候因为内存不足而抛出OOM错误。证明强引用对象只有在不可达时才能被回收

6.2 软引用

在这里插入图片描述
看最后一个GC日志可以很清晰的看到发生Full GC回收老年代大概9M内存,这块内存就是前面软引用对象占用内存。也就证明软引用在内存不足发生GC时会释放内存被回收

6.3 弱引用

在这里插入图片描述
当执行System.gc()时GC日志显示回收9M左右内存空间,这时内存明显是充足的。因为对象引用关系为弱引用,只要发生GC即回收对象释放内存空间

6.4 虚引用

在这里插入图片描述
随时可以被回收的一种引用关系,重点记住通过get()不能获取到对象实例。与引用队列配合当对象被回收时会将引用关系放入队列中,通过比较队列中元素与虚引用关系是否相等即可得到结论