GC回收也是jvm学习中非常重要的一环,在栈中栈帧是栈的主要内存结构,每一个栈帧在栈中占用的内存基本都是确定的,随着方法的调用结束,栈帧内存将会被回收,随着这整个线程的结束,栈的内存也会随之被回收,而程序计数器只是一块很小的内存,只是用存储字节码执行的行号,它伴随的持有它的线程销毁而被回收,而在堆内存和方法区中,GC回收就不是那么简单的事情了
GC回收主要判断对象是否还“活着”,判断对象是否还活着的主要方法由两个:
引用计数算法:在jvm中每个对象都有一个引用计数器,每当有地方引用了这个对象时就会给这个计数器加1,当引用失效时计数器就会减1,当计数器为0时,GC就会判断它死了然后将它被收回,这个算法虽然简单而且执行效率非常的高(可以做到几乎不影响程序执行),但是它也会有一个问题:它无法解决对象之间的相互引用,而导致内存溢出
可达性分析算法:可达性分析算法是从离散数学的图论引入的,判断对象的引用链是否可达,从而判断对象是否可以被回收掉。把一个叫做GC Roots的点看做是对象的起始点,从这个点出发向下搜索能够与对象的相连的,就认为它是可达的,如果不相连就是不可达的,我们可以把这个结构看做是一个内存图,GC会对这个图,按照从GC Roots往下搜索的规则进行遍历,查找不可达对象然后将他们的内存清理。
哪些对象可以看做是GC Roots呢?
其实只要jvm中调用的方法中正在被引用的对象都可以作为GC Roots,这些对象主要如下所列:
1、虚拟机栈中引用的对象(栈中的本地变量表);2、方法区中变量引用的对象、类静属性态引用的对象;3、本地方法栈中的Native方法引用的对象;4、活跃线程中被引用的对象
这里提多次提到的引用,我们又可将其分类,按照强弱程度依次排列为:强引用、软引用、弱引用、虚引用。
强引用:类似于当一个对象被new出来后,并且该对象的引用在方法中被调用,这时该对象的引用存在于栈中,而对象的实际内容存在于堆中,这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象就会被回收。当对象的引用还在栈中并且内存不足时,jvm宁愿抛出OutofMemoryError也不会去回收该引用对象
软引用:如果对象是软引用,内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。jdk1.2后,提供了SoftReference实现软引用
弱引用:弱引用最多只能生存到下次GC回收之前。弱引用的对象会在即将到来的垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。jdk1.2后,提供了WeakReference实现弱引用
虚引用:虚引用不会对对象的生命周期造成影响,也无法通过虚引用获取一个对象实例,简直就形同虚设一样的存在,为对象套上虚引用的目的就是为了跟踪对象被GC回收时的活动。jdk1.2后,提供了PhantomReference实现弱引用,用法和上图类似,就不放图了。
对象的死缓之finalize()方法:被finalize()方法覆盖的对象并且该对象没有被执行过finalize()方法,在GC进行可达性分析后这些对象会被筛选出来,然后扔到一个F-Queue队列中,这时对象可以进行自我救赎才可以逃过一劫(与其他的引用连接上),然后GC将会对这个队列进行检查(这个检查就是去执行finalize()方法),如果这个队列中的对象还没有与其他的引用连接上那就彻底凉凉了,对象将会被GC视为垃圾进行回收
讲了那么多估计大家也看累了,不过这些只是开胃菜,接下来才是重点的内容!!
垃圾收集算法:
标记-清除算法:这个算法比较容易理解,记住它的名字就能猜到它要干什么了,它大体分为了两个步骤:1、标记:标记出所有需要进行回收的对象。2、清除:清除掉被标记的对象。这个算法虽然简单粗暴,但是在垃圾回收的过程中是需要暂停其他的线程工作的,标记和清除都得使用可达性分析算法,每次都需要从GC Root从上到下遍历分别进行标记和清除,效率却令人堪忧,两个步骤的效率都很低下,而且它还会造成空间碎片过多,以致后期无法给较大的对象分配足够的连续内存
复制算法:复制算法解决了效率和空间碎片问题,它将可用内存按容量按比例划分为两部分,一部分对象面,一部分空闲面,对象在对象面进行创建,当对象面的内存使用完后,就将还存活着的对象面复制到空闲面上,然后将已经所用过的对象面进行清理,这样每次都是其中的一面进行回收,不必考虑内存碎片问题,只要移动堆顶指针,按顺序分配内存即可,这种算法适用于对象存活率低的场景,年轻代的回收用的就是复制算法,但是对于生存期长的对象则会导致内存使用率低下,因为这种算法的代价是牺牲了一部分内存去实现复制回收,所以这个方法不适合垃圾对象少的场景,例如:老年代。在堆内存里表现为:eden、survivorTo、survivorFrom的复制与清除,每一次都有一个survivor空间是空闲面,所以缺点就是,内存使用率低下、
复制算法的应用:
标记-整理算法:又名标记-压缩算法,由于复制算法对老年代不适用,所以提出了标记-整理算法对应老年代的回收,标记-整理算和标记-清理算法类似,听到名字就知道它想要干嘛,它是先标记需要清除的对象,然后将存活对象进行整理即将存活对象一块一块的都往一端移动,然后直接清理掉边界以外的内存
分代收集算法:这个算法是jvm主要使用的收集算法,jvm根据对象的生存周期不同的特点将对象分为了新生代和老年代,而各个收集算法对于新生代和老年代会产生效率或内存使用率不一的结果,而根据对象生存周期的特点采取适合的算法就是分代收集算法的主要作用,在老年代中使用标记-整理法,在新生代中使用复制算法
垃圾收集器:
在学习垃圾收集器前我们先了解jvm运行模式:
Server:因为此模式的虚拟机是采用的重量级的虚拟机,所以启动速度慢,启动完成进入稳定期后程序速度会比Client模式的快
Client:因为此模式的虚拟机为轻量级的虚拟机,所以启动速度快,启动完成进入稳定期后程序速度会比Server模式的慢
查看我们jvm是哪种模式的可以执行java -version 进行查看:
新生代收集器:
Serial收集器:中文名是串行收集器,使用的是复制算法(新生代采用复制算法,老生代采用标志整理算法),通过设置-XX:+UseSerialGC来使用此收集器,这个一个历史悠久并且最稳定高效简单的收集器,但是由于这个收集器是一个单线程的收集器,它工作时其他的线程的工作就必须得停顿( “Stop The World” :将用户正常工作的线程全部暂停掉),虽然serial有诸多缺点,但是它仍然是虚拟机运行于Client模式下新生代的默认收集器(很神奇吧!),原因是Serial收集器简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率
ParNew收集器(Serial收集器的多线程版本-使用多条线程进行GC):这是Serial收集器的多线程版本,也是新生代收集器,通过设置-XX:+UseParNewGC来使用此收集器,使用的是复制算法,其特点和Serial完全一样,只是除了多线程收集之外,与Serial相比没有其他创新的地方,它是许多运行在Server模式下虚拟机的首选新生代收集器,除了Serial收集器意外,只有它能够与老年代收集器CMS配合工作,由于ParNew有线程交互开销,所它的单核收集效率比Serial收集器低
Parallel Scavenge收集器:这是一个新生代收集器,是多线程收集器,使用的是复制算法的,通过设置-XX:+UseParallelGC来使用此收集器。这个收集器更加关注系统的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),CMS垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。通过参数打开自适应调节策略。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这个叫做GC自适应的调节策略,使用Parallel Scavenge收集器可以配合虚拟机的自适应调节策略,这个收集器是运行在Server模式下的新生代默认收集器
老年代收集器:
Serial Old收集器:Serial收集器的老年代版本,使用的是标记-整理算法,通过设置-XX:+UseSerialOldGC来使用此收集器。这个收集器的特点和Serial几乎是一样的:单线程、工作时其他线程必须暂停工作、稳定简单高效、Client模式下老年代的默认收集器,它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,JDK1.6及之后用来代替老年代的Serial Old收集器,使用多线程和“标记-整理”算法。通过设置-XX:+UseParallelOldGC来使用此收集器。在Server模式,多CPU的情况下,如果注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
CMS收集器:CMS收集器是老年代收集器,使用的是标记-清除算法,它以获取最短回收停顿时间为目标的收集器,通过设置-XX:+UseConcMarkSweepGC来使用此收集器。目前很大一部分的java应用都集中在互联网或者B/S系统的服务器上,这类应用尤其重视响应速度,尽量缩短系统的停顿时间,以给用户带来较好的体验,它的工作原理是基于标记-清除算法实现的,整个过程是分为了初始标记,并发标记,重新标记,并发清除四个步骤
应用场景:
1、与用户交互较多的场景;(如常见WEB、B/S-浏览器/服务器模式系统的服务器上的应用)
2、希望系统停顿时间最短,注重服务的响应速度;
CMS的缺点:由于CMS是基于“标记+清除”算法来回收老年代对象的,因此长时间运行后会产生大量的空间碎片问题,可能导致新生代对象晋升到老生代失败。由于碎片过多,将会给大对象的分配带来麻烦。因此会出现这样的情况,老年代还有很多剩余的空间,但是找不到连续的空间来分配当前对象,这样不得不提前触发一次Full GC。解决办法是使用"-XX:+UseCMSCompactAtFullCollection"和" -XX:+CMSFullGCsBeforeCompaction",需要结合使用。CMS收集器提供−XX:+UseCMSCompactAlFullCollection标志,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
究极收集器:
G1收集器:G1的使命是在未来替换CMS,并且在JDK1.9已经成为默认的收集器,它可以做到并发并行(可以与用户线程并发执行以缩短系统停顿时间),它的使命就是替代jdk1.5中发布的CMS收集器,相比CMS收集器有以下特点:
1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为找不到足够大的连续内存空间而触发下一次GC
3、可预测停顿:降低停顿时间是CMS和G1共同的关注点,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
4、分代收集:在G1收集器中的内存物理结构已经不再是是jdk8时把新生代和老年代分别全部放在一大块内存内存中,而是把内存分成了一小块一块的Region,每块内存最大32M,整个堆内存最多有2048个Region,也就是最大堆内存是60G到70G,但是在概念上依然是保留了分代,收集垃圾也使用了分代收集的方式,清除垃圾的方式类似复制算法,但是比复制算法复制很多
上面提到的收集器的收集范围是整个新生代或者老年代,而G1不再是这样。G1收集器将java堆内存分为了多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔阂,它们都是其中的一部分(可以不连续)。G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始发出效果。和CMS类似,G1收集器收集老年代对象会有短暂停顿
G1如何找到引用对象?
前面的垃圾收集器都是使用了可达性算法找对象之间的引用,而由于G1的这种结构,各个Region全部在同一个区域,Region中的对象可能与其他的Region有引用关系,如何使用可达性算法找,是需要扫描整个堆区域的,这显然和G1作为各种收集器的升级版身份不符合,所以G1引入了一个叫做Remember Set的集合帮它记录Region的引用关系,每个Region都拥有自己的Remember Set,通过扫描Remember Set就能找到每个Region中对象的引用关系了
指定使用G1收集器:"-XX:+UseG1GC"
当整个Java堆的占用率达到参数值时,开始并发标记阶段,默认为45:"-XX:InitiatingHeapOccupancyPercent"
为G1设置暂停时间目标,默认值为200毫秒:"-XX:MaxGCPauseMillis"
设置每个Region大小,范围1MB到32MB,目标是在最小Java堆时可以拥有约2048个Region:"-XX:G1HeapRegionSize"
新生代最小值,默认值5%:"-XX:G1NewSizePercent"
新生代最大值,默认值60%:"-XX:G1MaxNewSizePercent"设置STW期间,并行GC线程数:"-XX:ParallelGCThreads"
设置并发标记阶段,并行执行的线程数:"-XX:ConcGCThreads"
为什么只有ParNew能与CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作,因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现,而其余几种收集器则共用了部分的框架代码;
下图是各个垃圾收集器能相互配合的选择了相应的老年代收集器,
例如:选择serial系统自动激活serial old, 选择parNew自动激活CMS,选择parallel scavenge自动激活parallel old