零散知识点

实战

Posted by Ekko on November 6, 2025

[TOC]


问题一:为什么越来越多人喜欢用构造器注入

1、不可变性 (Immutability)

构造器注入强制依赖项为 final,确保对象不可变

清晰的契约:对象创建时所有依赖必须就位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;

    // 构造器注入 - 所有依赖都是 final
    public UserService(UserRepository userRepository, 
                      PasswordEncoder passwordEncoder,
                      EmailService emailService) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.emailService = emailService;
    }
    
    // 业务方法...
}

2、依赖完整性保证

构造器注入确保对象在创建时所有依赖都可用

Setter注入,有一些场景下注入失败,运行的时候才会NPE异常

  1. 缺少 @Component
  2. 缺少 @Autowired
  3. 复杂依赖关系
  4. @PostConstruct 中使用

3、避免循环依赖问题

构造器注入能更早地暴露循环依赖问题

启动失败:BeanCurrentlyInCreationException


主线程等待子线程完成

CountDownLatch

Future


对象的创建过程

1
2
3
4
5
6
7
8
9
10
11
类加载检查
↓ (加载、验证、准备、解析、初始化)
内存分配
↓
初始化零值(默认值)
↓
设置对象头(MarkWord、类型指针)
↓
执行构造方法
↓
引用关联

Http Referer

Referer 是HTTP请求头中的一个字段,表示当前请求是从哪个页面链接过来的

Referer 可以被伪造


SimpleDateFormat 线程不安全

1
2
3
4
5
6
7
public class SimpleDateFormat extends DateFormat {

}

public abstract class DateFormat extends Format {
    protected Calendar calendar;
}

Calendar 对象用于日期计算,这个对象的状态会在每次格式化和解析时被修改

解决方案:

  1. jdk1.8 DateTimeFormatter

  2. 每次使用的时候,new 新的 SimpleDateFormat 对象 (不推荐)


为什么 ConcurrentHashMap 不允许key为null

value 也不允许为null,HashTable 线程安全也是一样逻辑

在并发环境中,存在歧义

1、key null 不存在 map 中

2、key null 存在,但是 value 是null

2种场景下返回的都是 null

总结:ConcurrentHashMap 为并发而生的,内部处理了所有线程安全问题,但为了清晰和安全,全面禁止null


问题一:HashMap 允许null,不会有歧义吗

会有歧义

HashMap 是高性能的单线程工具,如果要在多线程使用,就要自己处理所有同步问题

追求单线程下的极致性能,线程安全是使用者的责任

这是2种不同的设计思路


Java 中对象一定在堆上分配吗

不一定,普遍做法是在 堆 上分配

逃逸优化,HotSpot 默认启用该优化

逃逸优化:对象仅在方法内部使用,未被外部引用或线程共享,可能进行优化

处理方式:

  1. 栈上分配,随方法结束自动销毁,无需GC介入
  2. 标量替换,将对象拆解成基本类型(标量),存储在栈帧或寄存器,避免对象整体分配

AOP什么时候失效

@Transactional @Async 都是代理实现

1、 内部方法调用失效

内部调用通过 this 直接调用目标对象的方法,绕过了代理

解决方案:使用 AopContext.currentProxy()调用,需要开启 exposeProxy 配置

2、 非Spring管理的对象

通过 new 关键字直接创建对象,而不是通过 Spring 容器获取


SpringBoot 自动装配原理

老项目,应该都写过 xml 吧,注入bean,依赖配置 bean 等等

SpringBoot 主要优化的就是这部分内容

@EnableAutoConfiguration -> 加载 Spring.factories -> 条件筛选(@Conditional) -> 动态注册Bean

启动类注解 @SpringBootApplication 包含了 @EnableAutoConfiguration

个人理解:先通过spi机制加载Spring.factories注册一些bean,使springboot具备自动装配的能力,再通过 @ComponentScan 扫描包路径等方式,加载其他的bean

flowchart TD
    A[启动类加载] --> B[SpringBootApplication]
    B --> C[EnableAutoConfiguration]
    C --> D[AutoConfigurationImportSelector]
    D --> E[spring.factories加载]
    E --> F[自动配置类筛选]
    F --> G[条件注解校验]
    G --> H[配置类实例化]
    H --> I[Bean注册到容器]

SPI 机制

服务发现机制,动态加载第三方实现,无需硬编码依赖。通过配置文件做到 接口与实现 的解耦

对扩展开放、对修改关闭

在 jar 包的 META-INF/services 目录下查找以接口全限定命名的文件

典型应用场景:

1、 JDBC 驱动

1
com.mysql.cj.jdbc.Driver

2、 日志桥接

1
ch.qos.logback.classic.servlet.LogbackServletContainerInitializer

3、 SpringBoot 自动配置

spring.factories 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

...
...

Spring 解决循环依赖

