第九周课后练习
JVM 简介
一,垃圾回收简介
首先,在了解 G1 之前,我们需要清楚的知道,垃圾回收是什么?简单的说垃圾回收就是回收内存中不再使用的对象。
垃圾回收的基本步骤
回收的步骤有 2 步:
查找内存中不再使用的对象
释放这些对象占用的内存
1,查找内存中不再使用的对象
那么问题来了,如何判断哪些对象不再被使用呢?我们也有 2 个方法:
引用计数法
引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
2.根搜索算法
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
现在我们已经知道如何找出垃圾对象了,如何把这些对象清理掉呢?
2. 释放这些对象占用的内存
常见的方式有复制或者直接清理,但是直接清理会存在内存碎片,于是就会产生了清理再压缩的方式。
总得来说就产生了三种类型的回收算法。
1.标记-复制
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。它的优点是实现简单,效率高,不会存在内存碎片。缺点就是需要 2 倍的内存来管理。
2.标记-清理
标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。它的优点是效率高,缺点是容易产生内存碎片。
3.标记-整理
标记操作和“标记-清理”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有 存活的对象都向一端移动,并更新引用其对象的指针。因为要移动对象,所以它的效率要比“标记-清理”效率低,但是不会产生内存碎片。
基于分代的假设
由于对象的存活时间有长有短,所以对于存活时间长的对象,减少被 gc 的次数可以避免不必要的开销。这样我们就把内存分成新生代和老年代,新生代存放刚创建的和存活时间比较短的对象,老年代存放存活时间比较长的对象。这样每次仅仅清理年轻代,老年代仅在必要时时再做清理可以极大的提高 GC 效率,节省 GC 时间。
java 垃圾收集器的历史
第一阶段,Serial(串行)收集器
在 jdk1.3.1 之前,java 虚拟机仅仅能使用 Serial 收集器。 Serial 收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
PS:开启 Serial 收集器的方式
-XX:+UseSerialGC
第二阶段,Parallel(并行)收集器
Parallel 收集器也称吞吐量收集器,相比 Serial 收集器,Parallel 最主要的优势在于使用多线程去完成垃圾清理工作,这样可以充分利用多核的特性,大幅降低 gc 时间。
PS:开启 Parallel 收集器的方式
-XX:+UseParallelGC -XX:+UseParallelOldGC
第三阶段,CMS(并发)收集器
CMS 收集器在 Minor GC 时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在 Full GC 时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。
PS:开启 CMS 收集器的方式
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
第四阶段,G1(并发)收集器
G1 收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于 4GB)时产生的停顿。相对于 CMS 的优势而言是内存碎片的产生率大大降低。
PS:开启 G1 收集器的方式
-XX:+UseG1GC
二,G1 介绍
G1 的第一篇 paper(附录 1)发表于 2004 年,在 2012 年才在 jdk1.7u4 中可用。oracle 官方计划在 jdk9 中将 G1 变成默认的垃圾收集器,以替代 CMS。为何 oracle 要极力推荐 G1 呢,G1 有哪些优点?
首先,G1 的设计原则就是简单可行的性能调优
开发人员仅仅需要声明以下参数即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC 为开启 G1 垃圾收集器,-Xmx32g 设计堆内存的最大内存为 32G,-XX:MaxGCPauseMillis=200 设置 GC 的最大暂停时间为 200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
其次,G1 将新生代,老年代的物理空间划分取消了。
这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1 算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者 Survivor 空间。老年代也分成很多区域,G1 收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1 完成了堆的压缩(至少是部分堆的压缩),这样也就不会有 cms 内存碎片问题的存在了。
在 G1 中,还有一种特殊的区域,叫 Humongous 区域。 如果一个对象占用的空间超过了分区容量 50%以上,G1 收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。
PS:在 java 8 中,持久代也移动到了普通的堆内存空间中,改为元空间。
对象分配策略
说起大对象的分配,我们不得不谈谈对象的分配策略。它分为 3 个阶段:
TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
Eden 区中分配
Humongous 区分配
TLAB 为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在 Eden 空间中,每一个线程都有一个固定的分区用于分配对象,即一个 TLAB。分配对象时,线程之间不再需要进行任何的同步。
对 TLAB 空间中无法分配的对象,JVM 会尝试在 Eden 空间中进行分配。如果 Eden 空间无法容纳该对象,就只能在老年代中进行分配空间。
最后,G1 提供了两种 GC 模式,Young GC 和 Mixed GC,两种都是 Stop The World(STW)的。下面我们将分别介绍一下这 2 种模式。
三,G1 Young GC
Young GC 主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时会被触发。在这种情况下,Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到年老代空间。Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终 Eden 空间的数据为空,GC 停止工作,应用线程继续执行。
这时,我们需要考虑一个问题,如果仅仅 GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1 引进了 RSet 的概念。它的全称是 Remembered Set,作用是跟踪指向某个 heap 区内的对象引用。
在 CMS 中,也有 RSet 的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种 point-out,在进行 Young GC 时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在 G1 中,并没有使用 point-out,这是由于一个分区太小,分区数量太多,如果是用 point-out 的话,会造成大量的扫描浪费,有些根本不需要 GC 的分区引用也扫描了。于是 G1 中使用 point-in 来解决。point-in 的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次 GC 时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在 G1 中又引入了另外一个概念,卡表(Card Table)。一个 Card Table 将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于 128 到 512 字节之间。Card Table 通常为字节数组,由 Card 的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外 RSet 也将这个数组下标记录下来。一般情况下,这个 RSet 其实是一个 Hash Table,Key 是别的 Region 的起始地址,Value 是一个集合,里面的元素是 Card Table 的 Index。
Young GC 阶段:
阶段 1:根扫描
静态和本地对象被扫描
阶段 2:更新 RS
处理 dirty card 队列更新 RS
阶段 3:处理 RS
检测从年轻代指向年老代的对象
阶段 4:对象拷贝
拷贝存活的对象到 survivor/old 区域
阶段 5:处理引用队列
软引用,弱引用,虚引用处理
秒杀系统
单独实现功能。做隔离。
静态化
秒杀时间到的时候,才将购买 url 写入到 js 里面
多级阀门
好使的技术一刀解决问题,一刀封侯。
眼光要开阔,不要拿着。
评论