目录
一、回收什么?
二、何时回收?
引用计数法
根搜索算法-GC-root算法
引用
强引用
软引用
弱引用
虚引用
垃圾回收条件
方法区的回收
三、如何回收?
垃圾回收算法
1.标记-复制
2.标记-清理
3.标记-整理
垃圾收集
新生代垃圾收集器
1. Serial收集器 - 复制算法
2. ParNew 收集器 - 复制算法
3. Parallel Scavenge并行回收收集器 - 复制算法
老年代垃圾收集器
1. Serial Old 收集器 - 标记整理算法
2. Parallel Old收集器 - 标记整理算法
备注:常用参数
进过技术不断的发展,内存的动态分配与内存回收技术已经相当成熟,一切看起来都以进入“自动化”的时代,那我们为什么还要去了解gc和内存分配呢?因为自动的好处是我们暂时可以不用管,省心,前提是一切运行正常。但是如果程序运行中发生了内存的泄漏和溢出,或者当垃圾回收成为系统达到高并发的瓶颈的时候,我们就需要对这些“自动化”的技术实施必要的脚控和调节。
问题导读:
java内存运行时区域的各个部分,其中程序计数器/虚拟机栈/本地方法栈三个都是随着线程而生,随线程而灭;栈中的每一个栈帧分配多少内存基本上是在类结构确定下来的时候就大体已经确定了【除了JIT编译器进行的一些优化】,所以随着方法的结束和线程的结束,内存自然就跟着回收了。但是堆和方法区不一样,我们只有在程序运行的时候才知道分配那些内存和创建的对象,这部分的内存分配和回收都是动态的,由于其不确定性所以需要格外的关注。
java所创建的对象基本上都存储在堆区,虽然“永久代”方法区的垃圾回收效率比较低,但是“永久代”对于废弃的常量和无用的类,也会进行相应的回收,尤其大量使用反射和动态代理CGlib等场景都需要虚拟机具备类卸载功能,保证无用的资源可以被回收,不会发生溢出。这里要回收就要确定该对象是否“存活”或者“死去”;“死去”的对像就是不可能再被任何途径使用的对象,这些对象就可以被回收。回收什么,就是要回收内存中一切不再利用的资源。例如回收一些堆中无用的对象,方法区中废弃的常量和类等。
当该对象没有任何其他对象和程序的引用,即计数为0就可以回收。
基本思想是:进过一系列名为“gc-root"的对像作为起始点,从这些节点向下走的路径称之为引用链,当一个对象到gc-root没有任何引用链相连的时候,则证明此对象是不可达的。不可达的对象即可回收。
GC-root对象包含:
如果reference类型的数据中存放的数值是另外一块内存的起始地址,就称这块内存代表着一个引用。
JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
软引用SoftReference是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,它的唯一目的是当对象被系统回收时收到一个系统通知。
引入引用的作用
在搜索算法中的不可达对象,也并非是非死不可的。真正宣告一个对象的死亡需要经以下两个条件:
finalize()方法是对象逃过死亡的最后一次机会。但是finalize方法只会被调用一次,如果有必要执行finalize方法,且该对象不行被回收,则只需要在F-Queue队列中重新引用上任何一个对象即可。如果此时对象任然没有任何的引用,那么就会被稍后GC正式的回收。流程如下:
废弃的常量:
无用的类:
如何回收就需要良好的垃圾回收算法,这个也是能让程序员解脱内存管理,专心编码的关键所在。大体主要分为3类垃圾回收算法。
主要思想是:将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。
优点 | 实现简单,效率高; 不会存在内存碎片; |
缺点 | 就是需要2倍的内存来管理; 复制需要时间开销; |
使用场景 | 新生代垃圾收集器基本都采用标记复制; |
我们可以看到参数:-XX:SurvivorRatio=8,表示新生代中Eden区域和Survivor区域的容量比值,代表Eden:Survivor=8:1。一个Survior占新生代的1/10,表达的意思是Survivor中的from区间和to区间 与 Edon的比例是1:1:8,from和to的空间也就是采用了标记复制的算法,所以空间大小一样,且程序运行期间浪费年轻代的1/10。
标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
优点 | 清除效率高; |
缺点 | 存在内存碎片; |
使用场景 | 老年代垃圾收集器CMS采用“标记-清理”但是可以通过设置灵活改变; G1垃圾收集的全局看是基于“标记-清除”,局部是标记-整理; |
与“标记-清理”不同的是标记完存活对象,清理完对象后,将所有存活的对象都向一端移动,并更新引用其对象的指针,进行了内存的复制整理。因为要移动对象,所以它的效率要比“标记-清理”效率低,但是不会产生内存碎片。
优点 | 没有碎片; |
缺点 | 复制移动资源需要时间的开销; 指针的改变增加了程序的消耗; |
使用场景 | 老年代的串行收集器和并行收集器都采用“标记-整理”的算法; |
基于分代的思想
由于java大部分对象都具有“朝生夕死”的特性,所以对于存活时间长的对象,减少被gc的次数可以避免不必要的开销。这样我们就把内存分成新生代和老年代,新生代存放刚创建的和存活时间比较短的对象,老年代存放存活时间比较长的对象。这样每次仅仅清理年轻代,也就是Minor GC,非常频繁,但是速度较快;老年代的Major GC又称[Full GC]仅在必要时时再做清理可以极大的提高GC效率,节省GC时间。
1.新生代的收集器包括:
2.老年代的收集器包括:
3.回收整个Java堆(新生代和老年代)
几种收集器及它们之间的组合关系如下:
Serial收集器是一个新生代收集器,单线程收集器,使用复制算法。进行收集时,必须暂停所有的线程。
优点 | 对与单cpu,实现简单高效 |
缺点 | 单线程工作不能充分利用多核cpu性能;之间会产生STW,给用户带来不良体验; |
使用场景 | 新生代垃圾收集器,虚拟机运行在Client模式下的默认新生代收集器 |
使用参数 | 串行收集器,分为年轻代 Serial 和老年代 Serial Old 收集器。
而 Serial 收集器出现的日志为 DefNew . 注:【“+” 号的意思是ture,开启,反之,如果是 “-”号,则是关闭】 |
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
单CPU,ParNew 不会比Serial收集效果更好,但是随着CPU的数量增加。ParNew则在GC时对系统资源有效利用更好。另外,从图一可以看出,除了serial收集器外,只有ParNew收集器可以和cms收集器愉快的合作;
优点 | 在多核cpu的情况下,gc效率更高; |
缺点 | 单核的效率 <= Serial; 产生STW; |
使用场景 | 新生代垃圾收集器,在Server模式下,ParNew收集器是一个非常重要的收集器,且能与CMS垃圾收集配合使用。 |
参数使用 | 并行收集器是 Serial 的多线程版本,在 CPU 并行能力强大的计算机上有很大优势。 其中:
而 ParNew 的 GC 日志则表吸纳出 ParNew。 |
Parallel Scavenge收集器也是一个新生代并行多线程收集器。parallel Scavenge收集器的目标则是:追求可控制高吞吐量+高效利用 CPU。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
优点 | 吞吐量可控,让用户代码获得更长的运行时间;并行收集高效利用多核cpu; |
缺点 | (1) 标记和清理产生2次STW; (2) 不能和cms一起愉快的合作,框架不同; |
使用场景 | 该收集器是 Java 8 的默认新生代收集器,因为它能够根据系统当前状态给出吞吐量最高的GC 配置。所以,在一些手工调优复杂的场合或者对实时性要求不高的场合,可以使用该处理器。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 |
参数使用 |
注: 参数1-2其实是矛盾的,吞吐量和停顿时间是反比的,需要找到一个平衡点。如果是调参场景比较复杂的情况下,可采用自适应策略。PS 处理器的 GC 日志则是 PSYoungGen。 |
Serial Old是Serial收集器的老年代垃圾回收,它同样是一个单线程(串行)收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。工作流程如图三所示,这里不再重复。
优点 | 单线程工作效率高,实现简单; |
缺点 | 产生STW;多核CPU不能充分利用其性能 |
使用场景 |
|
参数使用 | 参考Serial收集器的使用 |
Parallel Old是Parallel Scavenge
收集器的老年代版本
,也是一种关注系统吞吐量的老年代垃圾回收机制。使用多线程
和“标记-整理”
算法。 之前一直是Parallel Scavenge + old serial的组合,由于无法真正的利用多核cpu的性能,所以其吞吐量反而不一定有PreNew+CMS
组合给力,现在终于才有了新生代和老年代都关注的吞吐量的新组合:Parallel Scavenge + Parallel Old
。
优点 | 高效利用cpu;获得较大的吞吐量;可以和Parallel Scavenge一起愉快的合作; |
缺点 | |
使用场景 | 老年代垃圾回收,注重吞吐量以及CPU资源敏感的场景,“吞吐量优先” 收集器的名副其实组合:Parallel Scavenge + Parallel Old 组合。 |
参数使用 |
|
JVM常用配置参数
配置参数 | 功能 |
---|---|
-Xms | 初始堆大小。如:-Xms4g |
-Xmx | 最大堆大小。如:-Xmx4g |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
GC日志打印参数参考
gc日志打印参数 |
|
参考资料:
《深入了解jvm虚拟机》
https://www.cnblogs.com/liyutian/p/9690974.html
https://www.jianshu.com/p/dbd32622ad20
https://blog.csdn.net/qq_31156277/article/details/79962445
https://www.cnblogs.com/yang-hao/p/5936059.html
https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://www.cnblogs.com/yunxitalk/p/8987318.html