Dawn's Blogs

分享技术 记录成长

0%

请求

请求路径映射

通过 @RequestMapping 注解实现请求路径的映射。

请求参数

默认情况下 Controller 方法的参数名,就是请求参数的映射名字。

  • 也可以通过 @RequestParam(用于 URL 或者表单)和 @RequestBody(用于接收 json 数据)注解,指定请求参数。
  • 通过 @DateTimeFormat 注解,指定日期格式。
  • 通过 @PathVariable 注解,指定路径参数(@RequestMapping 中使用 {id} 指定路径参数)

响应

使用 @RequestBody 注解,设置 Controller 的返回值作为响应体。

  • 方法的返回值为字符串,会将其作为文本内容直接响应给前端。
  • 方法的返回值为对象,会将对象转换成 Json 文本响应给前端。

如果不适用 @RequestBody,返回的字符串代表 HTML 文件名,会返回对应的 HTML 文件。

介绍

SpringMVC 是一个轻量级的 MVC 框架,可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。

  • M(Model):数据层,也就是 dao 和 bean。
  • V(View):视图层,用于展示模型中的数据。
  • C(Controller):接受用户请求,并将请求发送至 Model,最后返回数据给请求方。

组件

SpringMVC 的核心组件包括:

  • DispatcherServlet:中央处理器,负责接收请求、分发,并给予客户端响应。
  • HandlerMapping:Handler 映射器,根据 URL 去匹配查找能处理的 Handler,并会将请求涉及到的拦截器和 Handler 一起封装。
  • HandlerAdapter:Handler 适配器,根据 HandlerMapping 找到的 Handler,适配器负责 Handler 的执行。
  • Handler:实际处理请求的组件。
  • ViewResolver:视图解析器,解析并渲染真正的视图,并传递给 DispatcherServlet 用于响应。

工作原理

SpringMVC 的工作流程如下:

  1. 客户端(浏览器)发送请求, DispatcherServlet 拦截请求
  2. DispatcherServlet 根据请求信息调用 HandlerMapping。HandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
  3. DispatcherServlet 调用 HandlerAdapter 适配器执行 Handler
  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给 DispatcherServlet,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
  5. ViewResolver 会根据逻辑 View 查找实际的 View
  6. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  7. 把 View 返回给请求者(浏览器)。

img

本文总结 Spring 用到的一些设计模式。

工厂模式

Spring 可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象,ApplicationContext 继承自 BeanFactory。

  • BeanFactory:延迟注入,所以占用更少的内存,程序启动速度更快。
  • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 bean 。并且 ApplicationContext 继承自 BeanFactory,扩展了功能。

ApplicationContext 有但不限于以下实现类:

  • ClassPathXmlApplicationContext:从 ClassPath 中加载 XML 配置文件,创建 ApplicationContext。
  • FileSystemXmlApplicationContext:从文件系统中加载 XML 配置文件,创建 ApplicationContext。
  • XmlWebApplicationContext:从 Web 系统中加载 XML 配置文件,创建 ApplicationContext。
  • AnnotationConfigApplicationContext:通过注解配置类,创建 ApplicationContext。

单例模式

bean 的作用域默认就是单例模式的,Spring 通过 ConcurrentHashMap 作为单例注册表来实现单例模式,key 为 bean name,value 为单例对象。

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
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

}
}
}

代理模式

Spring AOP 就是基于动态代理的,Spring AOP 根据被代理对象是否有实现接口,分别使用 JDK Proxy 和 CGLIB 实现动态代理。

模板方法

TransactionTemplate.execute 使用到了模板方法,实现事务。Spring 使用 Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。

以 Template 结尾的对数据库操作的类,都使用到了模板方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

try {

// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}

}
});
}

观察者模式

观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,依赖这个对象的所有对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

Spring 事件驱动模型

Spring 事件驱动模型有三种角色:

  • 事件:ApplicationEvent 是一个抽象类,继承自 java.util.EventObject 并实现了 java.io.Serializable 接口。
  • 事件监听者:ApplicationListener 是一个接口,定义了 onApplicationEvent 方法来处理事件。只要实现了 ApplicationListener 接口,就可以完成对事件的监听。
  • 事件发布者:ApplicationEventPublisher 作为事件的发布者,也是一个接口。这个接口在 AbstractApplicationContext 中实现。

