JVM学习(二):垃圾回收

我刚工作的时候问一个前辈,我们能针对JVM做出什么样的优化。前辈说,我们系统现在的性能并不需要调优,用默认的配置就能满足现在的需求了。我又问,那你为什么要看JVM相关的书呢?前辈微微一笑,悠悠地来了句,为了面试。
玩笑归玩笑,不过事实上确实萌新程序员确实不需要在实际工作中进行JVM调优。一方面Java虚拟机的默认配置足够我们使用,另一方面功能强大的IDE让我们在编写代码的时候基本不需要考虑虚拟机的问题。那么抛开应付面试,我们为什么要学习JVM呢?
在我看来,学习无论从广度还是从深度上都应当超前于工作中的应用。就像科研一样,理论永远比实践走得要远(想想爱因斯坦1916年预测的引力波到2016年才被探测到)。这里不多说,增加知识储备肯定是好的。闲话少说,我们开始聊垃圾回收。

上一篇说到Java程序员不需要手动垃圾回收(GC),这是因为Java虚拟机已经帮我们自动完成了。那么为什么Java虚拟机知道哪个对象需要回收,何时需要回收,回收后如何重新分配呢?
对于程序计数器、虚拟机栈、本地方法栈这样的非线程共享的内存,它们的内存跟线程的生命周期相同,我们不用考虑这些内存的回收。此处垃圾回收指的是线程共享区域(堆和方法区)的内存回收。

哪些内存需要回收?

引用计数法

引用计数法是古老的辨别对象存亡的算法。引用计数法的方法是为每个对象添加一个引用计数器,统计引用对象的次数,如果引用次数为0,那么此对象则判断为死亡。
这样的判断方法有一个最大的弊端是,此种方法没有解决对象循环依赖的问题。例如对象A和对象B,两个相互引用。此时两个对象的引用计数器都不为0,那么这两个对象将永远不会被回收,这样就容易引起内存泄露问题。

可达性分析

Java虚拟机通常采用的是可达性分析算法,这个算法是将一系列GC Roots作为初始的存活对象集合。然后从这个集合出发将所有该集合引用到的对象加入到存活对象集合中。最终,未被加入到该集合的对象将被判定为死亡对象。如果你对判定算法感兴趣,可以参考我之前写的一篇文章如何判断一个图中是否存在环路

不过此方法也不是完美的解决办法。在多线程的环境下,假如一个在存活对象集合中的对象,在运行过程中其引用被删除,我们认为此对象应该被回收掉。但是可达性分析尚未完成,这时候就会出现漏报。漏报不会出现什么问题,因为下次可达性分析完成时就可以回收掉这个对象的内存。另一种情况是误报,一个对象没有在存活对象集合中,在被回收之前却被其他对象引用到了,这时候我们认为此对象是存活的。然而这时候Java虚拟机可能会回收掉这个对象,这就会引起很严重的问题。因此在合适的时机进行垃圾回收是很重要的。
GC Roots是指堆外指向堆内的引用,一般有如下几种:

  1. Java方法栈帧中的局部变量
  2. 已加载类的静态变量
  3. JNI handles
  4. 已启动尚未停止的Java线程

    何时进行垃圾回收?

    Stop-The-World

    为了避免上述漏报误报的问题,在Java虚拟机中,垃圾回收是在某个时刻进行的,在此期间不会出现引用关系的变化,这段时间也叫做 Stop-the-world。在Stop-the-world期间,将会停止其他非垃圾回收线程的工作,直到完成垃圾回收。因此垃圾回收造成了停顿时间(GC Pause)。

在Java虚拟机运行的过程中,程序的引用更新是很难预期到,因此Stop-the-world并不是有均匀间隔时间的,而是通过安全点(Safepoint)检测机制来实现的。安全点检测是为了找出Java虚拟机堆栈不会更新的稳定状态,当所有线程都达到安全点的时候,才会允许Stop-the-world线程进行垃圾回收。

线程的稳定状态一般有:JNI执行本地代码、解释执行字节码、执行即时编译器生成的机器码和线程阻塞。

Java Native方法不会去调用Java对象或者调用Java方法,因此Java虚拟机堆栈不会出现变化,属于安全点。
解释执行的字节码,字节码与字节码之间皆可作为安全点。
即时编译生成的机器码是直接运行在机器上的,这部分的运行不受虚拟机的控制。因此生成机器码的时候要插入安全点检测,避免长时间等待导致的停顿。HotSpot虚拟机的做法是在生成代码的方法出口和非计数循环的循环回边处加入安全点检测。
阻塞的线程还处于Java虚拟机的线程管理之中,因此是安全点。

垃圾回收的方式

清除

最简单粗暴的方式就是清除(Sweep)。也就是把未在存活集合中的对象内存全部回收,记录在一个空闲内存列表。当新建对象的时候就从空闲内里列表中划去所需要的内存。

由于Java堆中对象必须是连续的内存,因此这个方法有两个显而易见的缺点。一个是分配效率低下,每次分配内存时都要遍历空闲内存列表,找到符合大小的空闲内存块。第二个缺点是容易造成内存碎片,存在一种情况是内存空间足够,但是没有足够的连续内存为新的对象分配(这个可以参考操作系统中的内存管理,是相同的道理)。

压缩

压缩指的是对于存活对象的内存,移动到内存的起始点位置。可以想象每次回收的时候都需要移动内存,所以这种方法的性能开销很大。

复制

