一、JVM内存区域
虚拟机栈:线程私有,生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,每个方法从调用到执行结束,对应一个栈帧在虚拟机栈的出栈和入栈。
栈帧:用户存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈:与虚拟机栈作用相似,本地方法栈为虚拟机用到的Native方法服务。
程序计数器:线程私有的,可以看作是当前线程的行号指示器。字节码解释器工作是通过改变计数器的值来选取下一条需要执行的字节码指令。它可以实现分支、跳转、循环、异常处理、线程恢复等基本功能。不会发生OOM。
本地内存:线程共享区域,java8中,本地内存就是我们通常说的堆外内存,包含元空间和直接内存,Java8之前永久代/方法区是在堆中的,永久代主要存储类信息、常量、静态变量、即时编译器编译后的代码,这部分在堆中实现,受GC管理,由于永久带有-XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM。
二、如何识别垃圾
2.1 引用计数法
对象被引用一次,对象头引用次数加1,如果没有引用(引用此处为0)则对象可被回收。
循环依赖会导致问题引用次数无法减到0,对象就无法被回收
2.2 可达性算法
现代虚拟机基本都采用可达性算法判断对象是否存活。可达性算法原理是以一系列GC Root 的对象为起点出发指向引用节点。遍历所有的节点,如果相关对象不在任意一个GC Root为起点的引用链中,则这些对象会被判定为垃圾,可被回收。
对象可回收,并不一定会被直接回收。
当对象不可达(可回收),发生GC时会判断对象是否执行了finalize方法,如果没执行,先执行finalize方法,可以在finalize方法中将当前对象与GC Root关联,执行完finalize方法之后,GC会再次判断对象是否可达,如果不可达直接回收,如果可达则不回收。
finalize方法只会被执行一次
哪些对象可以成为 GC Root
- 先根据可达性算法标记出响应的可回收对象
- 对可回收的对象进行回收
标记清除操作简单,不用移动数据,缺点是清除之后产生内存碎片,不利于内存分配。
3.2 复制算法
把堆等分为两部分,A 和 B,区域A负责分配对象,区域B不分配,把区域A中存活的对象标记出来,把A中存活的对象都复制到B中,最后把A区对象全部清理掉释放空间,这样就解决了内存碎片。
缺点是 浪费空间。500M的堆其实只能用250M,移动存活对象也比较耗时。
3.3 标记整理
标记整理法跟标记清除类似,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,即将所有存活对象都往一端移动,再清理掉边界以外的内存。
3.4 分代收集算法
研究表明绝大多数对象都会在很短时间内被回收。
分代收集算法整合了以上算法,综合了这些算法的优点,最大程度避免了它们的缺点,所以是现代虚拟机首选的算法。
分代收集算法根据对象存活周期的不同将堆分为新生代和老生代默认比例是1:2,新生代又分为Eden区,from survivor(s0) to surviver(s1)三者比例为8:1:1。我们把新生代发生的GC称为Yong GC,老生代发生的GC称为Full GC。
3.5 分代收集工作原理
a 对象在新生代的分配与回收
上面分析可知,大部分对象在短时间内都会被回收,对象一般分配在Eden区
当Eden区将满时,触发Yong GC
- 因为大部分对象在很短时间内都会被回收,所以经过Yong GC后只会少部分对象会存活,它们会被复制到s0,同时对象年龄加一(对象年龄即发生Yong GC的次数),最后Eden区对象全部清理并释放空间。
- 当触发下一次Yong GC时,会把Eden区存活的对象和s0中存活的对象一起移动到s1,存活对象年龄+1,同时清空Eden和s0的空间
- 下一次Yong GC,则重复上一步,只不过此时变成了从Eden,s1区将存活的对象复制到s0,每次垃圾回收 s0和s1角色互换。
b 对象何时晋升老年代
- 当对象的年龄达到我们设定的阈值,则会从s0或s1晋升到老年代。
- 大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在Eden区,会直接分配老年代,因为如果把大对象分配在Eden区,Yong GC 再移动到s0或s1会有很大的开销,也会很快占满s0或s1区。所以干脆直接移到老年代。
- 还有一种情况,即 s0或s1区的相同年龄的对象大小之和大于s0或s1区的一半,则年龄大于等于该对象的对象也会晋升到老年代。
3、空间分配担保
在发生Yong GC之前,虚拟机会检查老年代最大连续可用空间是否大于新生代所有对象的总空间,如果大于则Yong GC 可以确保是安全的。如果不大于虚拟机会查看HandlePromotionFailure设置的值是否允许担保失败。如果允许继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行Yong GC,否则可能进行一次Full GC。
3、Stop The World
如果老年代满了,会触发Full GC,Full GC会同时回收新生代和老年代(即对整个堆进行GC),它会导致Stop The World
什么是 STW ?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。
一般Full GC停顿时间较长(因为Full GC会清理整个堆中不可用对象)
四、垃圾收集器种类
内存回收算法是方法论,则垃圾收集器就是内存回收的具体实现
图中的垃圾回收器如果有连线,则代表它们之间可以配合使用
- 新生代工作的垃圾回收器:Serial,ParNew,ParallelScavenge
- 老年代工作的垃圾回收器:CMS,Serial Old,Parallel Old
-
4.1 新生代收集器
1. Serial
Serial 是工作在新生代的,单线程的的垃圾收集器。适用于运行在client模式下的虚拟机。用户桌面应用分配给虚拟机的内存一般不会很大。
2. ParNew
ParNew收集器是Serial的多线程版本,除了使用多线程,其他像 收集算法,STW,对象分配规则和回收策略与Serial收集器一样。底层也共用了很多代码
ParNew主要工作在Server模式,多线程可以让垃圾回收的更快,减少STW时间,能提升响应时间,是许多运行在Server模式下的虚拟机首选新生代收集器。另一个与性能无关的原因除了Serial收集器,只有它能与CMS收集器配合工作
CMS是一个划时代的垃圾收集器,是真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程基本上同时工作3. Paraller Scavenge
Paraller Scavenge 是一个使用复制算法,多线程,工作在新生代的垃圾收集器,看起来与ParNew功能相同。
关注点不同:CMS等垃圾回收器关注的是尽可能的减少单词垃圾回收的时间,也就是尽量缩短用户线程等待的时间。而Paraller Scavenge目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码的时间 / (垃圾回收时间 + 运行用户代码的时间)),也就是CMS等垃圾收集器更适合用到与用户交互的程序,因为停顿时间短用户体验好,而Paraller Scavenge收集器更加关注吞吐量,更适合做后台运算等不需要太多用户交互的任务。4.2 老年代收集器
1. Serial Old 收集器
Serial 是工作在新生代的单线程收集器,Serial Old是工作在老年代的单线程收集器
2. Paraller Old 收集器
Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标
3. CMS 收集器
CMS 全称ConcurrentMarkSweep,是一款并发、使用标记-清除算法的垃圾回收器
优点是:并发收集,低延迟
缺点: 会产生内存碎片,在无法分配大对象的情况下,不得不提前触发 Full GC。
- CMS收集器对cpu资源比较敏感,并发阶段,它不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。(个人认为这个不算缺点,这是实现用户线程不停顿的原因)
- CMS收集器无法处理浮动垃圾 可能出现 Concurrent Mode Failure 失败而导致下一次Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程同时运行或者交叉运行,那么在并发标记阶段如果产生新垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的对象没有在本次Full GC 中回收,会在下一次GC中回收。 (这个感觉也不算是缺点)
思考:为什么并发清除阶段要使用标记清除算法? 因为并发清除阶段是跟用户线程同时或者交叉进行,所以不能够更改存活对象的地址,会导致应用线程引用错误。所以不能使用标记整理算法。
参数:
- -XX: +UserConcMarkSweepGC 手动指定使用CMS 收集器执行内存回收任务
- 开始该参数后会自动将 -XX: +UseParNew打开。 即 ParNew(新生代) + CMS(老年代) + Serial Old 的组合
- -XX: CMSInitiatingOccupancyFraction=n。 当老年代内存使用达到 n%,开始回收
- JDK5及以前版本的默认值为68,即老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值是 92%
- 如果内存增长比较缓慢,则可以设置一个稍微大的值,大的阈值可以有效的降低CMS的触发频率,减少老年代回收的次数可以较为明显的改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,避免频发触发老年代的串行收集器。通过该选项可以有效的控制Full GC 的次数。
- -XX: +UseCMSCompactAtFullCollection 用户指定在执行完Full GC后对内存空间进行压缩整理,用来整理内存碎片。由于内存压缩整理过程无法与用户线程并发执行,所带来的问题就是用户线程停顿时间变长了。
- -XX: CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。 与上一个参数配置同时配置生效
- -XX: ParallelCMSThreads 设置CMS的线程数量。
- CMS 默认启动的线程数是 (ParallelGCThreads + 3) / 4。 ParallelGCThreads是新生代并行收集器的线程数。当CUP资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
- Yong GC 的线程数默认 (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8)。可以通过-XX:ParallelGCThreads= N 来调整
运行原理:
步骤
- 初始标记 **stw**
- 并发标记
- 重新标记 **stw**
- 并发清除
初始标记:用户线程 Stop-the-World,这个阶段的主要任务仅仅只是标记出 GC Roots能直接关联到的对象。标记完成后立刻恢复用户线程。与GC Root直接关联比较少,所以初始标记速度很快。
并发标记:从GC Roots的直接关联对象开始遍历整个对象图。这个过程耗时较长但是不需要停止用户线程。
重新标记:由于并发标记阶段用户线程运行,导致当前标记不准,需要重新标记。 并发标记阶段产生的变动对象都会打上标记,这个阶段STW,并重新标记并发标记节点改变的对象。这个阶段时间比初始标记时间长但是比并发标记要短。
并发清除:此阶段并发清理掉未被标记为存活的对象,由于不需要移动存活对象,可以与用户线程同时进行。
4 G1 收集器
G1是一款面向服务器的垃圾收集器,被成为驾驭一切的垃圾收集器,主要有以下几个特点
- 和CMS收集器一样,能够与用户线程并发
- 整理空闲空间更快
- 需要GC停顿时间更好预测
- 不会像CMS一样牺牲吞吐量
- 不需要更大的Java Heap
与CMS相比,它在以下两方面变现更出色
- 运行期间不会产生垃圾碎片,G1从整体上看是采用标记整理算法,局部上看是基于复制算法实现的,这两个算法都不会产生碎片,收集后提供连续可用内存,有利于程序长时间运行。
- 在STW上建立了可预测的停顿时间模型,用户可以指定期望的停顿时间,G1将停顿时间控制在用户设定的停顿时间以内。
为什么G1能建立可预测的停顿模型,主要原因是G1对堆空间的分配与传统垃圾收集器不一样,传统的堆内存分配是连续的,分为新生代和老年代。
G1的存储地址不是连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址,如下图
除了传统的新生代、幸存区、老年代,Region还多了一种 H,它代表Humongous,这表示该类Region存储的是巨大对象,大小等于Region一半的对象,超大对象直接分配给了老年代,防止反复拷贝移动。
传统的收集器如果发生Full GC,是对整个堆进行全区域的垃圾收集,而分配成各个Region的话,方便G1跟踪各个Region中垃圾堆积的价值大小(回收所获得的空间大小及回收所需要的经验值),这样根据价值维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region,避免了整个老年代的回收,减少了STW造成的停顿时间。同时由于只收集部分Region,所以能做到STW时间可控。
G1收集器的工作步骤如下:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
五、常见问题List
- GC Roots 个人你能够理解记住的有以下几种
- 虚拟机栈中的对象应用
- 方法区中的静态属性、常量引用对象
- 本地方法栈中引用的对象
- 被Synchronized锁持有的对象
**
**