流程

Spring 中完成对事件的订阅和发布,流程如下:

  1. 定义一个事件,继承自 ApplicationEvent。
  2. 定义一个事件监听者,实现 ApplicationListener 接口,重写 onApplicationEvent 方法。
  3. 使用事件发布者发布消息,通过 ApplicationEventPublisher 的 publishEvent 方法发布消息。
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
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;

private String message;

public DemoEvent(Object source,String message){
super(source);
this.message = message;
}

public String getMessage() {
return message;
}
}

// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}

}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

@Autowired
ApplicationContext applicationContext;

public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}

适配器模式

适配器模式将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。

Spring AOP 通知

Spring AOP:通知用到了适配器模式,与之相关的接口是 AdvisorAdapter

Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return 之前)等等。每个类型 Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptorAfterReturningAdviceInterceptorThrowsAdviceInterceptor 等等。

Spring 预定义的通知要通过对应的适配器,适配成 MethodInterceptor 接口(方法拦截器)类型的对象(如:MethodBeforeAdviceAdapter 通过调用 getInterceptor 方法,将 MethodBeforeAdvice 适配成 MethodBeforeAdviceInterceptor )。

Spring MVC

Spring MVC:SpringMVC 中有 HandlerAdapter,用于适配执行 Handler。

为什么 SpringMVC 需要适配器?

Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码。新增的 Controller 类型就违反了设计模式中的开闭原则。

1
2
3
4
5
6
7
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}

装饰器模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源,可以通过装饰器模式动态的切换不同的数据源。

Spring 中用到的装饰器在类名上含有 Wrapper 或者 Decorator。

事务的 ACID 特性中,只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的。

Spring 事务

事务管理方式

编程式事务管理

编程式事务管理,通过 TransactionTemplate 或者 TransactionManager 手动管理事务。

  • 使用 TransactionTemplate,调用 execute 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

try {

// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}

}
});
}
  • 使用 TransactionManager,调用 getTransaction 获取一个新的事务。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}

声明式事务管理

通过 @Transaction 注解,代码的侵入性最小,原理是 AOP。

事务管理接口

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • **PlatformTransactionManager**:(平台)事务管理器,Spring 事务策略的核心。
  • **TransactionDefinition**:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  • **TransactionStatus**:事务运行状态。

可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinitionTransactionStatus 这两个接口可以看作是事务的描述。

PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是 PlatformTransactionManager,通过这个接口 Spring 可以为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)等提供了对应的事务管理器。

img

PlatformTransactionManager 接口中定义了三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}

事务属性

事务属性 TransactionDefinition,定义了基本的事务属性,包括五个方面:

  • 隔离级别。
  • 传播行为。
  • 回滚规则。
  • 是否只读。
  • 事务超时。

TransactionDefinition 接口定义如下。

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
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();

@Nullable
String getName();
}

事务状态

TransactionStatus 用于描述事务状态,接口定义如下:

1
2
3
4
5
6
7
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}

事务属性详解

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量,Spring 也相应地定义了一个枚举类 Propagation。

传播行为如下:

  • PROPAGATION_REQUIRED:默认的传播行为,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • PROPAGATION_REQUIRES_NEW:不管外部是否有事务,修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • PROPAGATION_NESTED:在外部方法开启事务时的情况下,在内部开启一个新的事务,作为嵌套事务存在。如果外部方法无事务,则单独开启一个事务。
  • PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

以下事务传播行为不常用,导致事务不会发生回滚:

  • PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

隔离级别

隔离级别与数据库中定义的隔离级别一致(读未提交,读已提交,可重复读,穿行化),这里不再赘述。

事务超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为 -1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。

只读属性

MySQL 默认对每一个新建立的连接都启用了 autocommit 模式。在该模式下,每一个发送到 MySQL 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。对于更新操作,只读事务中会抛出异常。

如果给方法加上了 Transactional 注解的话,这个方法执行的所有 SQL 会被放在一个事务中。如果声明了只读事务的话,数据库或者 ORM 框架就会去优化它的执行。

例如 Oracle 对于只读事务,不启动回滚段,不记录回滚log。

