gc基础
JVM架构图分析
下图:参考网络+书籍,如有侵权请见谅
内存分区
java中的内存是由java虚拟机自己去管理的,java的内存分配分为两个部分,一个是数据堆,一个是栈
堆内存用来存放由new创建的对象和数组,在堆中分配的内存由java虚拟机的自动垃圾回收器来管理;
栈用来存放类的信息的,它和堆不同,运行期内GC不会释放空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间:
1、如果程序声明了static的变量,就直接在栈中运行的,进程销毁了,不一定会销毁static变量。
2、在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配
内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。
新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发Minor GC(也称作Youn GC)。
旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。
IBM研究表明,新生代 98% 的对象都是朝生夕死的,所以并不需要 1:1 来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间
JVM GC地点(收堆区和方法区)
需要注意的是,JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
对象(回收那些对象)
判断哪些Java对象不再被使用,需要被回收。通常有两种算法:引用计数算法、可达性分析算法。
引用计数算法(已被淘汰)
引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
存在的问题:主流的Java虚拟机里面都没有选用引用计数算法来管理内存,因为单纯的引用计数很难解决对象之间 相互循环引用 (A引用B,B引用A)的问题。
可达性分析算法(主流的算法)
当前主流的商用程序语言(Java、C#,上至古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
算法的基本思路:通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链”(Reference Chain),如果某个对象到 GCRoots 间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java语言中,可作为 GC Roots 的对象包括下面几种:
1 | (1)虚拟机(JVM)栈中引用对象 |
GC算法
常见的GC算法:复制、标记-清除和标记-压缩
复制(新生代Eden区)
复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域,如图所示:
当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。
标记-清除
该算法采用的方式是从根集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。标记和清除的过程如下:
上图中蓝色部分是有被引用的对象,褐色部分是没有被引用的对象。在Marking阶段,需要进行全盘扫描,这个过程是比较耗时的。
清除阶段清理的是没有被引用的对象,存活的对象被保留。
标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片。
标记-压缩(适用于旧生代)
该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针,如下图所示
由于进行了移动规整动作,该算法避免了标记-清除的碎片问题,但由于需要进行移动,因此成本也增加了。(该算法适用于旧生代)
分代收集(多回收方式结合)
针对对象的存活周期将内存划分为几块,一般为 新生代 和 老年代,根据不同快对象的存活特点,选择合适的收集算法。新生代的算法存活率比较低,可以选择 Copying,老年代的对象存活率高,并且没有担保空间,可以选择 Mark-Compact算法。
JVM GC时间(何时gc)
最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
GC算法优劣标准
评价一个垃圾收集GC算法的两个标准
吞吐量(throughput)越高算法越好
jvm中垃圾回收算法的吞吐量通常被认为是单位时间内释放内存空间的大小信息。
举个例子:gc执行了两次, 总时间为(A+B). 假设对内存大小为S, 则平均吞吐量为: S/(A+B).
暂停时间(pause times)越短算法越好
多种垃圾收集器
JVM中Serial收集器、ParNew收集器、Parallel收集器解析
Serial收集器 单线程方式(没有线程切换开销,如果受限物理机器单线程可采用)串行且采用stop the world在工作的时候程序会停止Serial和serial old
ParNew收集器:多线程(多CPU和多Core的环境中高效),生产环境对低延时要求高的话,就采用ParNew和CMS组合来进行server端的垃圾回收
Parallel 收集器:多线程,并行, 它可以控制JVM吞吐量的大小,吞吐量优先的收集器,一般设置1%,可设置程序暂停的时间,会通过把新生代空间变小,来完成回收,频繁的小规模垃圾回收,会影响程序吞吐量大小
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上。CMS收集器是基于“标记-清除”算法实现,CMS收集器的内存回收过程是与用户线程一起并发执行的。优点:并发收集、低停顿。
不同垃圾收集器
其他
STW
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
堆与栈里存什么
1)堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用。
2)为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
3)Java中的参数传递时传值呢?还是传引用?程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
堆内存与栈内存的区别
申请和回收方式不同:栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
碎片问题:对于栈,不会产生不连续的内存块;但是对于堆来说,不断的new、delete势必会产生上面所述的内部碎片和外部碎片。
申请大小的限制:栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间,就会产生栈溢出;对于堆,是向高地址扩展的数据结构,是不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较:栈由系统自动分配,速度较快。但程序员是无法控制的;堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
java中的引用
强引用(Strong Reference):
在代码中普遍存在的,类似”Object obj = new Object”这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
软引用(Sofe Reference):
有用但并非必须的对象,可用SoftReference类来实现软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存异常异常。
弱引用(Weak Reference):
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用。
注意:这种表述,感觉是有问题的,
看一下官方文档对它做的说明
弱引用对象的存在不会阻止它所指向的对象变被垃圾回收器回收。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。
假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后垃圾收集器会把这个弱可达对象标记为可终结(finalizable)的,这样它们随后就会被回收。与此同时或稍后,垃圾收集器会把那些刚清除的弱引用放入创建弱引用对象时所登记到的引用队列(Reference Queue)中。
简单来说:弱引用对象无法独立存在,必须有强引用或者软引用才行(指向它),否则就会被回收,有点像守护线程的感觉。
虚引用(Phantom Reference):
也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。
软引用(Soft Reference):软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些
虚引用(Phantom Reference):虚引用是Java中最弱的引用,那么它弱到什么程度呢?它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被销毁。
3. How——如何使用弱引用?
拿上面介绍的场景举例,我们使用一个指向Product对象的弱引用对象来作为HashMap的key,只需这样定义这个弱引用对象:
1 | productA = new Product(...); |
young old 参数
Serial Serial old -XX:+UseSerialGC
ParNew Serial old -XX:+UseParNewGC
Parallel Scavenge Serial old -XX:+UseParallelGC
Parallel Scavenge Parallel Old -XX:+UseParallelOldGC
ParNew CMS + Serial Old -XX:+UseConcMarkSweepGC
Serial CMS -XX:+UseConcMarkSweepGC -XX:-UseParNewGC
![del04](20220724173504215_182177140.jpg)
**最新关系以这个为准**:
![del02](20220912103735904_871529671.jpg)
新生代收集器:Serial、ParNew、Paralle1 Scavenge;
老年代收集器:Serial old、Parallel old、CMS;
整堆收集器:G1;
![del02](20220912104010715_994751490.jpg)
以上信息可整理为如下:
![del02](20220912104805356_92961349.jpg)
![del02](20220912103514566_1893059515.jpg)
两个收集器间有连线,表明它们可以搭配使用:Serial/Serial old、Serial/CMS、ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial 0ld、Parallel Scavenge/Parallel 0ld、G1;
其中Serial old作为CMS出现"Concurrent Mode Failure"**失败的后备预案**。
(红色虚线)由于维护和兼容性测试的成本,**在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除**。
(绿色虚线)JDK14中:弃用Paralle1 Scavenge和Serialold GC组合(JEP366)
(青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
附
CMS:
![del02](20220912112145982_1550734827.jpg)
G1:
![del02](20220912112050855_1985291468.jpg)
## 参考
JVM架构和GC垃圾回收机制(JVM面试不用愁):https://blog.csdn.net/aijiudu/article/details/72991993
GC Root 对象有哪些:https://blog.csdn.net/baidu_20608025/article/details/87936633
JVM如何判断Java对象是否存活,是否要被GC回收?:https://blog.csdn.net/hbtj_1216/article/details/104139941
深入理解JVM的内存结构及GC机制:https://blog.csdn.net/anjoyandroid/article/details/78609971
JVM GC:https://www.jianshu.com/p/e8ee087bcd4f
最详细的JVM&GC讲解高广超:https://www.jianshu.com/p/99772ad092d3
Java性能优化之JVM GC(垃圾回收机制):https://zhuanlan.zhihu.com/p/25539690
对象会从年轻代进入老年代:https://blog.csdn.net/zhxdick/article/details/112131452
Major GC和Full GC的区别是什么?触发条件呢?:https://www.zhihu.com/question/41922036
JVM常见的垃圾回收器:https://blog.csdn.net/weixin_43228814/article/details/88934939
JVM成神之路-Java垃圾回收:https://blog.csdn.net/w372426096/article/details/81360083
理解Java中的弱引用(Weak Reference):https://www.cnblogs.com/absfree/p/5555687.html
Java 如何有效地避免OOM:善于利用软引用和弱引用:https://www.cnblogs.com/dolphin0520/p/3784171.html
垃圾收集器分类与GC性能指标:https://blog.csdn.net/Weixiaohuai/article/details/119358662