0%

note:本文基于 spring-framework 5.2.2.RELEASE

What is the real Spring IoC container? BeanFactory or ApplicationContext?

官网描述

Spring 官网 beans-introduction 描述中可以得到以下信息:

  • 源码工程中的 org.springframework.beansorg.springframework.context 包是 Spring Framework 的 IoC container 基础
  • BeanFactory 接口提供了一种高级配置机制能够管理任何类型的对象
  • ApplicationContextBeanFactory 的一个子类,它增加了以下功能:
    • 更容易和 Spring AOP 集成
    • 消息资源处理(用于国际化)
    • 事件发布
    • 应用层面特定上下文,例如用于 web 应用的 WebApplicationContext
  • 简而言之,BeanFactory 提供了配置框架和基础的功能,而 ApplicationContext 增加了更多企业级特性的功能。ApplicationContextBeanFactory 的一个完整超集

BeanFactory or ApplicationContext?

BeanFactoryApplicationContext 应该如何选择?

Spring 官网 BeanFactory or ApplicationContext 中也做了详细的描述。重点是这两句:

You should use an ApplicationContext unless you have a good reason for not doing so
(你应该使用 ApplicationContext,除非你有很好的理由不使用它)
Because an ApplicationContext includes all the functionality of a BeanFactory, it is generally recommended over a plain BeanFactory
(因为 ApplicationContext 包含了 BeanFactory 的所有功能,通常建议使用)

还附了一张关于 Feature Matrix(功能矩阵) 的表来描述 BeanFactoryApplicationContext 接口和实现所提供的功能:

特性 BeanFactory ApplicationContext
Bean 实例化/加工(wiring) Yes Yes
集成生命周期管理 No Yes
自动注册 BeanPostProcessor No Yes
自动注册 BeanFactoryPostProcessor No Yes
方便 MessageSource 访问(用于国际化) No Yes
内置 ApplicationEvent 发布机制 No Yes

可以看出,官方更推荐使用 ApplicationContext,但这并不代表 BeanFactory 就没有使用场景,只是场景真的比较有限,具体可参考官方文档描述。

实现原理

ApplicationContext Java Diagrams

ApplicationContext diagrams

从图中可以可以反映出:ApplicationContext 自身就是一个 BeanFactory 并且它还拥有其他功能。

那么,ApplicationContextBeanFactory 的一个完整超集这是如何实现的呢?

ApplicationContext internal BeanFactory

ApplicationContext 源码中,getAutowireCapableBeanFactory() 方法注释中有个重要的字眼:internal BeanFactory

这个字眼是否意味着,虽然 ApplicationContext 是一个 BeanFactory,但是其内部还有一个 BeanFactory?如果真是这样,是否多此一举呢?

顺着该方法注释提示去 @see ConfigurableApplicationContext#getBeanFactory() 源码,发现该方法注释第一句就解答了疑问:

Return the internal bean factory of this application context. Can be used to access specific functionality of the underlying factory.
(返回此应用程序上下文的内部 bean factory。可用于访问基础 factory 的特定功能)

How to set internal BeanFactory

这里就有一个新问题,这个 internal BeanFactory 是如何设置进去的?

按常理来讲,既然有 getBeanFactory() 方法,那应该也有一个 setBeanFactory() 方法用来把 beanFactory set 进去,但是很遗憾,源码中并没有 setBeanFactory() 方法。

继续查看 getBeanFactory() 的实现,发现有两个类实现了该方法:AbstractRefreshableApplicationContextGenericApplicationContext

继续看这两个类的源码会发现,这两个类里都有一个非常重要的内部属性:DefaultListableBeanFactory beanFactory,而 getBeanFactory() 方法返回的 internal BeanFactory 正是这个 beanFactory!

这种方式有点像代理,通过组合的方式把 BeanFactory 的实现 DefaultListableBeanFactory 组合进来,并不是完全去抽象或继承该类。

题外:其实这两个类都很重要,Spring 中”著名”的两个类都继承自它们:

  • ClassPathXmlApplicationContext extends … extends AbstractRefreshableApplicationContext
  • AnnotationConfigApplicationContext extends GenericApplicationContext

应用 internal BeanFactory

可以看下 Spring 里 AbstractApplicationContext 里的各个 getBean() 方法的实现,都是先调用 getBeanFactory() 再调用 getBean():

1
2
3
4
5
@Override
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}

可以看出,ApplicationContext 其实并不具备依赖查找的能力,都是委托给自己的 internal beanFactory 来实现。

同时也反映了,BeanFactory 其实是最底层的 IoC 容器,ApplicationContext 是在 BeanFactory 的基础上增加了一些特性。