事务回滚规则

定义了哪些异常会导致事务回滚而哪些不会,默认情况下,事务只有遇到运行异常以及 Error 才会回滚;遇到受检异常时,不会回滚。可以使用 rollbackFor 属性定义需要回滚的异常类型。

@Transaction

@Transaction 的作用范围:

方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。

:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。

接口:不推荐在接口上使用。

定义

@Transaction 的定义如下,常用配置有:

  • propagation:传播行为,默认为 REQUIRED。
  • isolation:隔离级别,默认为 DEFAULT。
  • timeout:事务超时事件,默认为 -1(不会超时)。
  • readOnly:是否是只读事务,默认为 false。
  • rollbackFor:用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
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
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

boolean readOnly() default false;

Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class<? extends Throwable>[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

}

原理

@Transaction 基于 AOP,AOP 又是基于动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。

如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

自调用问题

当一个方法被标记了 @Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。

这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,代理对象就无法拦截到这个内部调用,因此事务也就失效了。

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyService {

private void method1() {
((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2。
//......
}
@Transactional
public void method2() {
//......
}
}

AOP

面向切面编程也是一种设计思想,核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象;对于没有实现接口的对象,Spring AOP 会使用 CGLIB 生成一个被代理对象的子类来作为代理。

SpringAOPProcess

也可以使用 AspectJ,AspectJ 算的上是 Java 生态系统中最完整的 AOP 框架了。

Spring AOP 和 AspectJ AOP 的区别?

  • 实现原理不同:Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理,而 AspectJ 基于字节码操作。

  • Spring AOP 集成了 AspectJ AOP:Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

  • AspectJ 更快:如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

AOP 的应用场景:

  • 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
  • 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
  • 事务管理:@Transactional 注解基于 AOP 实现的,可以让 Spring 进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。
  • 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。
  • 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
  • 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
  • 。。。。。。

核心概念

AOP中的术语:

  • 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
  • 切面(Aspect):对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
  • 连接点(JoinPoint):连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
  • 通知(Advice):通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
  • 切点(Pointcut):一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如 execution(* com.xyz.service..*(..))匹配 com.xyz.service 包及其子包下的类或接口。
  • 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。

通知类型

Spring AOP 中定义的通知类型:

  • Before(前置通知):目标对象的方法调用之前触发。
  • After (后置通知):目标对象的方法调用之后触发。
  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发。
  • AfterThrowing(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
  • Around (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法

切面的执行顺序

多个切面的执行顺序如何控制,有两种方法:

  • @Order 注解,值越小优先级越高。
  • 通过实现 Ordered 接口,重新 getOrder 方法实现。

IOC

IOC(Inversion Of Control,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,被管理

  • 控制:指的是对象创建(实例化、管理)的权力。
  • 反转:控制权交给外部环境(Spring 框架中的 IOC 容器)。

IOC 的目的就是解耦,将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。也就是说,IOC 有两个好处:

  1. 对象之间的耦合性降低。
  2. 资源变的容易管理,由手动管理变为 Spring IOC 容器进行管理,比如可以很容易的通过 IOC 容器实现单例模式。

Bean

Bean 是指被 IOC 容器管理的 Java 对象。

声明 Bean

作用于类的注解

声明为 Bean 的注解:

  • @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用该注解。
  • @Repository:持久化层,用于数据库相关的操作。
  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层,返回数据给前端。

可以通过 @ComponentScan 注解定义要扫描的路径。

@Bean 注解

@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。使用示例如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}

}

@Bean 和 @Component 的区别如下:

  • @Component 作用于类,@Bean 作用于方法。
  • @Component 修饰类,表示这个类需要自动装配到 Spring 容器中。@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。
  • @Bean 注解比 @Component 的自定义性更强。比如当我们引用第三方库中的类需要装配到 Spring 容器中时,通过 @Bean 实现。

注入 Bean

用于注入 Bean 的注解:

  • @Autowired:来自于 org.springframework.bean.factory
  • @Resource:来自于 javax.annotation
  • @Inject:来自于 javax.inject

@Autowired 和 @Resource 的区别

@Autowired

@Autowired 是 Spring 内置的注解,默认注入方式为 byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

当接口存在多个实现类时,Spring 可以通过被注入的变量名称选择注入的 bean,变量名称等于首字母小写的实现类的名称。可以通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。

当一个接口存在多个实现类的话,byType 就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

@Resource

@Resource 是 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为 byType。@Resource 有两个属性,name 和 type。

1
2
3
4
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}

如果仅指定 name 属性则注入方式为 byName,如果仅指定 type 属性则注入方式为 byType,如果同时指定 name 和 type 属性(不建议这么做)则注入方式为 byType+byName。

Bean 的作用域

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

可以通过配置或者 @Scope 注解来声明 Bean 的作用域:@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Bean 是线程安全的吗?

取决于其作用域和状态

  • prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
  • singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。无状态则不存在线程安全问题(事实上,大部分 Bean 都是无状态的,如 Dao、Service)。

对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。
  2. 使用 ThreadLocal,将有状态的变量保存在 ThreadLocal 中。

Bean 的生命周期

Bean 的声明周期分为四步:

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入的对象、@Value 注入的值、setter 方法或构造函数注入依赖和值、@Resource 注入的各种资源。
  3. Bean 的初始化
    • 如果实现了 *.Aware 接口,调用相应的方法,如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessBeforeInitialization() 方法。
    • 如果 Bean 实现了 InitializingBean 接口,执行 afterPropertiesSet() 方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessAfterInitialization() 方法。
  4. 销毁 Bean:注册相关销毁回调接口,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
    • 如果 Bean 实现了 DisposableBean 接口,执行 destroy 方法。
    • 如果 Bean 在配置文件中的定义了 destory-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过 @PreDestory 注解标记 Bean 销毁之前执行的方法。

img

Spring 中的循环依赖

三级缓存

Spring 通过三级缓存来解决循环依赖问题,确保即使在循环依赖的情况下也能正确创建 Bean。Spring 中的三级缓存其实就是三个 Map,如下:

1
2
3
4
5
6
7
8
9
10
11
// 一级缓存
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

简单来说,Spring 的三级缓存包括:

  1. 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池。一般情况获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中 ObjectFactory 产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用 ObjectFactory#getObject() 都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories):存放 ObjectFactoryObjectFactorygetObject() 方法(最终调用的是 getEarlyBeanReference() 方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

Spring 创建 bean 的过程

Spring 创建 Bean 的流程:

  1. 先去一级缓存 singletonObjects 中获取,存在就返回;
  2. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  3. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory 方法,向三级缓存中添加一个 ObjectFactory 对象:

1
2
3
4
5
6
7
8
9
// AbstractAutowireCapableBeanFactory # doCreateBean #
public abstract class AbstractAutowireCapableBeanFactory ... {
protected Object doCreateBean(...) {
//..

// 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
}

如何解决循环依赖

如 A 和 B 发生了循环依赖,则会发生如下:

  • 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
  • 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
  • 那么此时就去三级缓存中调用 getObject() 方法去获取 A 的 前期暴露的对象 ,也就是调用 getEarlyBeanReference() 方法,生成一个 A 的 前期暴露对象
  • 然后就将这个 ObjectFactory 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。

只有两级缓存够吗

在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。

有 AOP 的流程:

  1. 生成一个 A 原始对象,A 对象在三级缓存中注册 ObjectFactory(一个回调函数),用于创建 A 的代理对象。
  2. A 引用 B 对象,B 又引用 A 对象。B 回调 ObjectFactory 创建 A 的代理对象,B 引用 A 的代理对象。
  3. B 将 A 的 ObjectFactory 移出三级缓存,将 A 的代理对象放入二级缓存
  4. A 填充 B 对象,完成属性填充。
  5. A 在 BeanPostProcessor 接口的 postProcessAfterInitialization 的方法中,A 尝试生成代理对象,首先检查二级缓存,若二级缓存中有代理则不会生成新的代理对象。

@Lazy

@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。

1
2
#默认false
spring.main.lazy-initialization=true

如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。

如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。

如何解决循环依赖

如 A 和 B 发生了循环依赖,A 的构造器上添加 @Lazy 注解后(延迟 Bean B 的实例化),加载的流程如下:

  • 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;

  • 由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;

    之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。

关键点就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖。

Spring 介绍

Spring 是一款开源 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 指的是 Spring Framework,包含很多模块,这些模块可以协助开发。如 Spring 支持 IOC 和 AOP,可以很方便地对数据库进行访问、可以很方便地集成第三方组件电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。

Spring 模块

Spring 1 仅仅支持配置文件,Spring 2 开始引入注解,Spring 3 引入纯注解模式(Spring 配置类代替配置文件)。

Spring 4 的模块架构图:

Spring4.x主要模块

Spring 5 的模块架构图:

Spring5.x主要模块

Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

从模块架构图中可以看到,Spring 的模块组织如下:

  • Core Container(核心容器):提供了对 IOC 控制反转的支持,即对象管理的支持,是 Spring 的基础模块。
  • AOP 和 Aspects:AOP 为面向切面的编程,Aspects 提供对 AOP 思想的实现。
  • Data Access/Integration:提供数据访问,数据集成的功能。
  • Web:Web 开发。
  • Test:单元测试与集成测试。
阅读全文 »

Java 类在加载时,完成以下事情:

  1. 通过全类名读取获取二进制字节流。
  2. 将字节流所代表的静态结构转为方法区的运行时数据结构。
  3. 生成一个该类对应的 Class 对象。

类加载器

ClassLoader 类加载器的主要作用就是加载 Java 类的字节码(.class 文件)到 JVM 中,在内存中生成一个代表该类的 Class 对象。除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。
  • 数组不是通过 ClassLoader 创建的,是由 JVM 直接生成的。

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

1
2
3
4
5
6
7
8
9
10
11
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}

类加载器类型

JVM 中内置了三个重要的 ClassLoader

  • BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  • **ExtensionClassLoader(扩展类加载器)**:主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • **AppClassLoader(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。

获取父类加载器

除了 BootstrapClassLoader 外,其他所有的类加载器都继承自抽象类 ClassLoader。类加载中 BootstrapClassLoader 时 ExtensionClassLoader 的父加载器,ExtensionClassLoader 是 AppClassLoader 的父加载器,而 AppClassLoader 是用户自定义加载器的父加载器。

通过组合而不是继承实现加载器之间的父子关系。

在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。(合成复用原则)

所以,加载器之间都是“父子关系”,类加载器中有一个 private final ClassLoader parent 属性,指向这个类加载器的父加载器,可以通过 getParent() 方法获取。如果获取到 parent 为 null 的话,那么父 ClassLoader 为 BootstrapClassLoader。

为什么 获取到 ClassLoadernull就是 BootstrapClassLoader 加载的呢? 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

ClassLoader 类有两个关键的方法:

  • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
  • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

双亲委派模型

双亲委派模型简而言之就是:

  1. 除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
  2. 在加载类是,首先委托父类加载器进行加载,父类如果发现已经被加载则直接返回,没有加载则进行加载(自底向上查找类是否被加载)。
  3. 当父类无法加载时,子类进行加载(自顶向下尝试加载类)。

类加载器层次关系图

执行流程

双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

双亲委派模型的实现代码都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}

if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);

//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}

好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

打破双亲委派模型

自定义类加载器,需要继承 ClassLoader,如果我们不想打破双亲委派模型,就重写 findClass 方法。如果想打破双亲委派模型,则重写 loadClass 方法。

上下文类加载器

单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。

比如,SPI 中,SPI 的接口(如 java.sql.Driver)是由 Java 核心库提供的,由BootstrapClassLoader 加载。而 SPI 的实现(如com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader)也会用来加载 SPI 的实现。按照双亲委派模型,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。

再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,所以 SharedClassLoader 无法找到业务类,也就无法加载它们。

如何解决这个问题呢? 这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader 了。

拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。这样就可以让高层的类加载器(SharedClassLoader)借助子类加载器( WebAppClassLoader)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。

线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。

在 Java 7 以及之前,堆分为三个区域新生代(Eden、S0、S1)、老生代(Tenured)、永久代;但是从 Java 8 开始,永久代已经被元空间所代替,元空间使用的是直接内存。

垃圾回收

空间回收原则

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

  • 部分收集 (Partial GC):

    • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
    • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
  • 整堆收集 (Full GC):收集整个 Java 堆和方法区。

JVM 内存分配与空间回收有以下几个原则:

  • 对象优先在 Eden 区分配。
  • 大对象直接进入老年代。
  • 长期存活的对象进入老年代。
  • 空间分配担保。

对象优先在 Eden 分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,然后在 Eden 区分配。

大对象直接进入老年代

大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本

  • G1 垃圾回收器根据 -XX:G1HeapRegionSize 参数设置的堆区域大小和 -XX:G1MixedGCLiveThresholdPercent 参数设置的阈值,来决定哪些对象会直接进入老年代。
  • Parallel Scavenge 垃圾回收器中,默认情况下,并没有一个固定的阈值来决定何时直接在老年代分配大对象。而是由虚拟机根据当前的堆内存情况和历史数据动态决定。

长期存活的对象进入老年代

虚拟机给每一个对象一个年龄计数器,作为对象存活的年龄。大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1

对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加到某个年龄时,所累加的大小超过了 Survivor 区的一半,则取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。

空间分配担保

空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

死亡对象判别

死亡对象判别常用方法有:应用计数法和可达性分析。

如何判断废弃常量?

如果没有任何一个 String 对象引用一个字符串常量,那么这个字符串常量就是废弃的。

如何判断无用的类?

判断无用的类,需要满足三个条件(虚拟机可以对满足上述 3 个条件的无用类进行回收,仅仅是可以,不是一定):

  1. 该类的所有实例被回收。
  2. 加载该类的 ClassLoader 被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

引用计数法

引用计数法:每一个对象有一个引用计数器,这个方法实现简单,效率高,但是没有办法解决循环引用问题。

可达性分析

可达性分析:GC ROOT 出发,找到所有可达对象;不可达对象就是需要被回收的。

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象

引用类型

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。

  1. 强引用(StrongReference):类似于必不可少的生活用品,平时的引用就是强引用。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会回收具有强引用的对象来解决内存不足问题。
  2. 软引用(SoftReference):类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
  3. 弱引用(WeekReference):类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  4. 虚引用(PhantomReference):虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动

虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

垃圾清扫算法

标记-清除

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题:标记和清除两个过程效率都不高。
  2. 空间问题:标记清除后会产生大量不连续的内存碎片

复制

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。虽然改进了标记-清除算法,但依然存在下面这些问题:

  • 可用内存变小:可用内存缩小为原来的一半。
  • 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。

标记-整理(标记-压缩)

标记-整理(压缩)算法是根据老年代的特点提出的一种算法,在回收时,让所有存活的对象向一端移动

由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。

分代收集

当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块,不同代采用不同的算法。一般将 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率比较高,而且没有额外的空间对它进行分配担保,所以必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

垃圾收集器

JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):

  • JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • JDK 9 ~ JDK20: G1

