首页 > 代码库 > Java 内存管理白皮书
Java 内存管理白皮书
1. 垃圾回收器
职责
- 分配内存
- 保证有引用对象不被回收
- 保证无引用对象被回收
设计方式
串行(Serial)与并行(Parallel)
串行的回收方式, 每次只能执行一种操作. 例如, 在多 cpu 的情况下, 只能有一个 cpu 来执行回收.
而并行则可以将回收任务分为多部分交给不同的 cpu 同时执行. 并行的方式速度更快, 但是会牺牲一些额外的复杂度和造成一些潜在的内存碎片
Concurrent vs Stop-the-world
Stop-the-world 的方式回收垃圾, 会使得程序的执行完全被挂起, 知道回收完成. 而 Concurrent 的方式则可以让垃圾回收任务与程序同时运行.Concurrent 只有小部分情况下会有短暂的 stop-the-world 的行为. Stop-the-world 的垃圾回收则比 Concurrent 更简单, 因为此时堆空间是静止的, 里面的对象不会发生改变. 而 Concurrent 的暂停时间会更短, 但是堆内对象在垃圾回收时还会更新, 所以他会有额外的性能开销, 并且需要更大的堆空间
压缩(Compacting) vs 非压缩(Non-compacting) vs 复制(Copying)
采用压缩的方式, 在垃圾回收期决定了哪些对象是存活的, 哪些是垃圾以后, 他会将存活都放在一起, 让后回收垃圾对象. 这样在下次分配内存空间的时候会更高效.
而非压缩的方式, 则是将垃圾对象原地释放(并不会移动存活的对象来创造连续的内存空间). 他的优点是回收速度更快, 但是会造成内存碎片, 使得分配内存的代价更大, 因为在分配的时候需要去搜索堆空间找到一块大内存来容纳一个大的对象.
复制回收器则会将存活对象都复制到一块新的空间, 优点是源内存空间可以被视为是空的, 回收更快, 缺点是需要额外的内存空间.
性能指标
- 吞吐量(Throughput) - 非垃圾回收时间占总时间百分比
- 垃圾回收开销(Garbage collection overhead) - 与吞吐量相反, 垃圾回收时间占总时间百分比
- 暂停时间(Pause time) - 垃圾回收期间程序暂停的时间
- 回收频率(Frequency of collection) - 垃圾回收频率
- 资源占用(Footprint) - 参数的大小, 例如堆大小
- 即时性(Promptness) - 对象变为垃圾到这块内存变为可用的时间
对于交互式程序来说, 更重要的是低暂停时间, 而非交互式程序则更关心总的垃圾回收时间.
2. J2SE 5.0 HotSpot JVM 的垃圾回收器
HotSpot 的分代机制
HotSpot JVM 中的内存被分为三代: young generation, old generation, permanent generation. 绝大部分对象的初始化都在 young 区. Old 则包含多次年轻代垃圾回收后存活下来的对象, 也包括一些无法分配到 young 中的大对象, 他们会被直接分配到 old. Permanent 则是包括一些 JVM 需要查找的便利信息, 例如对象类方法描述信息.
Young 由 Eden 和两个 Survivor 组成, 如图所示. 绝大部分对象在 Eden 初始化, 部分大对象会直接分配到年老代. Survivor 用来保存在 young 垃圾回收后存活下来的对象. 任何时候, survivor 区都是一个保存了上述的存活对象(标记为 From), 一个是空的, 空的留作下次回收时使用.
垃圾回收类型
每当 young 满了, 会进行一次 young generation collection (又叫 minor collection), 这次回收只针对 young 区. 每当 old 或者 permanent 满了, 会进行一次 full collection (又叫 major collection). 这次回收会对所有代进行回收. 一般来说, young 回收首先发生, 它使用专门对 young 设计的算法, 这个算法对 young 区最高效. 然后会使用 old 区回收算法对 old 和 permanent 进行回收. 而如果有压缩, 则每个代都会分别进行压缩.
有时候 old 区会因为数据太多造成放不下从 survivor 传过来的对象, 这时候, 除了 CMS 回收器以外, 会发生一次使用 old 回收算法对整个堆的回收. (CMS old 区回收算法是特殊情况, 它无法对 young 区进行回收)
快速内存分配
从大块的内存中分配空间效率是非常高的, 因为它使用了 bump-the-pointer 技术. 因为它会使用指针一直追踪最后一次内存分配的对象, 当新的分配发生时, 只需要检查剩余的内存是否足够, 然后移动指针并初始化对象.
对于多线程程序, 分配操作必须保证线程安全, 而使用全局锁则会造成性能瓶颈. HotSpot JVM 使用了一个叫做 Thread-Local Allocation Buffers (TLABs) 的技术. 它通过给每个线程分配自己的线程缓存来提高内存分配效率. 由于他们可以从自己的缓存中分配内存, 所以不需要全局锁, 配合 bump-the-pointer 技术则可以很快的分配内存. 只有在 TLAB 满了的时候, 需要申请更多的空间的时候才需要全局锁.
串行回收器
使用串行回收器, young 和 old 的回收会使用单个 cpu 并用 stop-the-world 的方式串行执行.
1) Young 区的串行回收
下图是 young 区回收示意图. Eden 中的存活对象会被复制到标记为 To 的 Survivor 区 (除了那些过大的对象, 会被直接复制到 Old 区). 同样, 标记为 From 的 Survivor 区的存活对象也会被复制到 To 区, 而 From 中足够老的对象则会复制到 Old 区.
如果 To 区已经满了, Eden 和 From 中还存活的对象则会直接复制到 Old 区. 在复制完成以后, 那些留在 Eden 和 From 中没有被复制的对象都会被标记为垃圾.
当 young 回收完成后, Eden 和原来的 From 都会被清空, 只有原来的 To 会存在存活的对象. 然后 From 和 To 会相互交换角色 (From 变成 To, To 变成 From). 如图
2) Old 区的串行回收
Old 区的串行回收使用 mark-sweep-compact 算法. Mark 阶段, 回收器标记所有存活的对象. Sweep 阶段, 回收器扫描所有代, 并识别出哪些是垃圾. 最后回收器会进行滑动压缩 (sliding compaction), 将存活对象都压缩到 old 区的起始端, 将空闲空间留在相反的一端. 这样在下次分配内存的时候就可以使用 bump-the-pointer 技术快速分配了. 如图
3) 什么时候使用串行回收
串行回收一般用于对暂停时间需求不高的客户端机器. 目前的硬件条件, 大部分64MB堆空间的程序可以再半秒左右的时间完成一次 full collection. (注: 文章写于 2008 年)
4) 启用串行回收器
串行回收器在非服务端机器上默认启用, 也可以通过参数 -XX:+UseSerialGC 启用.
并行回收器
To be continued