前言
回收,旧手机,旧冰箱,旧空调,旧洗衣机,电瓶车摩托车,自行车,报纸,塑料......
还记得小时候,我喝完的饮料瓶子都不会扔,每次都放到阳台。小区里听到收废品的吆喝,感觉带着这些瓶瓶罐罐冲下楼,换几块钱买雪糕,想想都是童年的回忆啊。
我一直都觉得骑个三轮车,回收废品的大爷特别酷,因为感觉他的车上面就像哆啦A梦的口袋,翻一翻什么都有。不过这些年,随着垃圾分类,感觉收废品的大爷也越来越少了。
回过头想,如果没有这些收废品的大爷,那我攒的瓶子也卖不了钱,家里阳台那么多瓶子还占地方。所以你大爷就是你大爷,主动过来帮你清理垃圾,还给你钱。
所以为什么Java越来越流行,除了说它一处安装,到处运行的机制以外。还因为程序员也越来越懒,跟C/C++相比,Java最适合懒人的便是引入了自动垃圾回收的机制,也就是GarageCollection(下文简称GC)。
网上对于Java垃圾回收的介绍堪称冠冕堂皇:
让程序员专注于程序本身,不用关心内存回收这些恼人的问题,真正让程序员的生产力得到了释放,程序员不用感知到它的存在。
说这么多,不就是程序员懒么,Java直接帮你把脏活累活都干了。就像咱们现在人都爱点外卖,为什么?因为不用自己动手,吃完也不用洗碗。还有你去餐盘吃饭,吃完就走,服务员会替你收拾好这些餐盘,你不会关心服务员什么时候来收,怎么收。
大家可能会说既然Java这么方便,已经帮我们完成了对垃圾的清理与回收,那GC方面的知识我不用了解好像也没事吧。但是人有失手,马有失蹄,假如突然有一天外卖小哥带着你的外卖小哥跑路了,你必须要亲自动手下厨,总不能饿死吧。
所以对于GC,道理也是一样的,线上的服务不遇到问题还好,出现Bug或者想自己做一些性能调优的时候,就需要对GC有深入了解才可以,这也是成为一名优秀Java程序员的必修课!
今天就把JVMGC相关的知识详细介绍一下,本文将会从以下几个方面来讲述相关知识,文字较多,相信大家耐心看了之后肯定有收获,码字不易,别忘了「在看」,「转发」哦。
JVM内存区域回收策略垃圾回收经典算法垃圾回收器对比
JVM内存区域
我们首先要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域,所以我们一起来看下JVM的内存区域。
JDK8以前
在JDK8之前的虚拟机,主要包含:
(1)堆
对象实例和数组都是在堆上分配的,GC也主要对这两类数据进行回收,这里是GC发生的主要区域!
(2)方法区(永久代)
方法区在JVM中是一个非常重要的区域,它与堆一样是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
方法区是堆的一个逻辑部分,为了区分Java堆,它还有一个别名Non-Heap(非堆)。相对而言,GC对于这个区域的收集是很少出现的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
随着动态类加载的情况越来越多,这块内存越来越不太可控。如果设置小了,当JVM加载的类信息容量超过了这个值,系统运行过程中就容易出现内存溢出OOM:PermGen的错误,设置大了又浪费内存。
(3)栈
栈是线程私有的,生命周期与线程相同,主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息。这块区域是不需要进行GC的。
(4)程序计数器
程序计数器也是线程私有的,它里面记录了下一次需要执行的行号,这块区域也不需要进行GC。
(5)本地方法栈
本地方法栈主要为了虚拟机执行Java的本地方法(NativeMethod)时服务,这块区域也不需要进行GC。
JDK8之后
JDK8最大的变化就是对JVM内存空间进行了改造,主要的区别是将方法区进行了移除,并新增了元空间,元空间是放置在JVM内存空间之外的直接内存中,并且JDK8中对于方法区的参数PermSize和MaxPermSize已经失效。
上文咱们已经介绍过,JDK8之前方法区放在JVM之中,但是随着动态类加载的情况越来越多,很容易因为大小的限制导致内存溢出OOM:PermGen的错误。
所以JDK8之后把使用元空间替代了原来的方法区,在这种架构下,元空间就突破了原来-XX:MaxPermSize的限制。这样就从一定程度上解决了原来在运行时生成大量类造成经常FullGC问题,如运行时使用反射、代理等,所以升级以后Java堆空间可能会增加。
垃圾回收策略
凡事都讲解个策略,那么Java怎么判断堆中的对象实例或数据是不是垃圾呢,应不应该把它回收掉呢?
引用计数法
第一种最简单粗暴的就是引用计数法。当对象被引用,程序计数器+1,释放时候-1,当为0时证明对象未被引用,可以回收。
但是这个算法有明显的缺陷,对于循环引用的情况下,循环引用的对象就不会被回收。例如下图:对象A,对象B循环引用,没有其他的对象引用A和B,则A和B都不会被回收。
可达性分析
第二种策略明显好的多,也就是所谓可达性分析法。它指的,通过一系列称之为“GCRoots”的对象作为起点;从此起点向下搜索,所走过的路径称之为引用链,当一个对象到GCRoots没有任何引用链相连接,代表此对象不可达。
在Java可以作为GCRoots的对象包括:
1、虚拟机栈(帧栈中的局部变量表)中的引用对象;
2、方法区中类静态属性引用的对象;
3、方法区中常量引用的对象;
4、本地方法栈中JNI(即一般说的Native方法)的引用对象;
画外音:GCRoots有哪些这个问题经常在面试中被问到,大家一定牢记!
垃圾回收经典算法
知道了应该对哪些对象进行回收,那接下来就要看如何回收了,经典的垃圾回收算法有三种。
标记-清除算法
在gc时候,首先扫描时对需要清理的无用对象进行标记,然后将这些对象直接清理。
操作起来非常很简单,但仔细想想有什么问题呢?
没错,内存碎片!如上图,如果清理了两个1kb的对象,再添加一个2kb的对象,是无法放入这两个位置的。
怎么解决呢,如果能把这些碎片的内存连起来就可以了!
标记-整理算法
标记-整理算法就是在标记-清理算法的基础上,多加了一步整理的过程,把空闲的空间进行上移,从而解决了内存碎片的问题。
但是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
复制算法
复制算法是将空间一分为二,在清理时,将需要保留的对象复制到第二块区域上,复制的时候直接紧凑排列,然后把原来的一块区域清空。
不过复制算法的缺点也显而易见,本来JVM堆假设有M内存,结果由于将空间一分为二,真正能用的变成只有50M了!这肯定是不能接受的!另外每次回收也要把存活对象移动到另一半,效率低下。
分代算法
分代收集算法整合了以上算法,综合了这些算法的优点,最大程度避免了它们的缺点。与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起,我们先从下图看看对象的生存规律。
由图可知,大部分的对象都很短命,一般来说,98%的对象都是朝生夕死的,所以分代收集算法根据对象存活周期的不同将堆分成新生代和老生代。
新生代和老年代的默认比例为1:2,新生代又分为Eden区,fromSurvivor区(简称S0),toSurvivor区(简称S1),三者的比例为8:1:1。
根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的GC称为YoungGC(也叫MinorGC),老年代发生的GC称为OldGC(也称为FullGC)。
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC;
MinorGC非常频繁,一般回收速度也比较快;出现了FullGC,经常会伴随至少一次的MinorGC,FullGC的速度一般会比MinorGC慢10倍以上。
整个过程大致分为以下几个步骤:
(1)当Eden满了后,进行MinorGC,将需要保存的数据复制到S0中;
(2)然后清空Eden和S1区域,需要保留的对象目前在S0中;
(3)下一次当Eden满了后,进行MinorGC,将原来S0存在的数据复制到S1中,将Eden中需要保存的数据也复制到S1中;
(4)清空Eden和S0区域,需要保存的对象目前都在S1中;
(5)Eden+S0复制到S1;
(6)Eden+S1复制到S0;
(7)Eden+S0复制到S1;
周而复始...
垃圾回收器对比
前面的内容更多的是方法论,真正执行垃圾回收的要靠各个垃圾回收器。
Java虚拟机规范并没有规定垃圾收集器应该如何实现,因此一般来说不同厂商,不同版本的虚拟机提供的垃圾收集器实现可能会有差别,一般会给出参数来让用户根据应用的特点来组合各个年代使用的收集器,主要有以下几种垃圾收集器。
Serial收集器
从名字看出这是一个单线程收集器。串行垃圾回收器在进行垃圾回收时,它会持有所有应用程序的线程,冻结所有应用程序线程,使用单个垃圾回收线程来进行垃圾回收工作。
它是JDK1.3之前新生代的回收器的唯一选择,在单线程的情况效果很好,因为单线程没有线程的切换的开销。但是在现在大部分都是多CPU的服务器,所以它现在被使用的很少了。
但是它还是JVM运行在Client模式下的默认垃圾收集器。因为一般桌面应用下新生代空间不是很大,使用这个垃圾回收器也可以保证回收的时间在毫秒左右。
Serial-Old收集器
这个收集器就是serial收集器的老年版本,他同样还是单线程的垃圾回收器。它存在的主要意义的还是JVM运行在client模式下的默认老年代回收器跟serial收集器一起使用,同样它还作为CMS垃圾回收器的后备垃圾回收器。
ParNew收集器
ParNew垃圾收集器就是serial回收器的多线程版本,有很多的代码都是和serial收集器公用的。一个很重要的作用就是作为新生代的垃圾回收器跟CMS垃圾回收器进行组合。但是在单核CPU的情况下,效率是没有serial垃圾回收器的效果好的。
可以通过-XX:UseConcMarkSweepGC或者-XX:UseParNewGC来指定使用它。默认情况它用于回收垃圾的线程的数目跟CPU的数目相同。可以通过-XX:parallelGCThreads来指定使用的垃圾回收的线程的数目。
ParallelScavenge收集器
与ParNew线程一样同样为多线程的垃圾回收器,但是这个垃圾回收器和其他回收器的