Serial/Serial Old 收集器

Serial 负责新生代,Serial Old 负责老年代。二者都是单线程收集器,在垃圾回收的过程中需要 STW,直到垃圾收集结束。新生代采用标记-复制算法,老年代采用标记-整理算法。

优点:它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

Serial 收集器

ParNew 收集器

ParNew 是 Serial 收集器的多线程版本。新生代采用标记-复制算法,老年代采用标记-整理算法。

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。

ParNew 收集器

Parallel Scavenge/Parallel Old 收集器

Parallel Scavenge 收集器是负责年轻代的垃圾回收,也是使用标记-复制算法的多线程收集器。Parallel Scavenge 收集器关注点是吞吐量,CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。使用多线程和标记-整理算法。

Parallel Old收集器运行示意图

CMS 收集器

CMS 主要关注老年代的对象收集,ParNew 作为年轻代的对象收集配合使用。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS 收集器是一种标记-清除算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
  • 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

CMS 在并行标记阶段使用三色标记法+插入和删除写屏障,保证黑色节点不会指向白色节点。

主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),在 JDK 14 被移除。

CMS 收集器

G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

它具备以下特点:

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1 收集器

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率。

从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。

ZGC 收集器

ZGC 在 Java 11 引入,处于实验阶段;在 Java15 已经可以正式使用了;在 Java 21 中引入分代 ZGC。