Java中的垃圾回收机制
- 一、Java垃圾回收机制面试题
- 二、哪些对象应该被回收
- 三、什么时候进行垃圾回收
- 四、怎么清除垃圾对象
- 4.1 标记-清除算法
- 4.2 复制算法
- 4.3 整理算法
- 4.4 分代收集算法(主流)
- 4.4.1 新生代
- 4.4.2 老年代
- 4.4.3 永久代
- 五、垃圾收集器
- 六、注意
任何语言在运行过程中都会创建对象,也就意味着需要在内存中为这些对象在内存中分配空间,在这些对象失去使用的意义的时候,需要释放掉这些内容,保证内存能够提供给新的对象使用。对于对象内存的释放就是垃圾回收机制,也叫做GC。
对象的创建是由JVM完成的,在对象创建的时候JVM会在Java堆中开辟一块空间用来存储这个对象。而当对象“死亡”的时候,同样是由JVM来处理的,JVM处理“死亡”对象的过程就是我们今天要讲的垃圾回收机制。
一、Java垃圾回收机制面试题
- JVM怎么确定哪些对象应该进行回收
- JVM会在什么时候进行垃圾回收的动作
- JVM到底是怎么清除垃圾对象的
二、哪些对象应该被回收
垃圾收集器回收垃圾的第一步先要确定哪些对象是可以被回收的。因此,JVM会扫描堆内存中的所有对象,并标记出可被回收的对象。而垃圾收集的标记算法有以下两种:
1.引用计数算法
引用计数算法通过在每个对象中添加一个计数器,当有一个地方引用它的时候计数器的值就会增加1;当引用失效的时候计数器的值则会减1。当计数器的值为0时,则可认为这个对象已经不再使用。因此对于引用计数算法,垃圾收集器只需要回收计数器为0的对象即可。
引用计数算法的优点是效率很高,不需要遍历所有对象。但它是存在一个致命的缺点,即无法解决对象之间循环引用的问题。比如对象A引用了对象B,对象B也引用了对象A,除此之外,A、B两个对象再也没有被其他地方引用。此时对象A和对象B的计数器均不为0,所以A、B两个对象都无法被回收。所以,目前商用的Java虚拟机都没有选用引用计数算法来进行标记。
2.可达性分析算法(根搜索算法)
这一算法的基本思路是用一系列的“GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为”引用链“(Reference Chain)。如果一个对象到”GC Roots"没有任何的引用链相连,则证明此对象可能不再被使用。
哪些对象可以被作为GC Roots呢?主要包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 在方法区中引用的对象,如字符串常量池(String Table)里的引用
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象以及一些常驻的异常对象等。
- 所有同步锁持有的对象
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
三、什么时候进行垃圾回收
- 会在cpu空闲的时候自动进行回收
- 在堆内存存储满了之后
- 主动调用System.gc()后尝试进行回收
四、怎么清除垃圾对象
垃圾回收算法又有四个:
标记-清除算法,复制算法,标记-整理算法,分代收集算法(主流)。
4.1 标记-清除算法
这是最基础的一种算法,分为两个步骤,第一个步骤就是标记,也就是标记处所有需要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象。
这一算法很容易理解,实现起来也很便捷,但是有两个缺点:
- 效率问题:如果Java堆中包含大量需要回收的对象。此时需要进行大量标记和清除操作。导致标记和清除这两个过程需要大量的时间,降低了执行效率。
- 空间问题:标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成OOM。
4.2 复制算法
标记-复制算法也被简称为复制算法。它是对标记-清除算法的改进。复制算法将内存划分为大小相等的两块,分配对象时只使用其中的一块。当这块内存用完时,就将存活的对象复制到另外一块上面,然后把已使用的这块内存一次性清理掉。复制算法的执行过程如下图所示:
但这种算法在一种情况下会很高效:Java 对象的存活时间极短。据 IBM 研究,Java 对象高达 98% 是朝生夕死的,这也意味着每次 GC 可以回收大部分的内存,需要复制的数据量也很小,这样它的执行效率就会很高。
注意: 复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,浪费了一半的空间。
4.3 整理算法
标记整理算法与标记清除算法很相似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不做任何处理,造成内存碎片;而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片。
简单理解,标记整理法,知识在标记清除的基础上,追加了碎片的散落问题,在清除之后进行了碎片的整理,但副作用是增了了GC的时间。
4.4 分代收集算法(主流)
分代收集算法是一种比较智能的算法,也是现在jvm使用最多的一种算法,他本身其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收。
那么现在的重点就是分代收集算法中说的自动根据具体场景进行选择。这个具体场景到底是什么场景。
场景其实指的是针对jvm的哪一个区域,1.7之前jvm把内存分为三个区域:新生代,老年代,永久代。
JVM Heap 分代后的划分一般如下所示,新生代一般会分为 Eden、Survivor0、Survivor1区。老年区。
4.4.1 新生代
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。
新生代内存按照 8:1:1 的比例分为一个Eden区和两个Survivor0、Survivor1区。
- 大部分新生对象在Eden区中生成。
- 当Eden区满,触发 Young GC,进行垃圾回收,此时将Eden区存活对象复制到Survivor0区,然后清空Eden区,为后续新的对象分配内存。
- 当Survivor0区也满了时,则将Eden区和Survivor0区存活对象复制到Survivor1区,然后清空Eden区和Survivor0区。
- 此时Survivor0区是空的,然后交换survivor0区和survior1区的角色(即下次垃圾回收时会扫描Eden区和Survivor1区 ),即保持Survivor0区为空,如此往复。
- 当Survivor01区也不足以存放Eden区和Survivor0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,即新生代、老年代都进行回收。
- 注意新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。而新生代触发Young GC是当Eden区满了才触发。
4.4.2 老年代
老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。
此外,老年代的内存也比新生代大很多(大概比例是1:2),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的频率比较低。
4.4.3 永久代
永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用Hibernate、反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。持久代也称方法区。
五、垃圾收集器
这些垃圾收集器按照运行原理大概可以分为如下几类:
-
Serial GC,串行,单线程的收集器,运行 GC 时需要停止所有的用户线程,且只有一个 GC 线程。
-
Parallel GC,并行,多线程的收集器,是 Serial 的多线程版,运行时也需要停止所有用户线程,但同时运行多个 GC 线程,所以效率高一些
-
Concurrent GC,并发,多线程收集器,GC 分多阶段执行,部分阶段允许用户线程与 GC 线程同时运行,这也就是并发的意思。
(1)Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
(2)Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
(3)ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
(4)Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
(5)Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
(6)CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
六、注意
- 在jdk8的时候java废弃了永久代,但是并不意味着我们以上的结论失效,因为java提供了与永久代类似的叫做**“元空间”**的技术。
- 废弃永久代的原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。
- 元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。
垃圾收集器详解:https://blog.csdn.net/qq_20521573/article/details/108567229