💪
AndroidCollect
  • 写在前面
  • 计算机基础
    • 计算机组成原理
    • 算法
      • 查找
        • 二分查找
      • 排序
        • 简单排序
        • 高级排序
        • 特殊排序
      • 海量数据
      • 思想
        • 贪心
        • 分治
        • 动态规划
        • 回溯
      • 哈希算法
    • 数据结构
      • 队列
        • 知识点
        • 相关题目
          • 用两个栈实现队列
          • 实现循环队列
          • 用链表实现队列
          • 用数组实现队列
      • 栈
        • 相关算法题目
          • 用链表实现栈
          • 用数组实现栈
      • 链表
        • 知识点梳理
        • 相关算法题目
          • 删除倒数第n个结点
          • 合并两个有序链表
          • 检测单链表是否有环
          • 获取中间结点
          • 反转链表
      • 跳表
      • 哈希表
      • 树
        • 二叉树
        • 二叉查找树
        • AVL 树
        • Trie 树
        • 红黑树
      • 堆
        • 存储
        • 堆的应用
      • 图
    • 网络
      • 应用层协议
        • DNS
        • HTTP
        • HTTPS
      • 传输层协议
        • TCP
        • UDP
      • 输入网址后发生了什么
    • 操作系统
      • 内存
    • 数据库
  • 软件工程
    • 编程思想
    • 设计模式
      • 状态模式
      • 装饰器模式
      • 代理模式
      • 责任链模式
      • 建造者模式
      • 单例模式
      • 观察者模式
  • Java
    • 基础
    • 异常
    • 并发编程
      • ThreadLocal
      • 线程池
      • 理解 volatile
      • AbstractQueuedSynchronizer
    • 集合
      • LinkedHashMap 源码
      • HashMap 源码
    • 注解
    • 反射
      • JDK 动态代理
    • JVM
      • 自动内存管理机制
      • Class 文件格式
      • 类加载机制
      • Java 内存模型(JMM)
      • 字节码指令
      • HotSpot 虚拟机实现细节
    • 源码与原理
    • 各版本主要特性
  • Android
    • 基础组件
      • Context
      • Activity
        • 生命周期
        • 启动模式与任务栈
        • 启动流程
      • Service
      • ContentProvider
      • BroadcastReceiver
      • Fragment
      • View
        • 常用控件问题总结
          • RecyclerView
          • ViewPager2
        • CoordinatorLayout
        • SurfaceView
        • 事件分发
        • 绘制流程
        • 自定义 View
        • Window
    • 数据存储
      • 存储结构
      • Sqlite
      • 序列化
      • SharedPreferences
    • 资源
      • 图片加载
    • 动画
      • 属性动画
    • 线程和进程
      • Binder 机制
      • 跨进程通信
        • AIDL
    • 内部原理
      • 消息循环机制
      • Binder
      • Window
      • SparseArray
      • ArrayMap
      • RecyclerView
      • App 启动流程
    • 性能优化
      • 内存
        • 内存使用优化
        • 内存泄漏
      • 启动优化
      • 缩减包大小
      • 布局优化
      • ANR
    • 打包构建
      • dex 文件
      • APK 打包流程
      • APK 签名流程
    • 架构
      • 运行时
      • Android 系统架构
      • 应用项目架构
    • 开源框架源码或原理
      • RxJava
        • 使用笔记
        • 源码解析
      • Retrofit
      • ButterKnife
      • BlockCanary
      • LeakCanary
      • OkHttp
      • 图片加载
        • Glide
        • Picasso
    • 碎片化处理
      • 屏幕适配
    • 黑科技
      • 热修复
    • Jetpack
      • Lifecycle
      • Room
      • WorkManager
    • 新动态
      • AndroidX
      • 各系统版本特性
  • 开发工具
    • 正则表达式
    • ADB
    • Git
  • Kotlin
  • Flutter
  • 关于作者
  • 致谢
由 GitBook 提供支持
在本页
  • 对象的创建
  • 为对象分配空间的方法
  • 如何保证内存分配的线程安全
  • 对象的内存布局
  • 对象头
  • 实例数据
  • 对齐填充
  • 对象的访问定位
  • 句柄访问
  • 直接指针
  • 内存回收算法实现
  • 枚举根节点
  • 安全点(Safe Point)
  • 安全区域(Safe Region)

这有帮助吗?

  1. Java
  2. JVM

HotSpot 虚拟机实现细节

对象的创建

  1. new 指令

  2. 2. 检查指令参数能否在常量池中定位到一个类的符号引用

  3. 检查符号引用对应的类是否已经加载、解析和初始化过(如果没有需先进行类加载过程)

  4. 虚拟机为新生对象分配内存(内存大小在类加载完成后便可以确定)

  5. 将内存空间初始化为零值

  6. 对对象进行必要配置(属于哪个类的实例、对象的哈希码、对象的GC分代年龄,这些信息存放在对象头(Object Header)中)

  7. 执行对象的构造方法<init>

为对象分配空间的方法

为对象分配空间的任务等同于把一块大小确定的内存从 Java 堆中划分出来。

假设 Java 堆中的内存时绝对规整的,用过的内存放在一边,未用过的内存 放在另一边,中间存放着一个指针作为分界点的指示器,则分配内存就是把指针往空闲的一侧移动与对象大小相等的距离,这种分配方式成为「指针碰撞」(Bump the pointer)。

