文章目录
JVM垃圾回收
1.JVM内存分配
Java的内存管理主要包含对象内存的回收和对象内存的分配,其中最核心的功能是堆中对象的分配与回收。堆的垃圾收集器管理的主要区域,因此也被称为GC堆。Java堆还可以细分为:新生代(Young Generation)和老年代(Old Generation)。再细分,新生代又可以分为Eden、From Survivor、To Suvivor空间。细分的目的是为了更好地回收内存或者更好地分配内存。
对象的分配:
大部分情况下,新的对象首先都会在Eden区分配,在一次新生代垃圾回收后如果还存活,就会进入Suvivor区域,并且对象年龄增加。当对象年龄到达一定阈值时,就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
JVM默认大对象如果大小超过Eden区就直接进入老年代。
主要进行GC的区域:
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
2.JVM如何判断对象存活状态
判断对象是否存活一般有两种方式:
1.引用计数法:
每个对象添加一个引用计数,每当新增一个引用计数就+1,有一个引用失效计数就-1。计数为0的时候对象就是可以被回收的。
缺点:无法解决对象之间互相循环引用的问题。
2.可达性分析算法:
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
可达的对象,即为能与GC Roots构成引用关系上的连通图的对象。
3.引用分类:
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
1.强引用(StrongReference)
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)
如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
3.弱引用(WeakReference)
如果一个对象只具有弱引用,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
public static void main(String[] args){
String s = "abc";
WeakReference<String> swf = new WeakReference<String>(s);
s = null;
System.out.println(swf.get());//输出为abc
System.gc();
System.out.println(swf.get());//输出为null
}
从上面的例子可以看出,当发生了一次GC,弱引用对象就会被回收,不管内存是否充足。
4.虚引用(PhantomReference)
"虚引用"顾名思义,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
3.垃圾收集算法
3.1 标记-清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:
1.效率过低:标记和清除过程的效率都不高
2.空间问题:标记清除后会产生大量的不连续的内存碎片,影响后续的内存分配
3.2 复制算法
复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
1.这种算法的代价是将内存缩小为原来的一半
2.长生存期的对象多次被复制,导致垃圾收集效率降低
3.3 标记-整理算法
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
3.4 分代收集算法
前提:假设绝大部分对象的生命周期都非常短暂。
“分代收集”(Generational Collection)算法:
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
新生代的垃圾回收(主要以复制算法为主):
- 新生成的对象首先存放在新生代,新生代的目标就是尽可能快速地收集掉生命周期短的对象
- 新生代内存按照比例分为一个Eden区和两个Survivor区域,大部分对象在Eden区中生成。回收时,先将Eden区存活的对象复制到From Survivor区,然后清空Eden区。当From Survivor区也满了的时候,就将Eden区和From Survivor区的存活对象复制到To Survivor区,然后清空Eden区和From Survivor区。最后交换两个Survivor区,保证总有一个Survivor区是空的。
- 当To Survivor区不足以存放Eden区和From Survivor区的存活对象,就直接将存活对象存放到老年代。若是老年代也满了,就触发一次Full GC。
老年代的垃圾回收(以标记-整理算法为主):
对象如何晋升到老年代
1.经历了一定次数的MinorGC依然存活的对象
2.Survivor区放不下的对象
3.新生成的大对象(可以通过-XX:+PretenuerSizeThreshold来设置阈值)
老年代存放的都是一些生命周期较长,存活率较高的对象,因此适合标记-整理算法。
4.垃圾收集器
常见的新生代垃圾收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器
常见的老年代垃圾收集器:Serial Old收集器、Parallel Old收集器、CMS(Concurrent mark sweep)收集器
新生代和老年代都能用的收集器:G1(Garbage-First)收集器
垃圾收集器运行过程中的一个核心问题就是:Stop the world问题
垃圾收集过程中的Stop-the-world:
本质:JVM由于要执行GC而暂停了应用程序的执行
发生场景:在任何一种GC算法中都会发生
不同的垃圾收集器通过减少STW的发生时间来提高自己的性能
1.Serial 收集器
参数控制:-XX:+UseSerialGC
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-整理。由于它是一个单线程的垃圾收集器,因此他在垃圾收集的过程中暂停其它所有的线程工作(Stop The World),直到收集结束。
优点:由于没有线程交互的开销,因此Serial 收集器可以获得很高的单线程收集效率。
2.ParNew 收集器
参数:-XX+UseParNewGC
-XX:ParallelGCThreads
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。新生代采用标记-复制算法,老年代采用标记-整理算法。
优点:除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
3.Parallel Scavenge 收集器
参数:-XX:UseParallelGC
Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量。
以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-整理。
JDK1.8 默认使用 Parallel Scavenge + Parallel Old。
4.Serial Old 收集器
参数:-XX+useSerialOldGC
Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。
5.Parallel Old 收集器
参数:-XX:UseParallelOldGC
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
6.CMS 收集器
参数:-XX:UseConcMarkSweepGC
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器.由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行(其他回收器的GC线程不与用户线程并发执行)。
优点:并发收集、低停顿
缺点:对CPU资源敏感,并发阶段会产生吞吐量,会产生空间碎片(这本质上是标记—清除算法的缺点)
Q:为什么ParNew可以和CMS配合使用而Parellel Scavenge不行
A:这与Hotspot VM的历史有关。Parallel Scavenge是不在“分代框架”下开发的,而ParNew、CMS都是在分代框架下开发的。
7.G1 收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
与CMS收集器相比G1收集器有以下特点:
- 空间整合,G1收集器采用标记-整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
- 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性。
4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
5.GC触发条件
5.1 Minor GC
当Eden区分配对象申请空间失败的时候,说明Eden区已经分配满了,就会触发一次MinorGC。
5.2 Full GC
有以下原因可能会触发Full GC:
1.System.gc( )方法的调用:此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。
2.老年代空间不足:老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象。为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
3.统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间:这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
4.CMS收集器:定时检查老年代的使用量,当使用量超过了一定比例就会触发一次GC,对老年代做并发收集。