jvm(2):垃圾收集和内存分配


typora-root-url: ./

垃圾收集

垃圾收集器关注的是线程共享的这部分内存。

jvisualvm用来监控JVM的运行情况,可以用它来查看和浏览Heap Dump、Thread Dump、内存对象实例情况、GC执行情况、CPU消耗以及类的装载情况。

回收方法区

永久代的垃圾收集主要回收两部分内存:废弃常量和无用的类

废弃常量:没有任何对象引用该常量。

无用的类:同时满足

  1. 该类所有的实例都已经被回收
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记—清除算法Mark-Sweep

分为标记清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

效率问题:标记和清除两个过程的效率都不高。

空间问题:会产生大量不连续的内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存不得不提前触发另一次垃圾收集动作。

复制算法Copying

将可用内存按容量划分大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

代价是将内存缩小为了原来的一半。而且如果对象存活率较高,复制操作多,效率会变低。

例子:新生代:一个eden,两个survivor[from和to](提高GC的效率)

每次使用一个eden和一个survivor。

标记—整理算法Mark-Compact

首先标记出所有需要回收的对象,在标记完成后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。

新生代:复制算法

老年代:标记清除/标记整理算法

HotSpot的算法实现

枚举根节点

从GC Roots节点找引用链:如果要逐个检查引用,必然消耗时间。

GC停顿上:GC进行时必须暂停所有Java执行线程。

准确式GC:虚拟机可以知道内存中某个位置的数据的具体类型。

虚拟机应当有办法得知哪些地方存放的是对象的引用。

在HotSpot的实现中使用一组OopMap的数据结构来达到这个目的。 在类加载完成的时候,HotSpot就把对象内什么偏移量上什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置时引用。这样,GC在扫描时就可以直接得知这些信息了。

安全点Safepoint

如果为每一条指令都生成OopMap,那将会需要大量的额外空间。 HotSpot没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点。程序执行时并非在所有地方都能停顿下来进行GC,只有在到达安全点时才能暂停。

Safepoint的选定

  • 太少:让GC等待时间太久;
  • 太多:增大运行时负荷。
  • 选定目标:具有让程序长时间执行的特征。最明显的就是指令序列的复用,例如方法调用、循环跳转、异常跳转等。

在GC发生时让所有线程都跑到最近安全点在停顿下来的两种方案:抢先式中断和主动式中断

  • 抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。[现在几乎没有虚拟机实现采用抢先式中断]
  • 主动式中断是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
安全区域 Safe Region

Safepoint机制保证了程序执行时,在不太长的时间内就会进入到可进入的GC的安全点。

但是线程不执行时,比如处于sleep状态或者blocked状态时,线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm也显然不太可能等待线程重新分配cpu时间。对于这种情况,使用安全区域来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们可以把安全区域看做是扩展了的安全点。

当线程执行到 Safe Region中的代码时,首先标识自己已经进入了 Safe Region,那样当在这段时间里JVM要发起GC时,就不用管标识自己为 Safe Region状态的线程了。当线程要离开安全区域时,它要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则它就必须等待,直到收到可以安全离开 Safe Region的信号为止。

垃圾收集器

Serial收集器

单线程、独占式、Client模式下的默认新生代收集器

Stop The World:虚拟机在用户不可见的情况下把用户正常工作的线程全部停掉

内存分配方式:指针碰撞

ParNew收集器

多线程并行[可以理解为Serial的多线程版本]、Server模式下的首选新生代收集器

应用程序仍会暂停

内存分配方式:指针碰撞

Parallel Scavenge收集器

多线程、新生代收集器

目标是达到一个可控制的吞吐量[运行用户代码时间/CPU总时间]:注重吞吐量以及CPU资源

自适应调节策略

Serial Old收集器

[Serial的老年代版本]

标记整理算法

Parallel Old回收器

[Parallel Scavenge的老年代版本]

标记整理算法

CMS(Concurrent Mark Sweep)收集器

并发收集、低停顿、对CPU资源敏感、无法处理浮动垃圾[并发清理阶段产生的垃圾]、并发阶段会降低吞吐量、老年代收集器

目标是获取最短回收停顿时间:重视服务器的响应速度

标记清除算法[产生大量空间碎片]

步骤:

  1. 初始标记:标记GC Roots能直接关联到的对象[速度很快]
  2. 并发标记:进行GC Roots Tracing的过程[时间较长]
  3. 重新标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录[时间稍长]
  4. 并发清除

内存分配方式:空闲列表

G1(Garbage-First)收集器

并发与并行、分代收集、空间整合、可预测停顿

整体:标记整理算法;局部:复制算法;[不会产生内存空间碎片]

新生代和老年代不再是物理隔离了,它们都是一部分(可以不连续)Region。

G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,优先回收价值最大的Region。细节方面,比如Region之间的对象引用要避免全堆扫描,虚拟机使用Remembered Set。G1中每个Region都有一个Remembered Set,虚拟机发现程序对引用类型的数据进行写操作时,产生一个中断,检查引用的对象是否处于不同的Region,如果是,就通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收的时候,在GC根节点的枚举范围中加入Remembered Set保证不全堆扫描。

步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收
GC日志
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm: 2999K->2999K(21248K)], 0.015007secs] [Times: user=0.01 sys=0.00, real=0.02secs]
虚拟机启动以来经过的秒数: [垃圾收集的停顿类型 [GC发生的区域: GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量), GC所占用的时间] GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量), [GC发生的区域: GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量), GC所占用的时间] [具体时间数据: 用户态消耗的CPU时间 内核态消耗的CPU时间, 操作从开始到结束所经过的墙钟时间]
垃圾收集器参数总结

内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden中分配。当Eden中没有足够空间时,虚拟机将发起一次Minor GC。

新生代GC(Minor GC):发生在新生代的GC,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般比Minor GC慢10倍以上。

大对象直接进入老年代

大对象:需要大量连续内存空间的Java对象,比如数组。

经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间。

长期存活的对象将进入老年代

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor中,并且Age设为1。

对象在Survivor每读过一次Minor GC,Age就+1,当Age达到一定程度时(默认15),就将对象移到老年代。

如果在Survivor中相同年龄所有对象大小的综合>Survivor空间的一半,年龄>=该年龄的对象就可以直接进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,则此次GC安全。

否则,虚拟机查看HandlePromotionFailure是否允许担保失败,如果允许,则继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多)。如果小于,或者设置不允许担保失败,这时要进行一次Full GC。

原文地址:https://www.cnblogs.com/angelica-duhurica/p/11346603.html