如果 Java 堆中内存不是规整的,已使用的内存和未使用的内存相互交错,那就没有办法应用指针碰撞了,虚拟机必须维护一个列表,记录哪些内存块是可用的,当分配空间时,找一块足够大的空间分配给内存实例,并更新列表上的记录,这种分配方式成为「空闲链表」(Free List)。

选择哪种分配方式由Java堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

如何保证内存分配的线程安全

对象创建在虚拟机中是非常频繁的行为,即使是修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及移动,对象 B 又使用原来的指针分配内存的情况。

解决方案:

  • 对分配内存空间的动作进行同步处理——虚拟机采用CAS和失败重试保证更新操作的原子性

  • 把内存分配的动作按照线程划分在不同的空间中进行,每个线程预先在堆中分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),线程的内存分配在自己的 TLAB 上进行,只有TLAB用完并分配新的 TLAB 时,才需要同步锁定。虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB 来配置

对象的内存布局

在 HotSpot 虚拟机中,对象在内存中存储的区域分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

对象头分为两部分,第一部分用于存储自身的运行时数据:如哈希吗、GC分代年龄、锁状态标志、线程持有的锁等,这部分数据在 32 位 和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方成为"Mark Word"。考虑到空间效率,Mark Word 被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

并不是所有的虚拟机实现都需要在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不一定要经过对象本身。

另外,如果对象类型是 Java 数组,那对象头中还要有一块记录数组长度的信息。因为虚拟机可以通过普通对象的元信息知道对象的大小,但是从数据的元数据信息中无法获得数组的大小。

实例数据

实例数据部分是对象存储的有效信息,即代码中定义的各种类型的字段内容,包括从父类继承的字段。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义的顺序的影响。

HotSpot 默认的分配策略为 longs/doubles 、ints、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers),也就是把相同宽度的字段分配到一起。在满足这个条件的前提下,在父类中定义的字段会出现在子类之前。如果 CompactFields 设置为 true(默认为true),那么子类中较窄的变量会插入到父类变量的间隙中。

对齐填充

对齐填充不是必然存在的,仅仅起着占位符的作用。由于 HotSpot VM 的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象的头部数据刚好是8字节的整数倍,所以当实例数据部分没有对齐时,需要进行填充。

对齐的好处是方便 CPU 对内存数据的读取。

对象的访问定位

Java 程序需要通过栈上的引用(reference)数据来操作堆上的具体对象,虚拟机规范中并没有规定引用该以何种方式定位、访问堆中的对象。目前主流的访问方式有使用句柄和直接指针两种。

句柄访问

Java 堆中划分出一块区域作为句柄池,而引用中存储的是对象的句柄的地址,句柄中包含了类的实例数据和类型数据的地址信息。

好处是 reference 中存储的是对象的句柄地址,在对象被移动(垃圾回收)时,不用更新reference的数据,只需要修改句柄中的地址。

缺点多了一次指针定位的时间开销。

直接指针

引用中存储的是对象在堆中的地址,对象布局中需要放置访问类型数据的相关信息。

好处:相比使用句柄速度快。

缺点:对象移动时要同时更新引用的值。

HotSpot VM 使用直接指针的方式访问对象。

内存回收算法实现

枚举根节点

可达性分析必须在一个能确保一致性的快照中进行,「一致性」的意思是在分析过程中不能出现引用关系还在不断发生变化的情况。这也是为什么GC进行时要停止所有 Java 执行线程(Stop the World),即使是在号称不会发生停顿的 CMS 收集器中,枚举根节点时也是必须要停顿的。

目前主流虚拟机采用的都是准确式GC,当执行系统停下来后,并不需要一个不漏的检查完所有执行上下文或全局引用,虚拟机有办法直接知道哪些地方存放着对象的引用。在 HotSpot 实现中,是使用一组 OopMap 的数据来达到这个目的,在类加载完成的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样GC 在扫描时就可以得知这些信息。

安全点(Safe Point)

为了节省空间,虚拟机不能为每条指令都生成 OopMap,所以只能在「特定位置」记录信息,这些位置成为安全点(Safe Point)。也就是说,程序不能再任何地点都能停顿下来进行GC,只有到达安全点时才能暂停。

安全点的选择以程序「是否有让程序长时间运行的特性」为标准进行选择,由于每条指令的执行时间都很短暂,程序不太可能因为指令流太长而长时间运行。「长时间运行」的明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等。

对于如何保证发生GC时所有线程都在安全点上,有两种方式:

抢先式中断

在 GC 发生时,停止所有线程,如果有线程没有执行到安全点,则恢复该线程让它执行到安全点。

主动式中断

当需要中断线程时,不对线程进行操作,而是设置一个标志,各个线程去轮训这个标志,如果标志为真则中断自己,轮询标志的地方和安全点是重合的,还要加上创建对象需要分配内存的地方。

安全区域(Safe Region)

安全点无法解决线程不执行(sleep、被挂起)时候的情况,这个时候线程无法响应 JVM 的中断请求,执行到安全点再中断,这种情况需要使用安全区域解决。

安全区域就是指在一段代码之中,引用关系不会发生变化,在这个区域的任何地方执行GC都是安全的。

在线程执行到安全区域时,首先标识自己已经执行到了安全区域,如果这时 JVM 要发起 GC 时,就不用管已经标识自己进入安全区域的线程了。在线程离开安全区域时,检查系统是否已经完成了根节点枚举,如果完成,线程继续执行,否则需要等到可以离开安全区域的通知为止。

上一页字节码指令下一页源码与原理

最后更新于5年前

这有帮助吗?

通过句柄访问对象
通过直接指针访问对象