1,JVM 运行时内存分布
垃圾回收针对的是 Heap(堆区), 因为运行时所有的对象实体都是存在这个区域的.
2, 垃圾回收算法
2.1, 引用计数法
1, 它的做法是为每个对象添加一个引用计数器, 用来统计指向该对象的引用个数, 当对象的引用个数为 0 的时候, 就可以回收了.
2, 引用计数法还有一个重大的漏洞, 那便是无法处理循环引用对象, 比方说 A 引用 B,B 引用 A, 但是 A 和 B 都没有其他引用, 那么这个时候就回造成对象无法回收.
2.2, 可达性分析
这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set), 然后从该合集出发, 探索所有能够被该集合引用到的对象, 并将其加入到该集合中, 这个过程我们也称之为标记(mark). 最终, 未被探索到的对象便是死亡的, 是可以被回收的.
那么, 什么是 GC roots 呢? 比如
1,Java 方法栈桢中的局部变量;
2, 已加载类的静态变量;
3, 已启动且未停止的 Java 线程.
3,Stop-the-world
为了避免在垃圾回收的过程中, 由于还有应用线程在运行, 导致内存状态的改变, 而引起的错误回收. 在 Java 虚拟机里, 采用的是一种简单粗暴的方式, 那便是 Stop-the-world, 停止其他非垃垃圾回收线程的工作, 直到完成垃圾回收. 这也就造成了垃圾回收所谓的暂停时间(GC pause).
Java 虚拟机中的 Stop-the-world 是通过安全点 (safepoint) 机制来实现的. 当 Java 虚拟机收到 Stop-the-world 请求, 它便会等待所有的线程都到达安全点, 才允许请求 Stop-the-world 的线程进行独占的工作.
4, 垃圾回收的三种方式
4.1, 直接清除
把死亡对象所占据的内存标记为空闲内存
1, 造成内存碎片, 内存不连续, 对象内存重新分配的时候效率低下.
2, 极端情况下, 会造成总内存足够, 但是对象无法分配内存.
4.2, 压缩
清除死亡对象, 把存活的对象聚集到内存区域的起始位置
1, 压缩算法存在性能开销
2, 对象的移动, 涉及到内存地址的变化, 需要更新引用地址.
4.3, 复制
把内存区域分为两等分, 分别用两个指针 from 和 to 来维护, 并且只是用 from 指针指向的内存区域来分配内存, 当发生垃圾回收时, 便把存活的对象复制到 to
指针指向的内存区域中, 并且交换 from 指针和 to 指针的内容.
1, 能够解决内存碎片化的问题
2, 堆空间的使用效率极其低下
5,JVM 堆划分
Java 虚拟机将堆划分为新生代和老年代. 其中, 新生代又被划分为 Eden 区, 以及两个大小相同的 Survivor 区.
通常来说, 当我们调用 new 指令时, 它会在 Eden 区中划出一块作为存储对象的内存. 由于堆空间是线程共享的, 因此直接在这里边划空间是需要进行同步的.
否则, 将有可能出现两个对象共用一段内存的事故.
Java 虚拟机的解决方法是 TLAB 技术(Thread Local Allocation Buffer, 对应虚拟机参数 - XX:+UseTLAB, 默认开启).
1, 每个线程可以向 Java 虚拟机申请一段连续的内存, 比如 2048 字节, 作为线程私有的 TLAB.
2, 这个操作需要加锁, 线程需要维护两个指针, 一个指向 TLAB 中空余内存的起始位置, 一个则指向 TLAB 末尾.
3,new 指令, 便可以直接通过指针加法 (bump the pointer) 来实现, 即把指向空余内存位置的指针加上所请求的字节数. 如果加法后空余内存指针的值仍小于或等于指向末尾的指针, 则代表分配成功. 否则, TLAB 已经没有足够的空间来满足本次新建操作. 这个时候, 便需要当前线程重新申请新的 TLAB.
6, 新生代的 GC, 也成 Minor GC 或者 Young GC
1, 当 Eden 区的空间耗尽, 这个时候 Java 虚拟机便会触发一次 Minor GC, 来收集新生代的垃圾. 存活下来的对象, 则会被送到 Survivor 区.
2, 新生代共有两个 Survivor 区, 我们分别用 from 和 to 来指代. 其中 to 指向的 Survivior 区是空的. 当发生 Minor GC 时, Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中, 然后交换 from 和 to 指针, 以保证下一次 Minor GC 时, to 指向的 Survivor 区还是空的.
3,Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次. 如果一个对象被复制的次数为 15(对应虚拟机参数 - XX:+MaxTenuringThreshold), 那么该对象将被晋升 (promote) 至老年代.
4, 如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 - XX:TargetSurvivorRatio), 那么较高复制次数的对象也会被晋升至老年代.
5, 发生 Minor GC 时, 应用了标记 - 复制算法, 将 Survivor 区中的老存活对象晋升到老年代, 然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中. 理想情况下, Eden 区中的对象基本都死亡了, 那么需要复制的数据将非常少, 因此采用这种标记 - 复制算法的效果极好.
6,Minor GC 的另外一个好处是不用对整个堆进行垃圾回收.
但是, 它却有一个问题, 那就是老年代的对象可能引用新生代的对象. 也就是说, 在标记新生代存活对象的时候, 我们需要扫描老年代中的对象. 如果该老年代对象拥有对新生代对象的引用, 那么这个老年代对象也会被作为 GC Roots.
这样一来, 又做了一次全堆扫描呢?
7, 卡表(Card Table)
HotSpot 给出的解决方案是一项叫做卡表 (Card Table) 的技术.
1, 该技术将整个堆划分为一个个大小为 512 字节的卡, 并且维护一个卡表, 用来存储每张卡的一个标记. 这个标识位代表对应的卡是否可能存有指向新生代对象的引用. 如果可能存在, 那么我们就认为这张卡是脏卡.
2,Minor GC 的时候, 可以不用扫描整个老年代, 而是在卡表中寻找脏卡, 并将脏卡中的对象加入到 Minor GC 的 GC Roots 里. 当完成所有脏卡的扫描之后, Java 虚拟机便会将所有脏卡的标识位清零.
3,Minor GC 伴随着存活对象的复制, 而复制需要更新指向该对象的引用. 因此, 在更新引用的同时, 又会设置引用所在的卡的标识位. 这个时候, 我们可以确保脏卡中必定包含指向新生代对象的引用.
4, 在 Minor GC 之前, 我们并不能确保脏卡中包含指向新生代对象的引用.
8, 垃圾回收器
新生代的垃圾回收器共有三个:
1,Serial,Parallel Scavenge 和 Parallel New.
2, 这三个采用的都是标记 - 复制算法.
3, 其中, Serial 是一个单线程的, Parallel New 可以看成 Serial 的多线程版本. Parallel Scavenge 和 Parallel New 类似, 但更加注重吞吐率. 此外, Parallel Scavenge 不能与 CMS 一起使用.
老年代的垃圾回收器也有三个:
1,Serial Old 和 Parallel Old, 以及 CMS.
2,Serial Old 和 Parallel Old 都是标记 - 压缩算法. 同样, 前者是单线程的, 而后者可以看成前者的多线程版本.
3,CMS 采用的是标记 - 清除算法, 并且是并发的. 除了少数几个操作需要 Stop-the-world 之外, 它可以在应用程序运行过程中进行垃圾回收.
来源: http://www.bubuko.com/infodetail-3147604.html