总结:三级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DefaultSingletonBeanRegistry {
    
    // 一级缓存:完整的单例Bean(已初始化完成)
    // 职责:提供最终可用的Bean实例
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 二级缓存:早期Bean引用(已创建但未初始化完成)
    // 职责:保证在创建过程中引用的一致性,避免重复创建
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
    
    // 三级缓存:Bean工厂
    // 职责:延迟创建早期引用,支持复杂的代理逻辑
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建 ServiceA 的流程
1. 创建 ServiceA 实例(构造函数调用)
2. 将 ServiceA 的 ObjectFactory 放入三级缓存
3. 开始属性注入,发现需要 ServiceB
4. 暂停 ServiceA 的创建,开始创建 ServiceB

// 创建 ServiceB 的流程  
5. 创建 ServiceB 实例(构造函数调用)
6. 将 ServiceB 的 ObjectFactory 放入三级缓存
7. 开始属性注入,发现需要 ServiceA
8. 从三级缓存中获取 ServiceA 的早期引用
9. ServiceB 完成属性注入和初始化
10. ServiceB 放入一级缓存

// 回到 ServiceA 的创建
11. 获取已完成的 ServiceB 实例
12. ServiceA 完成属性注入和初始化
13. ServiceA 放入一级缓存

从上面这个流程可以看出,循环依赖,其实只需要2层缓存对不对?那二级缓存是干嘛用的呢?


问题一:Spring循环依赖有3级缓存,二级缓存干嘛的

总结:避免重复创建代理对象

1、 C 需要 A 时,从三级缓存获取,调用 ObjectFactory 创建 A 的早期引用(可能是代理对象)

2、 如果后续还有其他 Bean 也需要 A 的早期引用,会重复调用 ObjectFactory

3、 对于需要 AOP 代理的 Bean,每次都会创建新的代理实例

问题就在这,多次创建新的代理对象,违反了单例原则。同一个类,代理对象不一致。二级缓存就是解决这个问题

3级缓存完整流程

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 以 A → B → C → A 的循环依赖为例,且A需要AOP代理

// 步骤1: 开始创建A
1. beforeSingletonCreation("serviceA") // 标记A正在创建
2. instance = createBeanInstance("serviceA") // 实例化A
3. addSingletonFactory("serviceA", () -> getEarlyBeanReference("serviceA", mbd, bean))
   // 三级缓存: {serviceA: ObjectFactory}
   // 二级缓存: {}
   // 一级缓存: {}

// 步骤2: A注入B,触发B的创建
4. populateBean("serviceA")  发现需要B
5. getSingleton("serviceB")  不存在开始创建B

// 步骤3: 开始创建B  
6. beforeSingletonCreation("serviceB")
7. instance = createBeanInstance("serviceB") 
8. addSingletonFactory("serviceB", ...)
   // 三级缓存: {
   // serviceA: ObjectFactory, 
   // serviceB: ObjectFactory
   // }

// 步骤4: B注入C,触发C的创建
9. populateBean("serviceB")  发现需要C
10. getSingleton("serviceC")  不存在开始创建C

// 步骤5: 开始创建C
11. beforeSingletonCreation("serviceC")
12. instance = createBeanInstance("serviceC")
13. addSingletonFactory("serviceC", ...)
    // 三级缓存: {
    // serviceA: ObjectFactory, 
    // serviceB: ObjectFactory, 
    // serviceC: ObjectFactory
    // }

// 步骤6: C注入A,触发获取A的早期引用
14. populateBean("serviceC")  发现需要A
15. getSingleton("serviceA") 
    - 一级缓存: 
    - 二级缓存:   
    - 三级缓存: 有ObjectFactory
16. singletonObject = singletonFactory.getObject() // 创建A的代理对象
17. earlySingletonObjects.put("serviceA", singletonObject) // 放入二级缓存
18. singletonFactories.remove("serviceA") // 移除三级缓存
    // 三级缓存: {
    // serviceB: ObjectFactory, 
    // serviceC: ObjectFactory
    // }

    // 二级缓存: {
    // serviceA: ProxyA
    // }

    // 一级缓存: {}

// 步骤7: C完成创建
19. initializeBean("serviceC")
20. addSingleton("serviceC", ...) // 放入一级缓存
    // 一级缓存: {
    // serviceC: CompleteC
    // }

// 步骤8: B完成创建  
21. B从一级缓存获取C完成注入
22. initializeBean("serviceB")
23. addSingleton("serviceB", ...)
    // 一级缓存: {
    // serviceB: CompleteB, 
    // serviceC: CompleteC
    // }

// 步骤9: A完成创建
24. A从一级缓存获取B完成属性注入
25. 执行initializeBean("serviceA") - 初始化回调(@PostConstruct等)
26. 在doCreateBean的最后阶段
    
    // 检查是否有早期暴露的引用
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        // 如果初始化后对象没有被增强,就使用早期暴露的代理对象
        if (exposedObject == bean) {  // exposedObject是初始化后的对象,bean是原始对象
            exposedObject = earlySingletonReference;  // 使用二级缓存中的代理对象
        }
    }
    
27. addSingleton("serviceA", exposedObject) // 将最终对象放入一级缓存
    // 同时清理缓存:
    // - 从 singletonFactories 移除 (三级缓存)
    // - 从 earlySingletonObjects 移除 (二级缓存)  
    // - 添加到 singletonObjects (一级缓存)

