Java的GC【垃圾回收】
GC英文全称为Garbage Collection,即垃圾回收。
Java中的GC就是对内存的GC,内存管理实际就是对对象的管理,其中包括对象的分配和释放。
堆(heap)和栈(stack)
堆
:是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程,C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁时都要占用时间,所以用堆的效率低的多!但是堆的好处是可以做的很大,C/C++对分配的Heap是不初始化的。
- 堆是由操作系统管理的一片空间,事先是没有在进程空间里分配的(比如你在没有分配堆的时候就访问堆空间会报一个内存访问错误),一般是由程序动态的分配出来,一旦分配了以后,一般需要程序去释放自己的堆空间.
- 堆的空间较大,但访问速度没有栈快
- 堆受垃圾处理器GC管理(GC会去找那些很久没有引用地址指向的内存块,把它们清理掉)
栈
:是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FILO的特性,在编译的时候可以指定需要的Stack的大小。
- 栈上是向下填充的,数据只能从栈的顶端插入和删除(先进后出原则)。把数据放入栈顶称为入栈(push),从栈顶删除数据称为出栈(pop)。
- 栈的空间较小,但访问速度快。
- 栈的生长方向是有高地址向低地址生长的。
- 栈的清理是由系统自动完成的。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的
",哪些对象是"不可达的
".当GC确定一些对象为"不可达
"时,GC就有责任回收这些内存空间。
注:判断对象"不可达",一般有两种方法,即引用计数法和可达性分析法。
引用计数法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。
这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。
可达性分析
在Java中采取了可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
在java中,有以下几种对象可以作为GCRoot
Java虚拟机栈(局部变量表)中引用的对象
方法区中静态引用指向的对象
仍处于存活状态中的线程对象
Native方法中的JNI引用
什么时候回收
不同虚拟机有不同的回收机制,一般都会在下面两种情况下触发垃圾回收
Allocation Failure:在堆中,可用内存分配不足导致内存分配失败的时候,系统会触发GC。
System.gc():应用层,主动调用此api来请求一次GC。
如何回
垃圾回收算法分为三种,分别为标记-清除算法
,复制算法
,标记-整理算法
。
标记-清除算法(Mark-Sweep)
从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。
-
Mark 标记阶段
:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。 -
Sweep 清除阶段
:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。
- 优点:实现简单,不需要将对象进行移动。
- 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
复制算法(Copying)
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图1所示
标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图2所示:
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
标记-整理算法(Mark-Compact)
需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:
Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
- 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
- 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
jVM分代回收策略
Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为
老年代(Tenured Generation)
和新生代(Young Generation)
,老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
一般是把Java堆分为新生代
和老年代
。
新生代
:
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收
70%~95%
的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。新生代又可以继续细分为 3 部分:Eden
、Survivor0
(简称 S0)、Survivor1
(简称S1)。这 3 部分按照8:1:1
的比例来划分新生代。
绝大多数刚刚被创建的对象会存放在 Eden 区,当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。
如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。
老年代
:
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。我们可以使用
-XX:PretenureSizeThreshold
来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。
新生代和老年代算法
目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的
Survivo
r空间,每次使用Eden
空间和其中的一块Survivor
空间,当进行回收时,将Eden
和Survivor
中还存活的对象复制到另一块Survivor
空间中,然后清理掉Eden
和刚才使用过的Survivor
空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)
QA
- 那些对象可以作为java中的 GC Root ?
1、虚拟机栈(javaStack)(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2、方法区中的类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(Native方法)引用的对象。
- 什么时候会触发 GC ?
Allocation Failure
:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次GC
。
Young GC
一般是在新生代的Eden
区满了之后触发的,每次发生Young GC
之前会进行检查,当老年代可用内存小于新生代全部 对象的大小,而这时候没开启空间担保参数(HandlePromotionFailure=false)
,会直接触发Full GC
(清理整个堆空间);当开 启空间担保参数(HandlePromotionFailure=true)
时,如果【老年代可用的连续内存空间】 < 【新生代历次 Young GC 后升入 老年代的对象总和的平均大小】,则说明本次Young GC
后升入老年代的对象大小可能会超过老年代当前可用内存空间。此时必须 先触发一次Old GC
让老年代腾出更多的空间,而后再执行Young GC
。
那么执行Young GC
之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一 次Old GC
如果老年代内存使用率超过了92%
,也要直接触发Old GC
,当然这个比例是可以通过参数调整的(Java8中,-XX:CMSInitiatingOccupancyFraction
的默认值是92)
System.gc()
:在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
- GC 算法有哪些?
标记清除算法、复制算法、标记整理算法。
- 新生代用的什么 GC 算法,老年代用的什么 GC 算法?
新生代用的是复制算法、老年代用的是复制整理算法。
- 标记清除算法为什么要暂停其他线程?
java采用的是可达性分析算法,可达性分析算法中枚举根节点(GC Roots)会导致java执行线程停顿。
分析工作必须在一个能确保一致性快照中进行。
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上。
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。
STW事件和采用那款GC无关,所有的GC都有这个事件。
哪怕是G1也不能完全避免Stop-the-World 情况发生,只能说垃圾回收器越来越优秀,回收效率越高,尽可能的缩短暂停时间。
开发中不要使用System.gc();会导致Stop-the-World的发生。
- 分代回收策略
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代
分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。
参考资料
https://www.jianshu.com/p/65359a218ffc
https://blog.csdn.net/yrwan95/article/details/82829186
https://blog.csdn.net/weixin_44027397/article/details/114383323
https://blog.csdn.net/qq_44787816/article/details/119239324
https://blog.csdn.net/qq_21267357/article/details/125130408
https://blog.csdn.net/Hao_JunJie/article/details/124154697
https://blog.csdn.net/Hao_JunJie/article/details/123670254