一. 概述
相比起 C 和 C++ 的自己回收内存, JAVA 要方便得多, 因为 JVM 会为我们自动分配内存以及回收内存.
在之前的 JVM 之内存管理 中, 我们介绍了 JVM 内存管理的几个区域, 其中程序计数器以及虚拟机栈是线程私有的, 随线程而灭, 故而它是不用考虑垃圾回收的, 因为线程结束其内存空间即释放.
而 JAVA 堆和方法区则不一样, JAVA 堆和方法区时存放的是对象的实例信息以及对象的其他信息, 这部分是垃圾回收的主要地点.
二. JAVA 堆垃圾回收
垃圾回收主要考虑的问题有两个: 一个是效率问题, 一个是空间碎片问题.
而 Java 堆中的垃圾回收可以分为两个区域, 一个是新生代, 一个是老年代. 其中新生代又分为一块比较大的 Eden 空间和两块较小的 Survivor 空间. 因为新生代和老年代所存储的对象群体是不一样的, 为了在效率和空间碎片问题中取得平衡, 新生代和老年代所使用的垃圾回收算法是不一样.
新生代 - 复制算法
从名字上就知道, 新生代主要存放的是比较新的对象, 回收多次之后仍然存活的对象, 就会被送到老年代中区. 由此可知新生代的垃圾回收是比较频繁的, 所以为解决效率问题, 新生代使用了复制算法. 复制算法可以将内存分为大小相等的两块, 每次分配时使用其中一块, 当这一块用完时, 就将还存活的对象复制到另一块内存上面区. 此时已使用过的这一块内存就可以一次清理掉, 这样也不用担心内存碎片的问题. 当然这种算法的一个缺点就是内存使用率比较低, 只有一半 (每次只能一半用来分配出去).
而 IBM 公司的研究表明, 新生代中的对象 98% 都是 "照生夕死", 所以不需要按照 1:1 划分, 故而会将内存分为一块较大的 Eden 空间和两块小的 Survivor 空间.
那么为什么会有两块 Survivor 呢, 复制算法不是只需要一块 Eden 和一块 Survivor 就够了吗?
其实这主要还是为了解决碎片化的问题. 假设只有一个 Survivor 区, 当 Eden 区满的时候, 进行 Gc, 存活对象被分配到了 Survivor 区, 清空 Eden 区. 当再一次 Gc 完成后, 存活的对象继续放在 Survivor 区, 这样不是很美好吗, 不会有内存碎片啊! 但是别忘了, 第一次存到 Survivor 区的对象很可能在第二次 Gc 的时候就失活了, 清理掉 Survivor 失活对象不就会产生内存碎片了吗?
所以 Java 堆使用了两个 Survivor 区, 一个 from Survivro 和一个 toSurvivor, 第一次 Eden 满的时候, 复制算法将存活对象放到 from Survivor 区, 清空 Eden. 第二次, Eden 满时, 将 Eden 和 from Survivor 区存活的对象放到 to Survivor 区, 清空 Eden 和 from Survivor, 然后重要的一步, 将 from Survivor 和 to Survivor 角色互换! 这样就解决了内存碎片化的问题.
老年代 - 标记 / 整理算法
首先要明白老年代存放的都是会存活得比较久的对象, 所以如果老年代也使用复制算法的话, 那么复制对象的开销时比较大的, 因为老年代的对象基本上都会存活.
标记 / 整理算法很好理解, 主要也就是 "标记","整理" 两个步骤, 先将要回收的对象标记, 然后让存活对象向着一端移动, 最后将边界以外的内存, 然后 Gc 完成.
三. 方法区垃圾回收
在某些地方的解释中, 方法区也会被叫做 "永久代", 与 JAVA 堆不同, 这里存放的是类的信息以及一些常量信息, 故而这个区域中被分配的内存一般比较难以被回收, 所以才有有 "永久代" 之名.
虽然方法区中垃圾回收效率较低, 但被分配的内存却也并非真的就永不被回收, 其主要回收的有两部分内容: 废弃常量和无用的类. 废弃常量的回收与 JAVA 堆中类实例回收类似, 当常量池中一个常量没有被引用时, 就有可能被回收. 比如常量池中有一个字符串常量 "abc", 当没有任何一个 String 对象值为 "abc" 时, 那么下一次垃圾回收 "abc" 常量就有可能会被回收.
而对于无用的类的回收, 首先需要判断什么样的类才是 "无用的类":
该类所有的实例都已被回收, 即 JAVA 堆中没有该类的实例.
加载类的 ClassLoader 已经被回收.
该类对应的 java.lang.Class 对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法.
虚拟机可能会堆满足这三个条件的 "无用的类" 进行回收, 仅仅是可能, 并非必然.
来源: https://www.cnblogs.com/listenfwind/p/9540167.html