复制是把内存划分为两等分,分别用两个指针from和to维护。只用from指针指向的内存区域来分配内存。垃圾回收时,把存活的对象复制到to指向的内存区域中,然后交换from和to指向的内容。这种方法解决了内存碎片问题,缺点是堆空间的使用率大大降低(只能用一半)。

总结

本篇讲了垃圾回收的基本原理。先是判断何种对象要回收,主要有引用计数法和可达性分析法。其次,讲了什么时候进行垃圾回收,主要有Stop-the-world以及安全点检测。最后是垃圾回收的方式,分别是清除、压缩和复制。其中清除会产生内存碎片,压缩机制有较大的算法开销,复制机制能解决内存碎片,但是堆空间使用率低下。

参考文章

极客时间——郑雨迪:深入拆解Java虚拟机

深入理解Java虚拟机:JVM高级特性与最佳实践

原文地址:https://www.cnblogs.com/rever/p/11417595.html

时间: 2024-10-11 21:01:06

JVM学习(二):垃圾回收的相关文章

JVM学习记录-垃圾回收算法

简述 因为各个平台的虚拟机的垃圾收集器的实现各有不同,所以只介绍几个常见的垃圾收集算法. JVM中常见的垃圾收集算法有以下四种: 标记-清除算法(Mark-Sweep). 复制算法(Copying). 标记整理算法(Mark-Compact). 分代收集算法(Generational Collecting). 标记-清除算法 标记-清除算法是现代垃圾回收算法的思想基础,主要分为两个阶段:标记阶段和清除阶段.首先根据可达分析算法,标记处可以回收的对象,标记完成后,进行清除阶段,将标记为可回收的对象

Java性能优化之JVM GC(垃圾回收机制)

Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-the-world 的发生. JVM GC回收哪个区域内的垃圾? 需要注意的是,JV

JVM常见的垃圾回收算法

JVM常见的垃圾回收算法 1.标记-清除算法 标记清除算法也是最基础的算法,就如同他的名字一样,标记清除算法的步骤分为两个步骤,首先标记出需要回收的所有对象,然后在完成标记之后统一清除掉所有被标记的对象,之所以说它是最基础的算法,是因为后续算法都是基于此算法,改进其缺点衍生出来的,标记清除算法有两个缺点,标记和清除这两个步骤都会有性能损耗,且效率低,其次是在完成对象的清除后会留下不连续的内存碎片,算法如下图: 2.复制算法 为了提高效率,我们又引入了复制算法,将现有可用的内存大小均分为二,每次只

【转载】Java性能优化之JVM GC(垃圾回收机制)

章来源:https://zhuanlan.zhihu.com/p/25539690 Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-

JVM分代垃圾回收策略的基础概念

JVM分代垃圾回收策略的基础概念 由于不同对象的生命周期不一样,因此在JVM的垃圾回收策略中有分代这一策略.本文介绍了分代策略的目标,如何分代,以及垃圾回收的触发因素. 文章总结了JVM垃圾回收策略为什么要分代,如何分代,以及垃圾回收的触发因素. 为什么要分代 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的.因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率. 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的S

JVM学习系列(二) 垃圾回收

如何判断对象是否可回收 引用计数法 1.概念:给对象中添加一个引用计数器,每当有一个地方引用他时,计数器的值+1,当引用失效的时候,计数器-1,任何时刻计数器为0的对象就是不可以在被使用的对象. 2.缺点:无法解决对象循环引用的问题(如下图) 可达性分析法 1.概念:垃圾回收根节点(GCRoot)向下搜索,搜索所走过的路径称为引用链,当一个对象对GCRoot没有任何的引用链时,代表当前对象不可用. 2.GCRoot包含的对象: 虚拟机栈(帧栈中的本地变量表)中的引用的对象 方法区类静态属性 所引

JVM系列文章(二):垃圾回收机制

作为一个程序员,仅仅知道怎么用是远远不够的.起码,你需要知道为什么可以这么用,即我们所谓底层的东西. 那到底什么是底层呢?我觉得这不能一概而论.以我现在的知识水平而言:对于Web开发者,TCP/IP.HTTP等等协议可能就是底层:对于C.C++程序员,内存.指针等等可能就是底层的东西.那对于Java开发者,你的Java代码运行所在的JVM可能就是你所需要去了解.理解的东西. 我会在接下来的一段时间,和读者您一起去学习JVM,所有内容均参考自<深入理解Java虚拟机:JVM高级特性与最佳实践>(

【JVM】JVM系列之垃圾回收(二)

一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二.哪些内存需要进行垃圾回收 对于虚拟机中线程私有的区域,如程序计数器.虚拟机栈.本地方法栈都不需要进行垃圾回收,因为它们是自动进行的,随着线程的消亡而消亡,不需要我们去回收,比如栈的栈帧结构,当进入一个方法时,就会产生一个栈帧,栈帧大小也可以借助类信息确定,然后栈帧入栈,执行方法体,退出方法时,栈帧出

JVM系列之垃圾回收(二)

一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二.哪些内存需要进行垃圾回收 对于虚拟机中线程私有的区域,如程序计数器.虚拟机栈.本地方法栈都不需要进行垃圾回收,因为它们是自动进行的,随着线程的消亡而消亡,不需要我们去回收,比如栈的栈帧结构,当进入一个方法时,就会产生一个栈帧,栈帧大小也可以借助类信息确定,然后栈帧入栈,执行方法体,退出方法时,栈帧出