标记-清除(mark and Sweep)是最经典的垃圾回收算法
碎片整理
内存模型就像一个衣柜有很多隔断,假如每个隔断内部都是满的,如果执行清理,那么最后可能内存会很乱,一三五层隔断有东西,二四六没东西,或者更乱。
大家可以想象一下,假如要写入一个很大的文件,需要一个连续的内存地址来存储。如果内存碎片很多,可能在寻找地址的时候就会浪费很多时间。
所以在JVM每次执行清理的时候会执行内存清理整理,以减少内存碎片。
分代假设
对象越多则收集垃圾的消耗时间越长,所以研究人员发现,程序中的大多可回收的垃圾归为两类:
- 大部分很快就不使用或立即无用
- 存活时间很长的
如果统一去处理这些垃圾,可能就会消耗更长的时间处理存活较长的垃圾,所以这些观测形成了弱代假设。基于这一假设,VM的内存分为,年轻代(Young)和老年代(Old/Tenured)
拆分这两个可清理的单独区域,允许采取不同的算法从而来大幅提高GC的性能。
但是这种方法不是没有问题,例如,不同分代中的对象可能会互相引用,这样在收集某个分代可能就是GC root。
没有完美的垃圾回收算法,只有好的算法,之所以这样说是因为,GC算法专门针对“要么死得快,要么活的长”这类特征的对像来优化,JVM对于那些不长不短的对象就很awkword(尴尬)
内存池(Memory Pools)
Java内存模型是很抽象的,很多定义也不同,网上资料各异没有明确官方定义,但是小编感觉,只要理解是对的,有助你Java开发,那么怎么理解怎么记,就算是错的,也会增加你对Java垃圾回收的理解,这里用一个图来把这个抽象的东西,表述一下。
新生代(Eden,伊甸园)
Eden是内存中的一个区域,用来分配新创建的对象。通常会与多个线程同时创建多个对象,所以Eden区被划分为多一个线程本地分配缓冲区,通过分离,避免与其他线程同步操作。
如果线程缓冲区没有足够内存了,就会在Eden共享区分配,如果共享区也没有了内存,就会触发年轻代的GC来释放内存。如果GC之后Eden区依然没有足够的内存,则对象就分配到年老代,Old
当Eden去进行垃圾收集的时候,GC将从root可达的对象过一遍,并标记存活对象,标记完成后,Eden中所有存活的对象,被复制到Eden的右边的存活区(Survivor spaces),这个时候Eden区可能就是空的,然后分配新对象,这种方法就是标记-复制,是复制而不是移动。
存活区(Survivor Spaces)
Eden 区的旁边是两个存活区, 称为 from 空间和 to 空间。需要着重强调的的是, 任意时刻总有一个存活区是空的(empty)。
空的这个存活区在下一次年轻代GC是存放对象,年轻代中所有的存活对象(包括Edenq区和非空的那个 “from” 存活区)都会被复制到 ”to“ 存活区。GC过程完成后, ”to“ 区有对象,而 ‘from’ 区里没有对象。两者的角色进行正好切换 。
存活的对象会在俩个区多次复制,当存活区对象在经历多次GC后依然,存活那么这个时候就可以升级为老年代Old。为了确定一个对象是否“足够老”, 可以被提升(Promotion)到老年代,GC模块跟踪记录每个存活区对象存活的次数。每次分代GC完成后,存活对象的年龄就会增长。当年龄超过提升阈值(tenuring threshold), 就会被提升到老年代区域。
具体的提升阈值由JVM动态调整,但也可以用参数-XX:+MaxTenuringThreshold
来指定上限。如果设置 -XX:+MaxTenuringThreshold=0
, 则GC时存活对象不在存活区之间复制,直接提升到老年代。
现代 JVM 中这个阈值默认设置为15个 GC周期。这也是HotSpot中的最大值。
如果存活区空间不够存放年轻代中的存活对象,提升(Promotion)也可能更早地进行。
老年代(Old Generation)
老年代的对象是经历了筛选进来的,老年代内存空间通常会更大,一般是垃圾的概率会相对小。
老年代的GC发生频率比年轻代小很多,因为大部分都是存活的,所以使用的算法也不是标记和复制,对于因为是垃圾的改变会相对小,所以如果说是清理垃圾,更不说是,优化内存。但是都会经历下面几步:
- 通过标志位(marked bit),标记所有通过 GC roots 可达的对象.
- 删除所有不可达对象
- 整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方,依次存放。
永久代(PermGen)
在Java 8 之前有一个特殊的空间,称为“永久代”(Permanent Generation)。这是存储元数据(metadata)的地方,比如 class 信息等。此外,这个区域中也保存有其他的数据和信息, 包括 内部化的字符串(internalized strings)等等。实际上这给Java开发者造成了很多麻烦,因为很难去计算这块区域到底需要占用多少内存空间。预测失败导致的结果就是产生 java.lang.OutOfMemoryError: Permgen space 这种形式的错误。除非 ·OutOfMemoryError· 确实是内存泄漏导致的,否则就只能增加 permgen 的大小,例如下面的示例,就是设置 permgen 最大空间为 256 MB:
java -XX:MaxPermSize=256m com.mycompany.MyApplication
元数据区(Metaspace)
既然估算元数据所需空间那么复杂, Java 8直接删除了永久代(Permanent Generation),改用 Metaspace。从此以后, Java 中很多杂七杂八的东西都放置到普通的堆内存里。
当然,像类定义(class definitions)之类的信息会被加载到 Metaspace 中。元数据区位于本地内存(native memory),不再影响到普通的Java对象。默认情况下, Metaspace的大小只受限于 Java进程可用的本地内存。这样程序就不再因为多加载了几个类/JAR包就导致 java.lang.OutOfMemoryError: Permgen space. 。注意, 这种不受限制的空间也不是没有代价的 —— 如果 Metaspace 失控, 则可能会导致很严重的内存交换(swapping), 或者导致本地内存分配失败。
如果需要避免这种最坏情况,那么可以通过下面这样的方式来限制 Metaspace 的大小, 如 256 MB:
java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
Minor GC vs Major GC vs Full GC
以下内容原文写的很清楚,是概念上内容,采用原文描述
垃圾收集事件(Garbage Collection events)通常分为: 小型GC(Minor GC) - 大型GC(Major GC) - 和完全GC(Full GC) 。本节介绍这些事件及其区别。然后你会发现这些区别也不是特别清晰。
最重要的是,应用程序是否满足 服务级别协议(Service Level Agreement, SLA), 并通过监控程序查看响应延迟和吞吐量。也只有那时候才能看到GC事件相关的结果。重要的是这些事件是否停止整个程序,以及持续多长时间。
虽然 Minor, Major 和 Full GC 这些术语被广泛应用, 但并没有标准的定义, 我们还是来深入了解一下具体的细节吧。
小型GC(Minor GC)
年轻代内存的垃圾收集事件称为小型GC。这个定义既清晰又得到广泛共识。对于小型GC事件,有一些有趣的事情你应该了解一下:
- 当JVM无法为新对象分配内存空间时总会触发 Minor GC,比如 Eden 区占满时。所以(新对象)分配频率越高, Minor GC 的频率就越高。
- Minor GC 事件实际上忽略了老年代。从老年代指向年轻代的引用都被认为是GC Root。而从年轻代指向老年代的引用在标记阶段全部被忽略。
- Minor GC 每次都会引起全线停顿(stop-the-world ), 暂停所有的应用线程。对大多数程序而言,暂停时长基本上是可以忽略不计的, 因为 Eden 区的对象基本上都是垃圾, 也不怎么复制到存活区/老年代。如果情况不是这样, 大部分新创建的对象不能被垃圾回收清理掉, 则 Minor GC的停顿就会持续更长的时间。
所以 Minor GC 的定义很简单 —— Minor GC 清理的就是年轻代。
Major GC vs Full GC
Minor GC 清理的是年轻代空间(Young space),相应的,其他定义也很简单:
- Major GC(大型GC) 清理的是老年代空间(Old space)。
- Full GC(完全GC)清理的是整个堆, 包括年轻代和老年代空间。
悲剧的情况出现了,很多Major GC是由于Minor GC触发的,所以很多情况下这和Full GC就没有多大区别了,这个就很awkword(尴尬)
这也让我们认识到,不应该去操心是叫 Major GC 呢还是叫 Full GC, 我们应该关注的是: 某次GC事件 是否停止所有线程,或者是与其他线程并发执行。
文章参考自附录地址,附带小编自己的学习经历和理解重新翻译,希望大家共进步