Summary

综上,可以说,BeanFactory 含有的能力 ApplicationContext 都有,并且 ApplicationContext 提供了更多的特性。

再看官网这句话,就豁然开朗了: BeanFactory 提供了配置框架和基础的功能,而 ApplicationContext 增加了更多企业级特性的功能。ApplicationContextBeanFactory 的一个完整超集。


JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jstack (Stack Trace for Java): Java 堆栈跟踪工具

功能

生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 javacore 文件)。

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。

线程出现停顿时通过 jstack 来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

命令格式

1
jstack [ option ] vmid

option

选项 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用到本地方法的话,可以显示 C/C++ 的堆栈

执行样例

使用 jstack 查看线程堆栈

1
$ jstack 5511

统计线程数

1
$ jstack 5511 | grep 'java.lang.Thread.State' | wc -l

使用 jstack 定位进程中 cpu 占用最高的线程

  1. 查看 cpu 占用高线程
  • -H: 指示 top 命令显示单个线程。如果没有该选项,则显示每个进程中的线程总和

  • -p: 后跟进程 ID (process id),这里为 LVMID

1
$ top -H -p 5511

  1. 转换线程 ID
1
$ printf "%x\n" 6072

  1. 定位进程中 cpu 占用最高的线程
  • -A: 往下展示 x 行信息

  • -B: 往上展示 x 行信息

加上 -A 或 -B,可以打印出线程具体的堆栈信息

1
$ jstack 5511 | grep 17b8 -A 10

同理检测 cpu 高的原因

  • 使用 top 命令找到 cpu 占用最高的进程

  • 查看进程中 cpu 占用最高的线程(同上)寻找原因

reference: top

