JavaGC垃圾收集机制

什么是GC

GC的全称是Garbage Collection,垃圾收集。垃圾收集机制的作用是清理虚拟机(JVM)内存,给Java程序的可持续运行提供了保障,假设没有GC,当你不断为新的对象申请空间,一旦内存不足,导致内存溢出,程序就会error并终止。GC会回收那些程序中用不到的对象和用不到的类,释放内存空间。

简述JVM的组成

JVM即Java虚拟机,是一个在Java环境下执行Java程序,分配和管理内存空间的虚拟机。每当开启一个Java进程,CPU就会为Java虚拟机分配一部分内存空间(CPU资源),Java虚拟机就可以对这部分内存空间进行管理,划分有不同功能的内存区域,大致可以分为这几部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器,在内存空间外还有执行引擎和连接本地方法库的本地库接口。
JVM示意图:
在这里插入图片描述
每个内存区域都有不同的作用:
1、方法区:方法区是各个线程共享的区域,其中主要存放的已被虚拟机加载的类的信息、常量(池)、静态变量、即时编译器编译后的代码等数据。其中的运行时常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后放在运行时常量池中存放,用的比较多的是String类的intern()方法。
2、堆:一般我们new的对象的实例都会保存在堆中,而关于对象的引用我们一般放在虚拟机栈中,关于对象实例的保存我们一般需要在堆中申请一段连续的空间来存储。堆是GC管理的主要区域,故有时也被称为“GC堆”。
3、虚拟机栈:是线程私有的,其生命周期与线程相同,虚拟机栈执行的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程,局部变量表存放了编译期可知的各种数据类型、对象引用类型,里面存储的一般是关于对象的引用(指向对象起始地址的引用指针、指向一个代表对象的句柄或与对象相关的位置),我们可以通过引用在堆中找到完整的对象,从而可以实现对象的实例化、修改、回收等一系列操作。
4、本地方法栈:和虚拟机栈的作用非常类似,区别是本栈为程序中用到的Native方法服务,Native方法及本地方法,可以通过Native方法实现和其他语言(C语言,C++语言等)的通信。
5、程序计数器:我们在执行程序的过程中需要知道程序执行到了哪里,以实现对程序运行的控制。在多线程中,每个程序都会有一个程序计数器,每执行一条语句,程序计数器就会+1,这在多线程单CPU资源的情况下,协调多线程交替工作的重要方法,另外,这在实现程序的循环、调用等功能的实现中也非常重要。此区域是Java虚拟机规范中唯一一个没有规定OutOfMemoryError的区域。
6、直接内存:直接内存不是虚拟机运行时数据区域的一部分,也不是虚拟机规范中定义的内存区域,但它经常被用到,Java中的NIO类是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以避免在Java堆和Native堆来回复制数据。Native堆的内存包括在虚拟机总内存中。
下述GC机制的建立是基于JVM的内存组成而实现的。

判断对象已死

我们要进行GC操作,首先需要知道哪些对象是需要被回收的,才能进行后续操作,那么我们如何判断一个对象“已死”呢?
一、引用计数法:为每一个对象设置一个计数器,初始为0,对象成功被引用就+1,引用失效就-1,如果在任何时刻计数器都为0,则可判断对象已死。这种方法很容易理解也似乎可行,但其实它是有一定的弊端的,比如它无法解决对象之间的循环引用的问题,当两个对象循环引用,但都已经断开了和程序其他地方的引用,那么他们引用还是存在,不会被GC收集,但它们确实导致了内存空间的浪费,这也是一种严重的内存泄漏现象,如果这样的对象不断创建,会导致内存溢出的严重后果。
二、可达性算法:为了解决对象之间循环引用的问题,我们引入了可达性算法,可达性算法是通过把对象在栈中的引用作为GC Roots,通过GC Roots我们可以找到这个对象实例保存的地方,只要可以通过GC Roots找到的对象,我们都认为它们是存活的,在上述的循环引用的两个对象也可以被GC判断为不可达,即死亡了。那么,哪些对象可以作为GC Roots呢?虚拟机栈中的对象引用,方法区的静态属性,方法区的常量、本地方法区的JNI引用的对象。
可达性算法图示:
在这里插入图片描述
三、生存还是死亡:在上述GC Root引用外的对象并不会直接被“杀死”,而是处于一个“缓刑”的状态,一个对象被GC回收是需要被两次标记的,第一次可达性分析后在GC Roots外的对象,我们会初步标记它并执行finalize()方法,然后把它存入一个队列F-Queue,稍后GC会对它们进行第二次小规模的标记,如果在第二次标记前这个对象还没有被别处引用,那么它就会被回收。也就是说,如果在这期间,对象被引用了,那么它还有存活的机会。
四、再谈引用:Java中对象的引用大致可以分为四种:1、强引用:这种引用在建立后一般就会一直存在,即对象在任何时刻都是存活的。2、软引用:被软引用连接的对象在系统发生内存溢出的时候,会被下一次GC清除。3、弱引用:这种引用对象的存活时间很短,只能存活到下一次GC到来之前。4、虚引用:这种引用对对象的存活时间没有影响,它的唯一作用是在它被回收时系统会收到一条通知。