// 最终状态:
// 一级缓存: {
// serviceA: ProxyA, 
// serviceB: CompleteB, 
// serviceC: CompleteC
// }
// 二级缓存: {}
// 三级缓存: {}

看完这个过程,看起来没有问题,但有一个细节点?二级缓存解决的是 重复创建 的问题,但是,不经过二级缓存,直接放 一级缓存 是不是也合理?因为最终的实例bean,都在一级缓存,二级缓存看似有些多余啊


问题二:代理对象,为什么不能直接放一级缓存?

上面的依赖,是比较简单的依赖,所以有误解。如果多次依赖较复杂的情况

没有 二级缓存

1
2
3
4
5
6
7
// 创建流程:
1. A开始创建ObjectFactory放入三级缓存
2. B需要A  调用ObjectFactory创建代理Proxy1
3. C需要A  调用ObjectFactory创建代理Proxy2  
4. D需要A  调用ObjectFactory创建代理Proxy3
5. 最终B持有Proxy1C持有Proxy2D持有Proxy3
6. 三个代理对象不一致

有 二级缓存

1
2
3
4
5
6
// 创建流程:
1. A开始创建ObjectFactory放入三级缓存  
2. B需要A  调用ObjectFactory创建代理Proxy1  放入二级缓存
3. C需要A  从二级缓存直接获取Proxy1
4. D需要A  从二级缓存直接获取Proxy1
5. 最终BCD都持有同一个Proxy1

是不是就清晰了?一级缓存,是最终态的实例Bean

A还没有完成最终的实例化,但是中间很多其他类依赖,他们只能通过三级缓存,但是代理类三级缓存又会有重复创建的风险,所以需要一个中间态的缓存持有

从最终结果来看,一级缓存中的A对象,其实是从二级缓存晋升上来的


问题三:为什么三级缓存是 HashMap,其他两个是 ConcurrentHashMap ?

总结:三级缓存所有操作,都在 synchronized (this.singletonObjects) 代码块中执行,没有必要上 ConcurrentHashMap

一级和二级缓存因为可能在同步块外被访问(读取),所以需要线程安全的ConcurrentHashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 一级缓存
	Object singletonObject = this.singletonObjects.get(beanName);
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 二级缓存
		singletonObject = this.earlySingletonObjects.get(beanName);
		if (singletonObject == null && allowEarlyReference) {
            // 同步代码块
			synchronized (this.singletonObjects) {
                // 三级缓存
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            }
        }
    }
}

什么是 Spring

IOC、AOP


什么是 Spring MVC

DispatcherServlet、HandlerMapping、ModelAndView


什么是 SpringBoot

配置、内嵌 servlet 容器、jar包独立运行


什么是 SpringCloud

微服务框架集合,注册中心、网关、限流、负载均衡… …


服务器 机器参数配置

2核4G、4核8G

堆内存:不要超过物理内存的70%,为操作系统保留足够内存(约1-2G),用于系统进程、文件缓存等

新生代:占堆内存的1/3到1/2,确保对象有足够空间在年轻代被回收

元空间:根据应用使用的类库数量调整,Spring Boot应用通常需要较大元空间

1
2
3
4
5
6
7
8
9
10
# 在4G物理内存的服务器上:
总物理内存:4G
分配给JVM堆内存:-Xmx3g
留给操作系统的物理内存:4G - 3G = 1G(约25%)

# 这1G用于:
- 操作系统内核:~200-300MB
- 系统进程:~100-200MB  
- 文件缓存:剩余空间
- 网络缓冲区等

pie
    title 2核4G服务器内存分配
    "JVM堆内存" : 3072
    "操作系统内核" : 300
    "系统进程" : 200
    "文件缓存/缓冲区" : 428
pie
    title 4核8G服务器内存分配
    "JVM堆内存" : 6144
    "操作系统内核" : 400
    "系统进程" : 300
    "文件缓存/缓冲区" : 1156

Swap 空间和内存预留空间,有什么区别

总结:

  1. 预留的物理内存,是保证操作系统正常运行的”工作空间”,必须保留
  2. Swap 空间,使用磁盘空间来模拟内存,在物理内存不足时提供缓冲
  3. 如果发现系统在频繁使用Swap,正确的解决方案是优化应用内存使用或增加物理内存,而不是调整Swap配置
1
2
3
4
5
6
7
8
9
# 对于Java服务器,Swap配置:
- 物理内存 ≤ 4G:Swap = 2倍物理内存
- 物理内存 8G:Swap = 1倍物理内存  
- 物理内存 ≥ 16G:Swap = 0.5倍物理内存或禁用

# 查看和设置Swap
sudo swapon --show                    # 查看当前Swap
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048  # 创建2G Swap文件
sudo mkswap /swapfile && sudo swapon /swapfile      # 启用Swap
1
2
3
4
# 内存管理的基本流程:
物理内存不足 → 找出不活跃的内存页 → 写入Swap空间 → 释放物理内存

需要访问被换出的数据 → 从Swap读回内存 → (可能)把其他内存页换出

这个过程称为”页面交换”(Paging/Swapping):

换出(Swap Out):内存 → 磁盘

换入(Swap In):磁盘 → 内存