开篇介绍
大家好,我是Java最全面试题库
的提裤姐,今天这篇是JavaSE系列
的第十八篇,主要总结了JVM中的垃圾回收
,在后续,会沿着第一篇开篇的知识线路一直总结下去,做到日更!如果我能做到百日百更,希望你也可以跟着百日百刷,一百天养成一个好习惯。
GC是什么? 为什么要有 GC?
Java 提供的GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,
Java 语言没有提供释放已分配内存的显式操作方法。
在堆中,找到已经无用的对象,并把这些对象占用的空间收回使其可以重新利用。
要请求垃圾收集,可以调用下面的方法之一:
System.gc()
Runtime.getRuntime().gc()
。
算法思路:把所有的对象组成一个集合,或者可以理解为树状结构,从树根开始找,只要可以找到的都是活动对象,如果找不到就应该被回收了。
Java 的 GC 哪些内存需要回收?
内存运行时 JVM 会有一个运行时数据区来管理内存。它主要包括 5 大部分:
- 程序计数器(Program Counter Register)
- 虚拟机栈(VM Stack)
- 本地方法栈(Native Method Stack)
- 方法区(Method Area)
- 堆(Heap)
而其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构确定是哪个时就已知了,因此这 3 个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。
但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC 主要关注的是这部分内存。
总结:GC 主要进行回收的内存是 JVM 中的方法区和堆
介绍一些常见的垃圾回收器
Serial收集器
: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。ParNew收集器
: Serial收集器的多线程版本,也需要stop the world,复制算法。Parallel Scavenge收集器
: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。Serial Old收集器
: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。Parallel Old收集器
: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。CMS(Concurrent Mark Sweep) 收集器
: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。G1收集器
: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
CMS收集器和G1收集器的区别?
- CMS收集器是
老年代
的收集器,可以配合新生代的Serial和ParNew收集器一起使用 ,G1收集器收集范围是老年代和新生代
,不需要结合其他收集器使用 - CMS收集器以最小的停顿时间为目标的收集器 ,G1收集器可预测垃圾回收的停顿时间
- CMS收集器是使用“
标记-清除
”算法进行的垃圾回收,容易产生内存碎片,G1收集器使用的是“标记-整理
”算法,进行了空间整合,降低了内存空间碎片。
GC的收集方法有哪些?详细说一下每个的原理与特点?
标记-清除算法(Mark-Sweep)
从根节点开始标记所有可达对象,其余没有标记的即为垃圾对象,执行清除。但回收后的空间是不连续的。标记-清除算法采用从根集合进行扫描,对存活的对象标记,标记完毕后,在扫描整个空间中未被标记的对象,进行回收。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间进行对象的移动。也就是s0,s1等空间。
标记-整理法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时,在回收不存活的对象占用的空间后,会将所有的存活对象网左端空闲空间移动,并更新相应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
Java 的 GC 什么时候回收垃圾?
对于堆中的对象,用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。根据实际对引用的不同需求,分成了 4 种引用,每种引用的回收机制也是不同的。
对于方法区中的常量
和类
,当一个常量没有任何对象引用它,它就可以被回收了。
对于类
,如果可以判定它为无用类,就可以被回收了。
如何判断一个对象是否存活?
判断一个对象是否存活有两种方法:
1. 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象
时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说
明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象
B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回
收,所以主流的虚拟机都没有采用这种算法。
2.可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC
Roots 没有任何引用链相连时,则说明此对象不可用。
在 java 中可以作为 GC Roots 的对象有以下几种:
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。
当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记:
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记
并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记
,这时,该对象将被移除”即将回收”集合,等待回收。
什么是静态分派与动态分派?
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(根据参数的静态类型来定位目标方法)。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机执行的。
动态分派
在运行期根据实际类型确定方法执行版本。