聊聊 Spring 如何避免被JVM 垃圾回收!

1,310 阅读7分钟

SpringBoot ApplicationContext 会被 JVM 当成垃圾对象,然后回收掉吗?

最近五阳哥在复习JVM 垃圾回收的知识,被别人问到这个问题,我心里感到一惊,如果Spring 被回收掉,Spring管理的bean全部会被回收,那我们的Java应用不就被一锅端了吗? 这太可怕了……

虽然我相信 Spring一定会处理好这个问题,确保自身不被垃圾回收,但是巨大的好奇心,驱使我一探究竟。

我们在岁月静好时,Spring帮我们做了哪些事情?

回顾一下,垃圾回收的基础知识

精通Java GC的可以跳过这一段。

五阳哥,前两天写了一篇文章,分析为什么gc一定需要Stop the world? # 点击查看,这篇文章? ,里面已经总结道,“当一个对象无法被 GC Root引用到,那么这个对象将在接下来的垃圾回收过程中被回收”。

在Java堆中,存放着所有Java的对象实例。在进行垃圾收集之前,JVM需要确定哪些对象已经不被使用(即垃圾),哪些对象仍然被使用。为了判断对象是否是“垃圾”,JVM采用了可达性分析算法。

可达性分析算法 是指通过指定 GC Root 根对象,从根对象开始搜索引用的对象,通过引用链条,层层遍历链条上的对象,可以到达的对象不可被垃圾回收。而最终没有被搜索遍历到的对象,则为 不可达对象,应该被垃圾回收。

JVM中的 GC Root根对象包括如下:

  1. 虚拟机栈引用的对象
  2. 本地方法栈内JNI(本地方法)引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象
  5. Java虚拟机内部的引用

如果 Spring 想不被垃圾回收,那么Spring一定要确保自己被以上 GC Root引用,以上五个,任意一个即可。

接下来,我们将分析Spring 源码!找到Spring不被垃圾回收的奥秘!

启动Spring Boot应用

以下代码启动了一个Spring Boot应用,这是官方推荐的启动方式,通过注解的方式,把启动类传递给 SpringApplication ,调用run 方法,启动Spring Boot。

@RestController
@SpringBootApplication
public class MyApplication {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

需要说明的是,run方法在Spring boot启动成功后,会立即返回,不会被阻塞。所以 main 线程在启动Spring boot后,将退出……

由于Spring Boot会启动Jetty/Tomcat等其他线程池,所以Java应用并不会退出。因为Java进程退出条件之一是:所有非守护线程全部退出,则JVM退出 点击查看 JVM 如何退出的详细信息 ,所以只有 main 线程退出,其他业务线程还存在情况下,Java不会退出。

由此可见,main 线程在启动 Spring Boot后,并不会一直持有 Spring boot 对象引用,官方文档里也没有 强调,一定要保持 main 线程 不退出。Spring Boot需要把自己交到其他对象手中,确保自己不被回收!

那么如何保证 Spring Boot不被垃圾回收呢?我需要从 SpringApplcaition 内部找原因!

Spring Boot 和 Tomcat

从上面的代码可以看到 Spring Boot控制了Java应用的入口,而Web容器 Tomcat等被Spring 管理,如果Spring不会被垃圾回收,那么Tomcat就不用担心被垃圾回收。

而在Spring Boot之前的Web应用,都是将Java项目打包到Tomcat容器中执行。那时候 Spring MVC和Spring 是要被Tomcat容器管理的,所以那时的Spring项目不用担心 被垃圾回收的问题。

而现在 Tomcat和 Spring boot的角色互换,决定了 Spring Boot应用必须要处理好垃圾回收问题!

探究Spring Boot代码

创建和启动上下文

下图是 SpringApplication.run方法的源代码,run 方法主要执行两步,创建 Spring 上下文和启动上下文。 image.png

刷新上下文的奥秘

在Spring Boot刷新上下文的代码中,首先调用 Spring Application.refresh方法启动Spring 上下文。然后 Spring boot就把 Close 方法注册到 Java shutdownHook 关闭钩子程序中!

image.png 基本上可以破案了!

因为Spring Boot控制了Java程序的入口,所以要负责整个项目的关闭流程,于是它 注册了Java关闭钩子。

image.png

接下来,我们看一下注册关闭钩子,会被 GC Root引用到吗?

关闭钩子

add 方法,将钩子程序注册到 一个容器中!

image.png

可以看到 Thread 类型的 钩子程序,被保存在 hooks Map 中。

而hooks列表的类型定义是 static 类型的。 static 变量都是GC Root。

image.png

总结

Spring Boot 在启动时会将关闭流程注册到 Java 关闭钩子中,并通过关闭钩子线程引用到 Spring 上下文。

关闭钩子会被保存在一个 static 静态类型的 Map 中,这个 Map 在 GC Root 上。

因此,Spring Boot 不被垃圾回收的关键是在启动时注册了关闭钩子。

破案了,spring永远不会被垃圾回收。

我的开源项目

最后夹带一点私货,五阳最近花了3个月的时间完成一个开源项目。

开源3周以来,已有近 230 多个关注和Fork

Gitee:gitee.com/juejinwuyan…

GitHub github.com/juejin-wuya…

开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是

  • 功能堆砌,大部分功能用不上,需要大量裁剪;
  • 逻辑差异点较多,需要大量修改;
  • 功能之间耦合,难以独立替换某个功能。

由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显

所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?

  • 他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
  • 他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
  • 他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响

开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?

memberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!

image.png

具体介绍可参见

Gitee开源地址gitee.com/juejinwuyan…

GitHub开源地址 : github.com/juejin-wuya…

在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。

  1. Mybatis、Mybatis-plus 集成多数据源
  2. Sharding-jdbc 多数据源分库分表
  3. redis/redisson 缓存
  4. Apollo 分布式配置中心
  5. Spring Cloud 微服务全家桶
  6. RabbitMq 消息队列
  7. H2 内存数据库
  8. Swagger + Lombok + MapStruct

同时你也可以学习到以下组件的实现原理

  1. 流程引擎的实现原理
  2. 扩展点引擎实现原理
  3. 分布式重试组件实现原理
  4. 通用日志组件实现原理 参考:juejin.cn/post/740727…
  5. 商品库存实现原理: 参考:juejin.cn/post/731377…
  6. 分布式锁组件: 参考:
  7. Redis Lua的使用
  8. Spring 上下文工具类 参考: juejin.cn/post/746927…