使用 jstack 检测死锁

  1. 死锁代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeathLockDemo {

private static final Lock LOCK_1 = new ReentrantLock(), LOCK_2 = new ReentrantLock();

public static void deathLock() {
Thread t1 = new Thread(() -> {
try {
LOCK_1.lock();
TimeUnit.SECONDS.sleep(1);
LOCK_2.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "death_lock_demo_thread1");
Thread t2 = new Thread(() -> {
try {
LOCK_2.lock();
TimeUnit.SECONDS.sleep(1);
LOCK_1.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "death_lock_demo_thread2");

t1.start();
t2.start();
}

public static void main(String[] args) {
deathLock();
}
}
  1. demo 运行后 jps 查询 LVMID 为 33391:
1
$ jps -l | grep DeathLockDemo
  1. 使用 jstack 检测死锁
1
$ jstack -l 33391

关键信息如下:



JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jhat (JVM Heap Analysis Tool): 虚拟机堆转储快照分析工具

功能

与 jmap 搭配使用,分析 jmap 生成的堆转储快照。

实现方式

jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。

使用情况

在实际工作中,多数人是不会直接使用 jhat 命令来分析堆转储快照文件的,主要原因是:

  • 一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为分析工作是一个耗时而且极为耗费硬件资源的过程(加载 dump 快照文件需要比生成 dump 更大的内存)。

    • (话说回来,既然都要在其他机器上进行,还有必要再受命令行工具的限制吗?)
  • jhat 的分析功能相对来说比较简陋,VisualVM 或 JProfiler 等工具也可以分析堆转储快照文件,而且能提供比 jhat 更强大专业的分析功能。

命令格式

1
jhat <filename>

使用 jhat 分析 dump 文件

arthas 官网提供的 arthas-demo.jar 为例。

  1. 在本地启动 arthas-demo.jar,并将日志输出到指定文件:
1
$ nohup java -jar arthas-demo.jar > demo.log &
  1. jps 查询 LVMID (进程 ID) 为 65034:
1
$ jps -l | grep arthas-demo.jar
  1. 生成 Java 堆转储快照。
1
$ jmap -dump:format=b,file=demo.bin 65034
  1. jhat 分析
1
$ jhat demo.bin

“Server is ready” 后,在浏览器中输入 http://localhost:7000/ 可以查看分析结果。

分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到上图中的 “Show heap histogram”(与 jmap-histo 功能一样)OQL 页签的功能(上图中的 “Execute Object Query Language (OQL) query”),前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似 SQL 的语法对内存中的对象进行查询统计。


JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jmap (Memory Map for Java): Java 内存映像工具

功能

  • 生成堆转储快照 (一般称为 heapdump 或 dump 文件)。

  • 查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。

获取 Java 堆转储快照的其他方法

- XX:+HeapDumpOnOutOfMemoryError

让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件。

-XX:+HeapDumpOnCtrlBreak

可以使用 [Ctrl] + [Break] 键让虚拟机生成堆转储快照文件。

Kill -3

发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。

Kill -3 使用示例

arthas 官网提供的 arthas-demo.jar 为例。

  1. 在本地启动 arthas-demo.jar,并将日志输出到指定文件:
1
$ nohup java -jar arthas-demo.jar > demo.log &
  1. jps 查询 LVMID (进程 ID) 为 54776:
1
$ jps -l | grep arthas-demo.jar
  1. 执行 Kill -3:
1
$ Kill -3 54776
  1. 查看 demo.log 文件内容

命令格式

1
jmap [ option ] vmid

options

选项 作用
-dump 生成 Java 堆转储快照。格式为:-dump:[live, ]format=b,file=<filename>,其中 live 子参数说明是否只 dump 出存活的对象
-finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效
-heap 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux/Solaris 平台下有效
-histo (histogram) 显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat(x) 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux/Solaris 平台下有效 (高版本已移除)
-F 当虚拟机进程对 -dump 选项没有响应时,可使用这个选项强制生成dump快照。只在 Linux/Solaris 平台下有效
-clstats 输出 ClassLoader 统计

执行样例

-dump

生成 Java 堆转储快照。格式为:-dump:[live, ]format=b,file=<filename>,其中 live 子参数说明是否只 dump 出存活的对象。

1
$ jmap -dump:format=b,file=resourceManager.bin 5511

-finalizerinfo

显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效。

1
$ jmap -finalizerinfo 5511

-heap

显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux/Solaris 平台下有效。

1
$ jmap -heap 5511

-histo

(histogram) 显示堆中对象统计信息,包括类(class name)、实例数量(instances)、合计容量(bytes)。

1
$ jmap -histo 5511

-clstats

输出 ClassLoader 统计。

1
$ jmap -clstats 5511

分析堆转储快照

堆转储快照直接看看不懂,如何来分析 jmap 生成的堆转储快照?

可以用 JVM 提供的另一款小工具 - jhat,但其实这个一般会比较少用,大多数情况是用 VisualVM 或 JProfiler 等工具,因为这些工具功能更强大。


JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jinfo (Configuration Info for Java): Java 配置信息工具

功能

实时查看和调整虚拟机各项参数:

  • 使用 jps -v 命令可以查看虚拟机启动时显式指定的参数列表,而使用 jinfo -flag 命令可以查询未被显式指定的参数的系统默认值。

    • 对于 JDK 6 及以上版本,还可以使用 java- XX:+PrintFlagsFinal 查看参数默认值(命令: $ java -XX:+PrintFlagsFinal -version)。
  • 使用 jinfo -sysprops 命令可以打印虚拟机进程的 System.getProperties() 的内容。JDK 6 之后,该命令除了提供信息查询功能,还加入了在运行期修改部分参数值的能力(使用 -flag[+/-]name 或 -flag name=value 在运行期修改一部分运行期可写的虚拟机参数值)。

命令格式

1
$ jinfo [ option ] pid

option

选项 作用
-flag <name> 打印虚拟机标记参数的值
-flag [+/-]<name> 开启或关闭虚拟机标记参数
-flag <name>=<value> 设置虚拟机标记参数
-flags 输出虚拟机参数
-sysprops 输出 Java 系统属性
<no option> 输出以上所有

执行样例

-flag <name>

查询 CMSInitiatingOccupancyFraction 参数值

1
$ jinfo -flag CMSInitiatingOccupancyFraction 5511

-flag [+/-]<name>

开启 / 关闭 PrintGC

1
2
3
4
5
$ jinfo -flag PrintGC 5511
$ jinfo -flag +PrintGC 5511
$ jinfo -flag PrintGC 5511
$ jinfo -flag -PrintGC 5511
$ jinfo -flag PrintGC 5511

-flag <name>=<value>

设置 HeapDumpPath

1
2
3
4
5
$ jinfo -flag HeapDumpPath 5511
$ jinfo -flag HeapDumpPath=/root/heapDump.hprof 5511
$ jinfo -flag HeapDumpPath 5511
$ jinfo -flag HeapDumpPath= 5511
$ jinfo -flag HeapDumpPath 5511

​但是不是每个参数都可以被动态修改的,比如:

1
jinfo -flag CMSInitiatingOccupancyFraction=1 5511

-flags

输出虚拟机参数

1
$ jinfo -flags

-sysprops

输出 Java 系统属性

1
$ jinfo -sysprops 5511

<no option>

输出以上所有

1
$ jinfo 5511