JVM内存的划分
分为三个部分(以下名词表示同一个区):
- 新生区、新生代、年轻代
- 养老区、老年区、老年代
- 永久区、永久代
划分区域的目的
唯一目的就是优化GC性能。
如果没有分代,我们所有的对象都放在一块,GC的时候我们需要对堆的所有区域进行扫描。而很多的对象都是“朝生夕死”的,如果把创建的新的对象都放在某一地方,当GC的时候就先把“朝生夕死”对象的区域进行回收,这样就会腾出很多大的空间来。
JVM的垃圾分配策略:
一、新生区的垃圾回收机制
新生区分为:Eden区、Survivor0区、Survivor1区(也称为from区和to区)
其中Eden区占80%的内存空间,每块Survivor各占用10%的内存空间(如:Eden占800M,每个Survivor占100M)
- 开始时创建的对象都是分配在Eden区域中,当Eden区快满了,就会触发垃圾回收Minor GC(使用复制算法进行垃圾回收)
- Minor GC处理后,首先会把Eden区中还存活着的对象一次性转入其中一块空闲着的Survivor区。然后清空Eden区,之后创建的对象就继续放入Eden区中了,直至下次Eden又被填满。
- Eden再次被填满时,就会再次出发Minor GC,清理后(Minor会清理Eden区和Survivor区的内存),Eden区和存在对象的Survivor区(此时的from区)中存活的对象转移到另一块空着的Survivor区中(此时的to区),并清空Eden区和之前存在对象的Survivor区(此时变为to区了,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。)
这就是复制算法的流程。
一直要保持一个Survivor区是空的以提供复制算法垃圾回收,而这块区域的内存只占整块的10%,其他90%内存都可以被使用,课件内存利用率还是相当高的。
二、什么时候进入老年区呢?
1 经历15次GC后进入老年区
默认情况下,如果新生区中的某个对象经历了15次GC后,还是没有被回收掉,那么它就会被转入老年区。
可通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15。
2 动态对象年龄判断
这种方法不用等到经历GC15次。
假如一批对象总大小大于当前Survivor区内存的50%,那么大于等于这批对象年龄的对象就会被转移到老年区。
例:假设Survivor0区中的两个对象都经历的3次GC(年龄3),而且这两个对象总大小50M,超过了Survivor0区内存大小的一半。那么此时Survivor0区中年龄大于等于3岁的对象就都要被全部转移到老年区。
3 大对象直接进入老年代
有一个JVM参数"-XX:PretenureSizeThreshold",默认值是0,表示任何情况都先把对象分配给Eden区。
若设置为1048576字节,也就是1M。则表示当创建的对象大于1M时,就会直接把这个对象放入到老年区,就根本不会经过新生区了。
这么做的原因:大对象在经历复制算法进行GC的时候会降低性能。
4 Minor GC后存活的对象太多无法放入Survivor区了
Minor GC后存活的对象太多,导致Survivor区放不下了,此时就会将所有的对象直接转移到老年区中。
三、老年区空间分配担保原则
执行每一次Minor GC前,JVM都先检查一下老年区可用的内存空间是否大于新生区所有对象的总大小。
原因:极端情况下,Minor GC后,新生代中所有的对象都活了下来,那就会把所有新生代中的对象放入老年区中。
- 如果说老年区可用内存大于新生代对象总大小,那么就可以放心的执行Minor GC。
- 但如果老年区内存小于新生区对象的总大小,这时候就会看一个参数:“-XX:HandlePromotionFailure”是否设置为true了。
如果为true,就进入下一次判断,看老年区可用内存是否大于之前每次Minor GC后进入老年区对象的平均大小。如果老年代可用内存小于平均大小或是参数没有设置成true,那就会直接触发“Full GC”,就是对老年代进行垃圾回收,腾出空间后,再进行Minor GC,相当于对新生区、老年区统一做了一次清理。
三种情况递进理解:
- 如果Minor GC后,存活的对象<Survivor区大小,直接进入Survivor区即可;
- 如果Minor GC后,存活的对象>Survivor区大小,但<老年区可用内存,直接进入老年区;
- 若Minor GC后,此时老年区都放不下这些存活的对象了,就会触发Full GC;
如果Full GC后老年区内存还是不够用,就会导致OOM内存溢出。
四、怎么判断对象已死亡?
在垃圾回收前首先就是要判断哪些对象已经死亡
1、引用计数法
每个对象中添加一个引用计数器
- 有一个地方引用它,计数器+1;
- 引用失效,计数器-1
计数器值为0的对象就是没有被使用的对象。
优点: 方法实现简单,效率高。
缺点:难以解决对象之间相互循环引用的问题。
2、可达性分析算法
以一系列“GC Roots”的对象作为起点,从这些节点开始向下搜索。
引用链: 节点所走过的路径。
怎么判断一个对象是否能被GC?
当一个对象到GC Roots没有任何引用链的话,证明此对象是不可用的,需要被回收。
图中:Object5~Object7之间虽然都有引用关系,但他们到GC Roots不可达,因此成为了被回收的对象。
GC Roots都有哪些?
(两栈、两方法区、一锁)
- 虚拟机栈(局部变量表)中引用的对象
- 本地方法栈(Native方法)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
对象可以被回收,就代表一定会被回收吗?
并不一定
可达性分析法中判断的不可达对象,它们会暂时处于“缓刑阶段”,要真正回收一个对象,至少需要两次标记过程:
- 可达性分析中不可达的对象被第一次标记,并且执行一次筛选(此对象有无必要执行finalize方法)
- 被判定为有必要执行finalize方法,那么该对象就会被放在一个对列中进行第二次标记。除非这个对象与引用链上的任何一个对象建立关联,否则就会真的被回收。
3、对象引用
强引用
(最为普遍)一个对象具有强引用,GC回收器绝不会回收它,当内存不足时,Java虚拟机直接抛OOM错误。
软引用
如果内存空间足够:GC回收器不会回收它;
如果内存空间不足:回收这些对象的内存
可用来实现内存敏感的高速缓存
弱引用
GC回收器线程扫描到它所管辖的内存区域时,一旦发现了具有弱引用的对象,不管当时内存空间是否足够,都会回收它的内存。
GC回收器优先级很低的线程,所以不一定发现那些只具有弱引用的对象。
虚引用
和没有任何引用一样。
用途:用来跟踪对象被垃圾回收的活动
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
程序设计中,一般使用软引用情况多,因为软引用:
- 可以加速JVM对垃圾内存的回收速度;
- 可以维护系统的运行安全;
- 防止内存溢出等问题产生。
如何判断一个常量是无用常量?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
如何判断一个类是无用类?
- 该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收了
- 该类对应的java.lang.Class对象没有在任何地方被引用
满足以上三点,就可以达到被回收的条件了,但不必然被回收。
五、垃圾收集算法
1、标记-清除算法
- 标记出所有不需要回收的对象
- 统一回收掉所有没被标记的对象
缺点:
- 效率低(大量对象需要被回收,需要进行大量的标记和清除动作)
- 内存空间碎片化问题(清除后,产生大量不连续的内存碎片,导致分配较大的对象时无法找到足够的连续内存而不得不再次GC)
2、标记-整理算法
- 标记所有不需要回收的对象
- 将标记的对象向一端移动,然后直接清理掉该端边界以外的内存
【缺点】
较为耗时
所以如果系统频繁出现Full GC,会严重影响系统性能,出现卡顿。所以JVM优化的一大问题就是减少Full GC频率。
注重吞吐量的GC收集器老年代都使用整理算法,因为:
即使不移动对象会使得收集器的效率提升,但是因为内存分配和访问相比垃圾收集的频率多很多,多以相比之下总体的吞吐量是下降的。
3、标记-复制算法
将内存分为大小相同的2块,每次使用其中一块。
- 先将对象分配在A块内存
- 发生一次GC后,将A中存活的对象都复制到B块中
- 将A中的对象全清理掉
每次内存回收,都是对内存区域的一半进行回收!大大提高了GC效率。
4、分代收集算法
此算法没有新的思想,仅是根据对象的存活周期不同将内存分为几块。
一般Java堆分为新生区、老年区,这样可以根据各个年代的特点选择合适的垃圾收集算法。
新生区 | 老年区 |
---|---|
标记-复制算法 | 标记-清除/标记-整理 |
新生区:每次GC都会有大量对象死去,且GC次数较频繁。使用标记复制,只需要付出少量的复制成本就可以完成每次更高效率的GC。
老年区:对象存活几率高,假如使用标记复制,就会在复制上消耗太多时间。所以使用标记清楚或标记整理。
五、垃圾收集器
新生区和老年区进行垃圾回收时是通过不同的垃圾回收器进行回收的
Serial 和 Serial Old
- 分别用于回收新生区(复制算法)和老年区(整理算法)。
- 单线程运行,垃圾回收时会停止我们系统的其他线程,再执行垃圾回收(不再使用);
ParNew
- Serial的多线程版本,可和Serial Old和CMS配合使用;
- 多线程并发,性能更好;但同样Stop The World。
Parallel Scavenge
- 用于回收新生区(复制算法)
- 多线程并发,可控制吞吐量(CPU利用效率)
- 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)
Parallel Old
- Parallel Scavenge的老年代版本(整理算法)
- 在注重吞吐量以及CPU资源的场合,优先考虑
CMS收集器
老年代收集器
特点:
- 以获取最短回收停顿时间为目标(非常注重用户体验)
- 第一款真正意义上的并发收集器(让垃圾回收线程和用户线程并发工作)
- 使用“标记-清除”算法(标记-整理耗时,停顿时间长,所以不用)
工作流程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
优点:
- 并发收集,低停顿
缺点:
- 对CPU资源敏感
- 使用“标记清除”算法会使收集结束时有大量空间碎片产生。
- 无法处理浮动垃圾
G1垃圾收集器
收集整个GC堆
简介:
- 面向服务器的垃圾收集器(主要针对多核处理器及大容量的机器)
- 以极高概率满足GC停顿时间要求的同时,还具备了高吞吐量性能
特点
1、并行与并发
利用多核多CPU的硬件优势,使用多个CPU来缩短Stop The World停顿时间。在执行GC动作的同时,通过并发的方式让用户程序继续运行。
2、回收算法
G1从整体来看是基于“标记-整理”算法实现的;但从局部上来看是基于“标记-复制”算法实现的。
2、停顿时间模型
指定在一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标
分配的时间多就多清理一些,分配的时间少就少清理一些,不求每次把垃圾清理干净,只求他在对的时间内努力做事。通过指定的最大停顿时间,就可反向推算出本次要收集的大体区域。
3、内存布局(保留了分代概念,但非固定区域内存了,改为一系列区域的动态集合)
为建立可预测的停顿时间模型:
-
首先得将堆内存划分为多个大小相等的独立区域(Region),将Region作为单次回收的最小单元。
-
每个Region都可以根据需要,扮演新生代的Eden区、Survivor区、老年区。
收集器对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活一段时间、熬过多次回收的旧对象都能获取很好的回收效果。 -
更具体的处理思路是让G1收集器跟踪各个Region里面的垃圾堆积的“价值”大小(价值:回收所获得的空间大小以及回收所需的时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值收益最大的那些Region(最大回收价值的Region集合通过用户设置的停顿时间来分配)。(工作原理)
因此保证了G1收集器在有限的时间内获取尽可能高的收集效率。
总结:
将回收最小单元缩小,将最有必要回收的最小单元优先回收掉,相较于对两块固定内存的回收,效率高出很多。
运行流程
G1与CMS的运行过程大体一致,分为四个步骤
1、 初始标记:
- 仅标记GC Roots能够直接关联到的对象;
- 修改TAMS指针的值(让下一阶段用户线程并发运行时,能正确的在可用的Region中分配新对象)
此阶段需要停顿线程,但耗时很短
2、并发标记
- 从GC Roots开始对堆中对象进行可达性分析,递归扫描整个对象图,找处要回收的对象;
- 并发时有引用变动的对象会产生漏标问题,G1中会使用SATB算法来解决
缺点:此阶段,耗时较长
优点:回收线程可与用户线程并发执行。
3、最终标记
- 对用户线程做一个短暂的暂停
- 用于处理并发标记阶段遗留下来的最后少量的SATB记录(漏标对象)
4、筛选回收
- 更新Region的统计数据,对各个Region的回收价值和成本进行排序
- 根据用户期望的停顿时间来指定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region全部空间。
这里涉及存活的对象的移动,必须要暂停用户线程,由多个回收器线程并行完成。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。