我刚工作的时候问一个前辈,我们能针对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高级特性与最佳实践

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

  1. JVM总括二-垃圾回收:GC Roots、回收算法、回收器

    JVM总括二-垃圾回收:GC Roots.回收算法.回收器 目录:JVM总括:目录 一.判断对象是否存活 为了判断对象是否存活引入GC Roots,如果一个对象与GC Roots没有直接或间接的引用关 ...

  2. JVM学习笔记——垃圾回收篇

    JVM学习笔记--垃圾回收篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分 我们会分为以下几部分进行介绍: 判断垃圾回收对象 垃圾回收算法 分代垃圾回收 垃圾回收器 ...

  3. JVM学习--(四)垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. stop the world 在介绍垃圾 ...

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

    简述 因为各个平台的虚拟机的垃圾收集器的实现各有不同,所以只介绍几个常见的垃圾收集算法. JVM中常见的垃圾收集算法有以下四种: 标记-清除算法(Mark-Sweep). 复制算法(Copying). ...

  5. JVM学习——G1垃圾回收器(学习过程)

    JVM学习--G1垃圾回收器 把这个跨时代的垃圾回收器的笔记独立出来. 新生代:适用复制算法 老年代:适用标记清除.标记整理算法 二娃本来看G1的时候觉得比较枯燥,但是后来总结完之后告诉我说,一定要慢 ...

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

    文章来源:https://zhuanlan.zhihu.com/p/25539690 Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我 ...

  7. 浅谈jvm中的垃圾回收策略

    下面小编就为大家带来一篇浅谈jvm中的垃圾回收策略.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧   java和C#中的内存的分配和释放都是由虚拟机自动管理的,此前我已 ...

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

    Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.st ...

  9. JVM——GC(垃圾回收)算法

    一.垃圾回收的基本概念 垃圾回收(GC,Garbage Collection),指内存中不会再被使用的对象清理掉. 垃圾回收有很多种算法:如引用计数法.标记压缩法.复制算法.分代/分区的思想 二.垃圾 ...

  10. JVM学习总结二——垃圾回收算法

    昨天总结了JVM内存分区相关的知识,这次我们将来了解下JVM的另一个核心知识点——垃圾回收算法.这一部分其实并不太难,如果对操作系统的内存处理算法有所了解,那么这部分算法其实只看名字就能明白,两者在原 ...

随机推荐

  1. rally使用tempest进行测试

    安装 通过Rally进行Tempest测试,执行如下命令创建tempest实例,Rally会自动同步tempest代码至本地: rally verify create-verifier --name ...

  2. numpy基本方法总结

    NumPy基本方法 一.数组方法 创建数组:arange()创建一维数组:array()创建一维或多维数组,其参数是类似于数组的对象,如列表等 读取数组元素:如a[0],a[0,0] 数组变形:如b= ...

  3. 部署k8s时容器中ping不通

    192.168.42.120 | UNREACHABLE! => {    "changed": false,     "msg": "Fail ...

  4. Python3 编程第一步_斐波纳契数列_连续赋值

    # Fibonacci series: 斐波纳契数列 # 两个元素的总和确定了下一个数 a, b = 0, 1 while b < 10: print(b) a, b = b, a+b # 1 ...

  5. koa2数据请求相关问题解决方案汇总

    前端请求后端数据,难免会遇到如下几个问题: 1⃣️跨域 2⃣️post/get,其中post请求的方式又分为多种 3⃣️后端数据返回格式(上一篇已经有讨论过,这里不再赘述) 用koa2的话,如何解决这 ...

  6. uni-app 使用本地打包配置安卓原生插件

    在使用 uni-app 开发的时候,遇到了一个很棘手的问题.即获取设备参数的时候 uni-app 并没有相关方法,而安卓开发是可以做到的,因为接的是三方推广,所以功能必须实现,所以求助了安卓的大佬帮我 ...

  7. 【ABAP系列】SAP MAC GUI750安装过程

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP MAC GUI750安装 ...

  8. deepin终端下载速度超慢解决方案

    最近发现在deepin终端下载软件包时速度简直是慢到不可饶恕,最快速度不上20KB/s,哭了,这要下载个几百KB的还能忍,稍微下载个百内MB的包就得等1-2小时,这咋行! 在网上查了几篇博客后,终于找 ...

  9. 【并行计算-CUDA开发】CUDA ---- Warp解析

    Warp 逻辑上,所有thread是并行的,但是,从硬件的角度来说,实际上并不是所有的thread能够在同一时刻执行,接下来我们将解释有关warp的一些本质. Warps and Thread Blo ...

  10. activeMQ(1)

    消息中间件 作用:解耦 削峰  异步 JMS编码总体架构: 一般代码流程 @Test public void test1(){ //创建连接工厂 ActiveMQConnectionFactory f ...