几种垃圾回收算法

我们在知道哪些对象需要回收后,自然要选择一种合适的算法去回收这些对象。以下介绍几种常用的算法。
一、标记-清除算法:
我们在判断回收的过程中把需要回收的对象做一个标记,然后在GC的时候清除这些对象。这种方法非常容易理解,操作也很方便,但是它存在了一些问题:首先是标记和清除两个步骤效率不高,其次在清除对象的过程中会导致内存空间的碎片化。可以想象一下,如果在这里删除一个对象,在那里删除一个对象,会导致可用的空间零散分布,那么当我们要为一个大对象申请实例化空间的时候会发现没有可用的空间。空间的碎片化也使给垃圾清除的过程带来了一些困难。所以,这是最基础的方法,一般我们都不会采用这种GC策略,而是在这种方法的基础上,延伸出一些别的方法。
标记-清除算法图示:
在这里插入图片描述
二、复制算法:
为了提高效率和避免内存空间的碎片化,我们可以尝试使用复制的思想,把内存空间分为相等的两部分,每次只使用其中的一部分,执行一次GC的时候,我们把当前存储的内存区域中的存活对象复制到另一块内存区域,然后清除之前的内存区域,这样在复制后得到的就是一块连续的内存区域,且效率比较高。但这种方法也存在了一些问题,这种方法需要浪费一半的内存空间,在JVM中,内存空间是很宝贵的资源。
复制算法图示:
在这里插入图片描述
三、标记-整理算法:
这种算法的优点是不需要每次都执行大规模的复制,只需要对内存做简单地整理,我们来看一下它的实现。标记过程和标记-清除算法相同,只是后续步骤不同。在这种方法里,存活的对象会向一侧移动,这样一来,死亡的对象就会逐渐出现在内存区域的边缘,这样,我们只要一次性清除边缘区域的对象就可以了。这种方法的优点是,我们可以获得连续的新的存储空间,也能实现对已死对象的清除。缺点依然存在,它对于对象的区分度还不是很高,对于能够长期存在的对象怎么处理,对于不断产生和消亡的对象怎么处理?我们还是需要更加严密划分,以实现更加高效的目标。
标记-整理算法图示:
在这里插入图片描述
四、分代收集算法:
把内存区域分为新生代和老年代,新生带又分为Eden区和Survior区,Survior区分为From Space区和To Space区。新生代是指那些需要频繁创建对象以及回收对象的区域,老年区存储的是那些可以存活很久的对象。我们创建一个新的对象一般存储在Eden区,当Eden区存满后,会赋复制到From Space区再清除原先的Eden区,最终From Space区会满,再以同样的方式存到To Space区,那么To Space区满了怎么办呢?我们需要老年代为其提供担保,使得To Space区的对象存到老年代。当老年代可能不够存储新的对象了怎么办,这时会执行一次Full GC,我们也称它为“Stop The Word”,此时会暂停所有线程,对所有内存空间执行垃圾清除。而我们上面提到的Eden存满后执行的GC是Minor GC,执行复制算法。因为新生代对象变动很多,所以我们在新生代使用的是复制算法;老年代对象变化较少,所有在老年代使用的是标记-清除算法或者标记-整理算法。分代收集算法的优点显而易见,我们可以对不同存活时间的对象采用不同的GC方法,提高了GC的效率。这也目前被广泛采用的一种算法。

HotSpot的算法实现

