JVM内存管理机制( 三 )


1 , 指针碰撞(Bump the pointer)
Java堆中的内存是规整的 , 所有用过的内存都放在一边 , 空闲的内存放在另一边 , 中间放着一个指针作为分界点的指示器 , 分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离 。例如:Serial、ParNew等收集器 。
2 , 空闲列表(Free List)
Java堆中的内存不是规整的 , 已使用的内存和空闲的内存相互交错 , 就没有办法简单的进行指针碰撞了 。虚拟机必须维护一张列表 , 记录哪些内存块是可用的 , 在分配的时候从列表中找到一块足够大的空间划分给对象实例 , 并更新列表上的记录 。例如:CMS这种基于Mark-Sweep算法的收集器 。并发处理:
对象创建在虚拟机中是非常频繁的行为 , 即使是仅仅修改一个指针所指向的位置 , 在并发情况下也并不是线程安全的 , 可能出现正在给对象A分配内存 , 指针还没来得及修改 , 对象B又同时使用了原来的指针来分配内存的情况 。处理方案有2种:
1 , 同步处理
对分配内存空间的动作进行同步处理 , 实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性 。
2 , TLAB
把内存分配的动作按照线程划分在不同的空间之中进行 , 即每个线程在Java堆中预先分配一小块内存 , 称为本地线程分配缓冲(Thread Local Allocation Buffer , TLAB) 。那个线程要分配内存 , 就在哪个线程的TLAB上分配 , 只有TLAB用完并分配新的TLAB时 , 才需同步锁定 。
3.内存空间初始化
虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB , 这一工作过程也可以提前至TLAB分配时进行 。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用 , 程序能访问到这些字段的数据类型所对应的零值 。
4.对象设置
接下来 , 虚拟机要对对象进行必要的设置 , 例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息 。这些信息存放在对象的对象头中 。
5.执行init()
在上面的工作都完成之后 , 从虚拟机的角度看 , 一个新的对象已经产生了 。但是从Java程序的角度看 , 对象的创建才刚刚开始init()方法还没有执行 , 所有的字段都还是零 。
所以 , 一般来说(由字节码中是否跟随invokespecial指令所决定) , 执行new指令之后会接着执行init()方法 , 把对象按照程序员的意愿进行初始化 , 这样一个真正可用的对象才算产生出来 。
对象内存布局
对象的内存结构又可以被分为:对象头 , 实例数据 , 对象填充 。
对象头
对象头包括两部分信息:
1 , 用于存储对象自身的运行时数据 ,  如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 , 这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits , 官方称它为“Mark word” 。
存储内容标志位状态对象哈希码、对象分代年龄01未锁定指向锁记录的指针00轻量级锁定指向重量级锁的指针10膨胀(重量级锁定)空 , 不需要记录信息11GC标记偏向线程ID、偏向时间戳、对象分代年龄01可偏向 。
2 , 类型指针 , 即是对象指向它的类的元数据的指针 , 虚拟机通过这个指针来确定这个对象是哪个类的实例 。
实例数据
对象真正存储的有效信息 , 也是在程序代码中定义的各种类型字段内容 。无论是从父类继承下来的还是子类定义的 , 都需要记录下来 。
对象填充
没有实际意义 , 仅仅起着占位符的作用 。以为对象的大小必须是8字节的整数倍 。
对象访问定位
建立对象是为了使用对象 , 我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象 。由于在Java虚拟机规范里面只规定了reference类型是一个指向对象的引用 , 并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置 , 对象访问方式也是取决于虚拟机实现而定的 。


推荐阅读