Java 虚拟机的堆划分
Java 虚拟机将堆划分为新生代和老年代. 其中新生代又被划分为 Eden 区, 以及两个大小相同的 Survivor 区.
默认情况下, Java 虚拟机采取一种动态分配的策略, 根据对象生成的速率, 以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例. 也可以通过参数 -XX:SurvivorRatio 来固定这个比例. 需要注意的是, 其中一个 Survivor 区会一直为空, 因此比例越低浪费的空间越高.
当调用 new 指令时, 它会在 Eden 区中划出一块作为存储对象的内存. 由于堆内存是线程共享的, 因此直接在这里划分空间是需要进行同步的. 否则会出现两个对象公用一段内存的事故.
Java 虚拟机的解决方法是: 每个线程可以向 Java 虚拟机申请一段连续的内存, 比如 2048 字节, 作为线程私有的 TLAB. 这个操作需要加锁, 线程需要维护两个重要的指针, 一个指向 TLAB 中空余内存的起始位置, 一个则指向 TLAB 末尾.
然后通过 new 指令, 便可以直接通过指针加法来实现, 即把指向空余内存位置的指针加上所请求的字节数. 如果加法后空余内存指针的值扔小于或等于指向末尾的指针, 则代表分配成功. 否则, TLAB 以及没有足够的空间来满足本次新建操作. 这个时候, 便需要当前线程重新申请新的 TLAB.
当 Eden 区的空间耗尽了, 这个时候 Java 虚拟机便会触发一次 Minor GC, 来收集新生代的垃圾. 存活下来的对象, 则会被送到 Survivor 区. 当发生 MinorGC 时, Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中, 然后交换 from 和 to 指针, 以保证下一次 Minor GC 时, to 指向的 Survivor 区还是空的.
Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次. 如果一个对象被复制 15 次, 那么该对象将被晋升至老年代. 如果单个 Survivor 区已经被占用了 50%, 那么较高复制次数的对象也会被晋升至老年代.
Minor GC 有一个问题, 那就是老年代的对象可能引用新生代的对象. 在标记存活对象的时候, 我们需要扫描老年代中的对象. 如果该对象拥有对新生代对象的引用, 那么这个引用也会被作为 GC Roots. 这样的话, 就相当于进行了一次全堆扫描.
卡表
针对上述的问题, HotSpot 给出了一种解决方案叫做卡表. 该技术将整个堆划分为一个个大小为 512 字节的卡, 并且维护一个卡表, 用来存储每张卡的一个标示位. 这个标示位代表对应的卡是否可能存在指向新生代对象的引用. 如果可能存在, 那么我们就认为这张卡是脏的.
在进行 Minor GC 的时候, 我们不用扫描整个老年代, 而是在卡表中寻找脏卡, 并将脏卡中的对象加入到 Minor GC 的 GC Roots 里. 当完成所有脏卡的扫描之后, Java 虚拟机便会将所有脏卡的标示位清零.
上述总结介绍了用卡表这种方式解决全堆扫描效率低下的问题, 置于如何标记脏卡, 如何更新脏卡就不做深入总结了.
问答
Q: 请问 JVM 分代收集新生代对象进入老年代, 年龄为什么是 15 而不是其他的?
HotSpot 会在对象头中的标记字段里记录年龄, 分配到的空间只有 4 位, 最多只能记录到 15
Q:GC ROOT 到底指的是对象本身, 还是引用?
严格来说应该是对象. 像局部变量中存放的引用只是导致对象成为 GC roots 的原因. 我个人倾向于将这些引用作为 GC roots, 因为 GC 是从这些地方出发开始探索的. 看各人理解方便吧.
总结
本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程, 通过课后反思以及借鉴各位学友的发言总结, 现整理出自己的知识架构, 以便日后温故知新, 查漏补缺.
来源: https://www.cnblogs.com/yuepenglei/p/10325174.html