HotSpot是一种JVM实现技术。我们上面介绍了回收哪些对象,怎么回收对象的方法,在这一部分我们介绍回收对象的时机,即什么时候回收?我们可以假设一下,如果GC和线程在同时运行,并且在线程的运行中使用到了对象或者修改了对象的引用,而GC又回收对象,此时程序就会出现问题。

  • 安全点:要想令程序正常运行,GC必然不能在线程运行时执行,这时我们需要停住所有线程,再进行GC,这样就可以防止出错。停止所有线程的思路是在所有方法返回前或者无界循环方法回跳前查询一个标志位,标志位决定线程是否中断挂起并执行GC。一旦需要GC,则置位标志位,线程执行到这里就会停止等待标志位恢复,在所有线程都停住后,就可以执行GC,这个点就是安全点,在结束GC之后再恢复线程。这种安全点GC的方法也有一些问题,如果此过程过长,会使得进程出现卡顿现象,这会对用户带来很不好的体验;另外,如果在这个时刻有些线程在等待或者阻塞中呢,它们不会进入安全点,这也是一个需要解决的问题。
  • 安全区域:安全区域是指在线程代码在执行时引用关系不会改变的区域,可以在这区域中开始GC,此时可以不暂停线程,只要在这个区域的起始处设置标志位,告诉GC可以工作即可,在线程要离开安全区域时需要判断GC是否结束,如果没有结束就要等待GC结束再继续运行。这种方法的好处是可以高效利用线程的安全时间,在多线程并发的时候可以执行GC并减少卡顿的现象。
  • 枚举根节点:我们如何尽可能地减少GC的时间呢?如果每次GC都遍历所有内存单元,这必然会花费大量的时间,所以我们这里引入了OopMap数据结构,在程序初始化时,在OopMap中记录对象的偏移量,在程序运行过程中可以不断修改OopMap的值,在GC时只需要枚举根节点并通过偏移量就可以找到对象,这种算法大大缩短了GC时间。

几种垃圾收集器

用什么收集器来收集?
1、Serial收集器:这是一种单线程的垃圾收集器,只适用于单线程并且其效率很高,它GC的方法就是“Stop The Word”。
2、ParNew收集器:这种收集器实现了多线程的垃圾收集,新生代采用复制算法收集,老年代采用标记-整理算法收集。
3、Parallel Scavenge收集器:这种收集器的目标是达到一个可控制的吞吐量(吞吐量为CPU运行代码时间和总时间的比值)。它支持自适应调节,只要把基本内存参数配置好,再给虚拟机设立一个优化目标,具体调参虚拟机自己会完成。
4、CMS收集器:它的GC包括四个步骤:初始标记,并发标记,重新标记,并发清除。这种收集器停顿低,可并发收集,但对CPU资源敏感,无法处理浮动垃圾,会有大量垃圾碎片产生。
5、G1收集器:特点有:并行与并发、分代收集、空间整合、可预测的停顿。标记过程是:初始标记、并发标记、最终标记、筛选回收。它是当今收集器发展最前沿的成果之一。
ParNew收集器图示:
在这里插入图片描述
G1收集器图示:
在这里插入图片描述

内存分配与回收策略

JVM对内存的合理分配也是实现GC的重要一环,我们在分代收集算法中提到了新生代和老年代的概念,也介绍了新生代的分区Eden、Servior区。下面我们介绍几种内存分配和回收策略。
1、对象优先在Eden中分配:每次new一个新的对象会优先保存在Eden中。
2、大对象直接进入老年代:大对象需要较大且连续的存储空间,如果它在Eden区存储,会导致即便还有大量空间就提前触发GC。因此,超过一定阈值的对象就会直接存储在老年代。
3、长期存活的对象将进入老年代:我们为对象赋予一个年龄,对象刚开始分配在Eden中时为0,之后每经历一次GC年龄就会+1,当年龄达到15时,就会晋升到老年代。
4、动态对象年龄判定:相同年龄所有对象内存大小的总和超过Survior空间的一半,他们会被存入老年代。
5、空间分配担保:老年代为新生代提供担保。在发送Minor GC前,会先检查一下老年代的最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,则GC安全,如果不成立,虚拟机会查看是否允许担保失败,如果允许,则再看老年代最大可用连续空间是否大于历次晋升老年代的对象的平均大小,如果大于,则尝试着进行一次GC,如果小于则不允许冒险,进行一次Full GC。
6、Full GC和Minor GC:一般Eden溢出会触发Minor GC,如果老年代溢出,则会导致Full GC,此时“Stop The Word”。

猜你喜欢

转载自blog.csdn.net/mayifan_blog/article/details/85094187
今日推荐