Moralok's Blog

你在幼年时当快乐

SPI 作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI 被广泛应用于各类技术框架中,例如 JDBC 驱动、SpringDubbo 等等。Dubbo 并未使用原生的 Java SPI,而是重新实现了一套更加强大的 Dubbo SPI。本文将简单介绍 SPI 的设计理念,通过示例带你体会 SPI 的作用,通过 Dubbo 获取拓展的流程图源码分析带你理解 Dubbo SPI 的工作原理。深入了解 Dubbo SPI,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。

阅读全文 »

Configuration 注解是 Spring 中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean 注解将创建的 bean 交给 Spring 容器管理。神奇的是,被 Bean 注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug 时注意过配置类的实例,你会发现配置类的 Class 名称中携带 EnhancerBySpringCGLIB。本文将从源码角度,分析 Configuration 注解是如何工作的。

阅读全文 »

Spring 中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring 的具体处理过程,并在最后给出笔者的个人思考。

阅读全文 »

Spring AOP 是基于代理实现的,它既支持 JDK 动态代理也支持 CGLib。

  • 在什么时候创建代理对象的?
  • 怎么创建代理对象的?

过程简单图解

准备工作

  • 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.12.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.12.RELEASE</version>
    </dependency>
  • 目标对象类
    1
    2
    3
    4
    5
    6
    public class MathCalculator {

    public int div(int i, int j) {
    return i / j;
    }
    }
  • 切面类
    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
    @Aspect
    public class LogAspects {

    @Pointcut("execution(public int com.moralok.aop.MathCalculator.*(..))")
    public void pointCut() {

    }

    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature().getName() + "除法运行@Before。。。参数列表为 " + Arrays.asList(joinPoint.getArgs()) + "");
    }

    @After("pointCut()")
    public void logEnd(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature().getName() + "除法结束@After。。。");
    }

    @AfterReturning(value = "pointCut()", returning = "result")
    public void logReturn(JoinPoint joinPoint, Object result) {
    System.out.println(joinPoint.getSignature().getName() + "除法正常返回@AfterReturning。。。运行结果 " + result);
    }

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void logException(JoinPoint joinPoint, Exception e) {
    System.out.println(joinPoint.getSignature().getName() + "除法异常@AfterThrowing。。。异常信息 " + e.getMessage());
    }

    @Around(value = "execution(public String com.moralok.bean.Car.getName(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println(joinPoint.getSignature().getName() + " @Around开始");
    Object proceed = joinPoint.proceed();
    System.out.println(joinPoint.getSignature().getName() + " @Around结束");
    return proceed;
    }
    }
  • 配置类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Configuration
    @EnableAspectJAutoProxy
    public class AopConfig {

    @Bean
    public MathCalculator mathCalculator() {
    return new MathCalculator();
    }

    @Bean
    public LogAspects logAspects() {
    return new LogAspects();
    }
    }
  • 测试类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class AopTest {

    @Test
    public void aopTest() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
    MathCalculator mathCalculator = ac.getBean(MathCalculator.class);
    mathCalculator.div(1, 1);
    mathCalculator.div(1, 0);
    ac.close();
    }
    }
  • Debug 断点的判断条件(可选)
    1
    beanName.equals("mathCalculator")

创建代理 Bean 和创建普通 Bean 的区别

其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。

常规的入口 getBean

mathCalculator 以 getBean 方法为起点,开始创建的过程。

1
2
3
4
5
6
@Override
public void preInstantiateSingletons() throws BeansException {
// ...(mathCalculator)
getBean(beanName);
// ...
}

应用后置处理器

在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。

可是,究竟是哪一个后置处理器做的呢

1
2
3
4
5
6
7
8
9
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
// ...
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
// ...
invokeInitMethods(beanName, wrappedBean, mbd);
// ...
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
return wrappedBean;
}

AnnotationAwareAspectJAutoProxyCreator

在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。

那么,它是什么时候,怎么加入到 beanFactory 中呢

PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。

BeanPostProcessor 的方法

postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。

  • AbstractAutoProxyCreator#postProcessAfterInitialization 就是创建代理对象的入口。
  • wrapIfNecessary 就是将 Bean 包装成代理 Bean 的入口方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
// ...
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// 什么都没做
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.contains(cacheKey)) {
// 如有必要,将 bean 包装成代理对象
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
// ...
}

创建代理 Bean 的过程

按需包装成代理 wrapIfNecessary

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
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 判断是否直接返回 bean
if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 如果有适用于当前 bean 的 advise 则为其创建代理
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

AbstractAutoProxyCreator 视角,创建代理

AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。

  1. 创建一个代理工厂 ProxyFactory
  2. 设置相关信息
  3. 通过 ProxyFactory 获取代理
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
protected Object createProxy(
Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

ProxyFactory 视角,获取代理

ProxyFactory#getProxy,创建一个 AopProxy 并委托它实现 getProxy。

AopProxy 的含义与职责从字面上有点不好理解。

1
2
3
public Object getProxy(ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}

ProxyFactor视角,创建 AopProxy

ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。

1
2
3
4
5
6
7
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
// 获取 AopProxy 工厂并创建一个 AopProxy
return getAopProxyFactory().createAopProxy(this);
}

AopProxyFactory视角,创建 AopProxy

AopProxyFactory#createAopProxy。

  • AopProxyFactory 有且仅有一个默认实现 DefaultAopProxyFactory。
  • createAopProxy 方法会根据配置信息,返回具体实现:开箱即用的有 JdkDynamicAopProxy 或者 ObjenesisCglibAopProxy。

这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
}

获取代理 AopProxy#getProxy

AopProxy 视角,获取代理。

JDK 动态代理

JdkDynamicAopProxy。

1
2
3
4
5
6
@Override
public Object getProxy(ClassLoader classLoader) {
// ...
// JDK 动态代理,已经和 Spring 无关
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
InvocationHandler 的 invoke 方法

根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this) 可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。

1
2
3
4
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
}

CGLib 动态代理

ObjenesisCglibAopProxy。

1
2
3
4
5
6
7
8
@Override
public Object getProxy(ClassLoader classLoader) {
// ...
// CGLib 动态代理,已经和 Spring 无关
Enhancer enhancer = createEnhancer();
// ...
return createProxyClassAndInstance(enhancer, callbacks);
}
为什么 Spring 中没有依赖 CGLib

你可能会注意到 Spring 中并没有直接依赖 CGLib,像 Enhancer 所在的包是 org.springframework.cglib.proxy。根据文档:

从 spring 3.2 开始,不再需要将 cglib 添加到类路径中,因为 cglib 类在 org.springframework 下重新打包并分布在 spring-core jar 中。 这样做既是为了方便,也是为了避免与使用不同版本 cglib 的其他项目发生潜在冲突。

创建代理前的准备

在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。

Bean 后置处理器 AspectJAwareAdvisorAutoProxyCreator 在什么时候,怎么加入到 beanFactory 中的?

Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。

1
2
3
4
5
6
7
8
@Override
public void refresh() throws BeansException, IllegalStateException {
// ...
invokeBeanFactoryPostProcessors(beanFactory);
// ...
finishBeanFactoryInitialization(beanFactory);
// ...
}

从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。

从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。

@EnableXXX 的魔法

注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;
boolean exposeProxy() default false;
}

在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 如有必要注册 AspectJAnnotationAutoProxyCreator
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// 根据配置设置一些属性
AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}
1
2
3
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

切面类 LogAspect 的解析是在什么时候?

进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。

  1. Object bean = resolveBeforeInstantiation(beanName, mbdToUse),在实例化前进行解析。
  2. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
// ...
try {
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
// ...

Object beanInstance = doCreateBean(beanName, mbdToUse, args);
// ...
return beanInstance;
}

入口方法 resolveBeforeInstantiation

根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
Object bean = null;
if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
Class<?> targetType = determineTargetType(beanName, mbd);
if (targetType != null) {
// 注意,应用的是实例化前的处理
bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
if (bean != null) {
// 注意,应用的是初始化后的处理
bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
}
}
}
mbd.beforeInstantiationResolved = (bean != null);
}
return bean;
}

InstantiationAwareBeanPostProcessor

应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 循环依次处理
Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
if (result != null) {
return result;
}
}
}
return null;
}

AnnotationAwareAspectJAutoProxyCreator 不仅仅是一个 BeanPostProcessor,它还是一个 InstantiationAwareBeanPostProcessor。

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
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
Object cacheKey = getCacheKey(beanClass, beanName);

if (beanName == null || !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

if (beanName != null) {
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
this.targetSourcedBeans.add(beanName);
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
}

return null;
}

和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。

注意:以下方法应注意是否被子类重写

org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip

1
2
3
4
5
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
// 查找并缓存 advisors
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// ...
}

org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean

1
2
3
4
protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
// ...
}

org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

1
2
3
4
5
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
// 查找并缓存 advisors
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// ...
}

容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。

查找并缓存 advisors

org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

1
2
3
4
5
protected List<Advisor> findCandidateAdvisors() {
List<Advisor> advisors = super.findCandidateAdvisors();
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
return advisors;
}

org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

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
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
// 第一次进入,没有缓存
synchronized (this) {
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new LinkedList<Advisor>();
aspectNames = new LinkedList<String>();
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
// ...
// 如果是切面,解析得到 advisors
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
// ...
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
// 以后进来读缓存
List<Advisor> advisors = new LinkedList<Advisor>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache 观察 advisors 的查找情况。

介绍

JDK 动态代理

Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。

CGLib 动态代理

使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。

cglib Github 仓库

适用场景

  • JDK 动态代理适用于实现接口的类,对未实现接口的类无能为力。
  • CGLib 不要求类实现接口,但对 final 方法无能为力。

性能比较

  • 在 JDK 8 以前,CGLib 性能更好
  • 从 JDK 8 开始,JDK 动态代理性能更好

根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。

使用

代理对象的类和接口

代理对象的类和实现的接口:

  • HelloService.java

    1
    2
    3
    public interface HelloService {
    void sayHello(String name);
    }
  • HelloService.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello(String name) {
    if (name == null) {
    throw new IllegalArgumentException("name can not be null");
    }
    System.out.println("Hello " + name);
    }
    }

JDK 动态代理示例

  • 自定义 InvocationHandler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class UserServiceInvocationHandler implements InvocationHandler {

    private Object target;

    public UserServiceInvocationHandler(Object target) {
    this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    System.out.println("do sth. before invocation");
    Object ret = method.invoke(target, args);
    System.out.println("do sth. after invocation");
    return ret;
    } catch (Exception e) {
    System.out.println("do sth. when exception occurs");
    throw e;
    } finally {
    System.out.println("do sth. finally");
    }
    }
    }
  • 测试类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class JdkProxyTest {

    public static void main(String[] args) {
    HelloService target = new HelloServiceImpl();
    ClassLoader classLoader = target.getClass().getClassLoader();
    Class<?>[] interfaces = target.getClass().getInterfaces();
    UserServiceInvocationHandler invocationHandler = new UserServiceInvocationHandler(target);

    HelloService proxy = (HelloService) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    proxy.sayHello("Tom");
    System.out.println("=================");
    proxy.sayHello(null);
    }
    }
  • 结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    do sth. before invocation
    Hello Tom
    do sth. after invocation
    do sth. finally
    =================
    do sth. before invocation
    do sth. when exception occurs
    do sth. finally
    Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
    at com.sun.proxy.$Proxy0.sayHello(Unknown Source)
    at com.moralok.proxy.jdk.JdkProxyTest.main(JdkProxyTest.java:19)
    Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.moralok.proxy.jdk.UserServiceInvocationHandler.invoke(UserServiceInvocationHandler.java:18)
    ... 2 more
    Caused by: java.lang.IllegalArgumentException: name can not be null
    at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)

CGLib 动态代理示例

  • 引入依赖
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
    </dependency>
    </dependencies>
  • 自定义 MethodInterceptor
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class UserServiceMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    try {
    System.out.println("do sth. before invocation");
    Object ret = proxy.invokeSuper(obj, args);
    System.out.println("do sth. after invocation");
    return ret;
    } catch (Exception e) {
    System.out.println("do sth. when exception occurs");
    throw e;
    } finally {
    System.out.println("do sth. finally");
    }
    }
    }
  • 测试类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class CglibTest {

    public static void main(String[] args) {
    UserServiceMethodInterceptor methodInterceptor = new UserServiceMethodInterceptor();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(HelloServiceImpl.class);
    enhancer.setCallback(methodInterceptor);

    HelloService proxy = (HelloService) enhancer.create();
    proxy.sayHello("Tom");
    System.out.println("=================");
    proxy.sayHello(null);
    }
    }
  • 结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    do sth. before invocation
    Hello Tom
    do sth. after invocation
    do sth. finally
    =================
    do sth. before invocation
    do sth. when exception occurs
    do sth. finally
    Exception in thread "main" java.lang.IllegalArgumentException: name can not be null
    at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
    at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.CGLIB$sayHello$0(<generated>)
    at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.invoke(<generated>)
    at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
    at com.moralok.proxy.cglib.UserServiceMethodInterceptor.intercept(UserServiceMethodInterceptor.java:14)
    at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.sayHello(<generated>)
    at com.moralok.proxy.cglib.CglibTest.main(CglibTest.java:18)

查看 JDK 生成的代理类

使用以下语句,将在工作目录下生成代理类的 Class 文件。

1
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
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
public final class $Proxy0 extends Proxy implements HelloService {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
// 将 InvocationHandler 传递给父类 Proxy
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

// 代理方法调用 InvocationHandler 的 invoke 方法
public final void sayHello(String var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

// 静态代码块,初始化 Method 属性。
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.moralok.proxy.HelloService").getMethod("sayHello", Class.forName("java.lang.String"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

查看 CGLib 生成的子类

使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。

1
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class");
  • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.class,代理类
  • HelloServiceImpl$$FastClassByCGLIB$$a5654167.class,被代理类的 FastClass
  • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.class,代理类的 FastClass

代理类定义

继承了被代理类。

1
2
public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory {
}

静态代码块

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
static {
// 调用静态钩子方法
CGLIB$STATICHOOK1();
}

static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31");
Class var1;
// 获取 Object 类的 equals、toString、hashCode、clone 这几个特定方法的 Method 对象
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
// 还生成了相对应的 Method 属性保存(为了减少一次寻址吗?)
CGLIB$equals$1$Method = var10000[0];
// 为每一个 Method 创建一个 MethodProxy
CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = var10000[1];
CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = var10000[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = var10000[3];
CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
// 被代理类的方法也做相同处理
CGLIB$sayHello$0$Method = ReflectUtils.findMethods(new String[]{"sayHello", "(Ljava/lang/String;)V"}, (var1 = Class.forName("com.moralok.proxy.HelloServiceImpl")).getDeclaredMethods())[0];
CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");
}

MethodProxy 稍后再做介绍。

构造器方法

构造器方法内,调用了绑定回调(Callbacks)方法。

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
public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() {
CGLIB$BIND_CALLBACKS(this);
}

// 标识是否已经绑定过回调
private boolean CGLIB$BOUND;

private static final void CGLIB$BIND_CALLBACKS(Object var0) {
HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 var1 = (HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31)var0;
if (!var1.CGLIB$BOUND) {
// 未绑定过回调则进行绑定,更新标识
var1.CGLIB$BOUND = true;
// 先获取 THREAD_CALLBACKS
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
// 如果为 null,再获取 STATIC_CALLBACKS
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
// 如果仍然为 null,直接返回
return;
}
}

// 每一个 Callback (像之前的 Method 一样)都有专门的属性保存
var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}

}

生成的代理方法

CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final void CGLIB$sayHello$0(String var1) {
super.sayHello(var1);
}

public final void sayHello(String var1) {
// 获取回调(拦截器)
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
// 如果为 null,先进行回调绑定
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}

if (var10000 != null) {
// 如果回调(拦截器)不为 null,则调用 intercept 方法
var10000.intercept(this, CGLIB$sayHello$0$Method, new Object[]{var1}, CGLIB$sayHello$0$Proxy);
} else {
// 否则直接调用父类方法
super.sayHello(var1);
}
}

CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。

分析 MethodProxy

如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。

创建 MethodProxy

MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。

1
2
3
4
5
6
7
8
9
10
11
12
CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");

public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
// sayHello 方法签名
proxy.sig1 = new Signature(name1, desc);
// CGLIB$sayHello$0 方法签名
proxy.sig2 = new Signature(name2, desc);
// 被代理类和代理类
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}

CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static class CreateInfo
{
// 被代理类
Class c1;
// 代理类
Class c2;
NamingPolicy namingPolicy;
GeneratorStrategy strategy;
boolean attemptLoad;

public CreateInfo(Class c1, Class c2)
{
this.c1 = c1;
this.c2 = c2;
AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();
if (fromEnhancer != null) {
namingPolicy = fromEnhancer.getNamingPolicy();
strategy = fromEnhancer.getStrategy();
attemptLoad = fromEnhancer.getAttemptLoad();
}
}
}

FastClass 和方法索引对

调用原始方法 invokeSuper

MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。

1
2
3
4
5
6
7
8
9
10
11
12
// invoke 方法的代码相似
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
// 初始化,生成 FastClassInfo
init();
FastClassInfo fci = fastClassInfo;
// 调用原始(父类)方法
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}

生成 FastClass 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void init()
{
// 双重校验锁,生成 FastClass 和方法索引对
if (fastClassInfo == null)
{
synchronized (initLock)
{
if (fastClassInfo == null)
{
CreateInfo ci = createInfo;

FastClassInfo fci = new FastClassInfo();
// 生成 FastClass
fci.f1 = helper(ci, ci.c1);
fci.f2 = helper(ci, ci.c2);
// 获取方法索引
fci.i1 = fci.f1.getIndex(sig1);
fci.i2 = fci.f2.getIndex(sig2);
fastClassInfo = fci;
createInfo = null;
}
}
}
}

FastClass 信息

  • f1 是被代理类的 FastClass 对象,i1 是 CGLIB$sayHello$0 方法在生成的 FastClass 中的索引。
  • f2 是代理类的 FastClass 对象,i2 是 sayHello 方法在生成的 FastClass 中的索引。

invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。

CGLib 完全有能力获得 CGLIB$sayHello$0 的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。

1
2
3
4
5
6
7
private static class FastClassInfo
{
FastClass f1;
FastClass f2;
int i1;
int i2;
}

FastClass 的 invoke 方法

以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511 为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0 方法。

  1. 获取代理对象
  2. 根据传入的方法索引,调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
HelloServiceImpl..EnhancerByCGLIB..c51b2c31 var10000 = (HelloServiceImpl..EnhancerByCGLIB..c51b2c31)var2;
int var10001 = var1;

try {
switch (var10001) {
case 0:
return new Boolean(var10000.equals(var3[0]));
// ...
case 16:
var10000.CGLIB$sayHello$0((String)var3[0]);
return null;
// ...
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}

throw new IllegalArgumentException("Cannot find matching method/constructor");
}

获取方法索引

怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。

在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch (var10000.hashCode()) {
// ...
case -1721191351:
if (var10000.equals("CGLIB$sayHello$0(Ljava/lang/String;)V")) {
return 16;
}
break;
// ...
}

return -1;
}

总结和思考

两者在使用上是相仿的。

  • 对于两者的源码,读得不多。有时候会感慨,看这么多年前的代码,还是感觉吃力。有时候想,如果不好好看源码,心里不踏实;如果花很多时间理清楚了,但是发现更多只是知道了一些细节,于整体理解的提升不大,又会感觉不值得。
  • 但也提醒自己,不要太在意,用得本就不多,涉及源码的机会更是没有,如果方方面面都要细究,人生太短,智商不够,等涉足相关问题再回头研究。
  • 基础的用法和概念应该了解,不然看到 Spring AOP 源码时,分不清 Spring 封装的边界在哪里。

借着梳理 Spring 的机会回头再看,又感觉轻松不少。

本文介绍如何通过 Dockers Compose 安装 GrafanaPrometheus 在局域网中配合各类 exporter 为主机和诸多内部服务搭建监控。

阅读全文 »

在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

基于 Redis 的分布式锁简单实现

本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

1
2
3
4
5
6
7
> setnx lock:user1 true
OK

... do something

> del lock:user1
(integer) 1

死锁问题

问题一:异常引发死锁 1

如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

改进一:设置超时时间

对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

1
2
3
4
5
6
7
8
> setnx lock:user1 true
OK
> expire lock:user1 5

... do something

> del lock:user1
(integer) 1

问题二:异常引发死锁 2

事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

改进二:setnx + expire 的原子指令

如果 setnx 和 expire 可以用一个原子指令实现就好了。

基于原生指令的实现

在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

1
2
3
4
5
6
7
> set lock:user1 true ex 5 nx
OK

... do something

> del lock:user1
(integer) 1
基于 Lua 脚本的实现

除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

1
2
3
4
5
6
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('expire', KEYS[1], ARGV[2])
return true
else
return false
end

超时问题

基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

问题一:其他线程提前进入临界区

如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

问题二:错误释放其他线程的锁

如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

改进一:不要用于较长时间的任务

与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

改进二:使用 watchdog 实现锁续期

为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

1
2
3
4
5
6
7
private void scheduleRenewal() {
String value = lockValue.get();
ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
() -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
);
renewalTask.set(scheduledFuture);
}

但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

改进三:加锁时指定 tag

可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

基于原生指令的实现
1
2
3
4
tag = random.nextint()
if redis.set(key, tag, nx= True, ex=5):
do_something()
redis.delifequals(key, tag)

但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

基于 Lua 脚本的实现
1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

可重入性

可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

使用 ThreadLocal 实现锁计数

Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

public boolean tryLock() {
Integer count = lockCount.get();
if (count != null && count > 0) {
lockCount.set(count + 1);
return true;
}
String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
if ("OK".equals(result)) {
lockCount.set(1);
scheduleRenewal();
return true;
}
return false;
}

使用 Redis hash 实现锁计数

还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

1
2
3
4
5
6
7
8
9
10
if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
return redis.call('pttl', KEYS[1]);

书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

代码实现

redis-lock

参考文章

  • 《Redis 深度历险,核心原理与应用实践》

演示字节码指令的执行

1
2
3
4
5
6
7
8
9
public class ByteCodeTest_2 {

public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

操作数栈和本地变量表的大小

在编译期间就可计算得到操作数栈和本地变量表的大小。

1
stack=2, locals=4, args_size=1

本地变量表

Slot,即槽位,可理解为索引。

1
2
3
4
5
Start  Length  Slot  Name   Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I

运行时常量池

1
2
3
#3 = Integer            32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V

字节码指令

1
2
3
4
5
6
7
8
9
10
11
12
 0: bipush        10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
  • bipush,将一个 byte,推入操作数栈。
    • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
    • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
  • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
  • ldc,从运行时常量池中将指定常量推入操作数栈。
  • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
  • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
  • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
  • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
  • getstatic,获取类的静态属性,推入操作数栈。
  • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
  • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
  • return,返回 void

分析 a++ 和 ++a

1
2
3
4
5
6
7
8
9
public class ByteCodeTest_3 {

public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}

字节码指令

1
2
3
4
5
6
7
8
9
10
11
 0: bipush        10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
  • a++ 和 ++a 的区别是先 load 还是先 iinc。
  • iinc,将局部变量表指定槽位的数加上一个常数。
  • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
  • b = 10 + 12 + 12 = 34
  • a = 10 + 1 + 1 - 1 = 11

分析判断条件

1
2
3
4
5
6
7
8
9
10
11
12
public class ByteCodeTest_4 {

public static void main(String[] args) {
int a = 0;
// ifeq, goto
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}

字节码指令

1
2
3
4
5
6
7
8
9
10
 0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
  • iconst,将一个 int 常量推入操作数栈。
  • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
  • goto,总是进入的分支,跳转到指定行号。

涉及的字节码指令

  • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
  • istore,将栈顶的 int,存入局部变量表的指定槽位。
  • iload,将局部变量表指定槽位的 int,推入操作数栈。
  • ldc,从运行时常量池将指定常量推入操作数栈。
  • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
  • getstatic,获取类的静态属性,推入操作数栈。
  • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
  • return,返回 void。
  • iinc,将局部变量表中指定槽位的数加一个常量。
  • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
    • ifeq,equals
    • ifne,not equals
    • iflt,less than
    • ifge,greater than or equals
    • ifgt,great than
    • ifle,less than or equals
  • goto,总是进入的分支,跳转到指定行号。

每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。

概述

垃圾收集(Garbage Collection,简称 GC)需要考虑什么?

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

为什么要去了解垃圾收集和内存分配?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。

在 Java 中,垃圾收集需要关注哪些内存区域?
程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

哪些内存需要回收?

哪些对象是还存活着,哪些已经死亡?

对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?

引用计数算法(Reference Counting)

优点:

  • 原理简单
  • 判定效率高

缺点:

  • 例外情况多,需要额外处理(比如循环引用)

提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?

可达性分析算法(Reachability Analysis)

  • 选取一系列称为“GC Roots”的根对象作为起始节点集。
  • 根据引用关系向下搜索。
  • 如果某个对象到 GC Roots 间没有任何引用链相连,即该对象不可能再被使用。用图论的话说,就是 GC Roots 到该对象不可达。

那么可作为 GC Roots 的对象有哪些呢?
固定的 GC Roots,主要是在全局性引用和执行上下文中:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的变量。
  3. 在方法区中类常量引用的对象,比如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中 JNI,即 Native 方法引用的对象。
  5. Java 虚拟机内部的引用,如基本类型的 Class 对象,常驻的异常类型,还有系统类加载器。
  6. 所有被同步锁(synchronized)持有的对象
  7. 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(不懂)。

临时性的GC Roots:
除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。

比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。

“引用”的概念扩充

对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。

曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。

引用的分类

  • 强引用(Strongly Reference),只要强引用还在,绝不会回收。
  • 软引用(Soft Reference),只被软引用关联的对象,在系统发生 OOM 前,会被列入回收范围进行第二次回收。
  • 弱引用(Weak Reference),只被弱引用关联的对象,只能生存到下一次垃圾收集发生为止,无论内存是否 足够,都会回收。
  • 虚引用(Phantom Reference),一个对象是否有虚引用,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是为了在对象被回收时收到一个系统通知。

虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。

弱引用的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ReferenceTest_3 {
private static final int _4MB = 4 * 1024 * 1024;

// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
// list -> WeakReference -> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
System.out.print(list.size() + " ");
for (WeakReference<byte[]> w : list) {

System.out.print(w.get() + " ");
}
System.out.println();
}
System.out.println("循环结束: " + list.size());
System.in.read();
}
}

在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。

了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。

finalize 方法

有趣的知识点,无趣的面试考点。

方法区的垃圾回收是什么样的?

  • 《Java虚拟机规范》中提到可以不要求虚拟机在方法区实现垃圾收集
  • 确实有未实现或未完整实现方法区类型卸载的收集器
  • 原因是方法区垃圾收集的性价比通常比较低

方法区的垃圾收集主要回收两部分:

  • 废弃的常量
  • 不再使用的类型

如何判定一个常量是否废弃?
没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。

“虚拟机中也没有其他地方引用这个字面量”怎么理解?

如何判定一个类型是否可卸载?

  • 该类的所有的实例都已经被回收
  • 加载该类的类加载器已经被回收
  • 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。

条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
“无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
Class 对象没有被引用时,会被回收吗?
卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?

如何回收:垃圾收集算法

分类:

  • 引用计数式垃圾收集(Reference Counting GC)
  • 追踪式垃圾收集(Tracing GC)

分代收集理论

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(String Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。
正因为有了区域划分,垃圾收集器才可以每次只回收一个或某些部分的区域,因而才有了“Minor GC”、“Major GC”和“Full GC”等回收类型划分;针对不同区域安排与里面存储对象存亡特征相匹配的垃圾收集算法,因而才发展出“标记-复制”、“标记-清除”和“标记-整理”等垃圾收集算法。

了解分代收集理论,分代收集算法更显得有理有据。

一般把 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。

针对不同分代的垃圾收集分类:

  • 部分收集(Partial GC):目标不是完整收集整个堆、
    • 新生代收集(Minor GC/Young GC)
    • 老年代收集(Major GC/Old GC):只有 CMS。Major GC有些混淆,应按上下文注意是老年代收集还是整堆收集。
    • 混合收集(Mixed GC):只有 G1
  • 整堆收集(Full GC)

区域划分引起另一个问题,跨代引用。这个问题在前文的 GC Roots 选择时也提到过。
根据前两条假说可逻辑推理得出隐含推论:存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

  1. 跨代引用假说(Integenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

依据这条假说,我们不应再为少量的跨代引用而去扫描整个老年代。那么怎么处理跨代引用呢?在后面 HotSpot 的实现细节中我们再提。

标记-清除算法(Mark Sweep)

在 1960 年由 Lisp 之父 John McCarthy 提出,分为标记和清除两个阶段。
缺点:

  • 执行效率不稳定,对象越多,且需要回收的对象越多,效率会降低。
  • 内存空间碎片化,长时间后没有足够的连续内存分配大对象。

标记-复制算法

为解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969 年 Fenichel 提出“半区复制”(Semispace Copying)。将内存划分为大小相等的两块,每次只使用一块,用完的时候,复制存活的对象到另一块。

优点:

  • 存活对象少时,仅需复制少量对象。
  • 不存在内存空间碎片

缺点:

  • 空间浪费

适用场景:

  • 新生代。

怎么减少空间的浪费呢?
IBM 公司研究表明新生代中 98% 的对象熬不过第一轮收集。在 1989 年,Andrew Appel 针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel 式回收”。具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。
HotSpot 虚拟机默认 Eden:Suvivor 为 8:1。

在测试中,尽管 survivor 有空间,仍然只在 Eden 空间进行分配。

如果 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时怎么办?
罕见情况的“逃生门”——使用其他区域(通常是老年代)进行分配担保(Handle Promotion)。

标记-整理算法(Mark Compact)

标记-复制算法面对对象存活率较高的情况,效率会降低;更关键的是,它需要额外空间进行分配担保。
针对老年代对象的存亡特征,1974 年 Edward Lueders 提出了另外一种有针对性的“标记-整理”算法,让所有存活对象都向内存空间一端移动。

移动和复制的开销有什么差距吗?撇开分配担保问题,大量复制和大量移动是类似的把?

移动对象的弊端

  • 大量对象存活的话需要大量移动对象,负担重。在以前,对象移动操作必须全程暂停用户应用程序才能进行(STW,Stop The World)。
  • 不整理内存碎片的话,需要依赖更复杂的内存分配器和内存访问器解决。内存访问最频繁,反而影响吞吐量。

不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。在这点上的区别分别演变出两种发展方向,低延迟和高吞吐量。

书中提到的内存分配器和内存访问器解决的是在碎片化的内存空间中进行内存分配,还是可以将大对象分散地存储到碎片化的空间中呢?还提到硬盘存储大文件不要求物理连续的磁盘空间以及内存访问环节的额外负担,应该是指一种分散存储的实现方案吧?

对于 CMS 的“和稀泥”解决方案,暂时容忍内存碎片,直到影响对象分配时再采用标记-整理算法收集一次,如果抛开备用的 Serial Old 单线程的效率问题不谈,除非在碎片化的内存空间中分配和访问内存在效率上低于在整理后的内存空间中,要不然“和稀泥”这种懒惰式的处理方案理论上效率更高吧?

HotSpot 的算法细节实现

根节点枚举

对象浩如烟海,怎么实现高效查找根节点的呢?

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。这是因为根节点枚举如果不在一个一致性快照中进行,准确性无法保证。

Exact VM 类型的虚拟机使用的是准确式垃圾回收,当用户线程停顿后,不需要一个不漏地检查完所有执行上下文和全局的引用位置,就有办法直接得到哪些地方存放着对象引用。HotSpot 的具体解决方案是使用一组称为 OopMap(Ordinary Object Pointer Map) 的数据结构。

  • 一旦类加载动作完成,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来。
  • 在即时编译过程中,也会在特定位置记录下栈和寄存器里什么位置是引用。

看书时,在这个地方其实有很多困惑。对于 OopMap,就像知道了一个不太懂的东西,了解了它能做什么,最明确的是,在即时编译中,会在安全点位置生成 OopMap。

如何识别数据类型

首先,我困惑的是,非准确式垃圾回收是什么东西,要找到对象引用得怎么做?
书中提到,准确式内存管理是指虚拟机可以知道内存中某个位置的数据具体是什么类型。比如内存中有一个 32 位的整数 123456,虚拟机将有能力分辨出它到底是一个指向了 123456 的内存地址的引用类型还是一个数值为 123456 的整数。
Exact VM 抛弃掉以前 Classic VM 基于句柄(Handle)的对象查找方式,因为在垃圾收集后对象可能被移动,如果地址改变(123456->654321),在没有明确信息表明内存中哪些数值式引用类型的情况下,虚拟机肯定不能把所有123456的值改为654321,所以要使用句柄来保持引用值得稳定。

看到这个举例之后容易理解多了,可能是没有手动管理内存的经验,对这方面体会不深刻。

以栈为例,栈帧里装有 int、double 等类型的数值,也有引用类型变量的地址,这些值在保守式 GC 看无法直接分辨是数值还是引用。但保守式 GC 其实还是有一定的分辨能力:

  1. 是不是正确的对齐值(可以理解为是一个正确的指针的值)
  2. 是否指向堆内的地址
  3. 是否指向对象的头(从这点上看,虚拟机是会尝试获取对象头校验一下吗?)

但是根据这些规则进行检测还是不够的,比如某一个 int 的值刚好指向某个对象的起始地址,就恰好满足上述的所有条件,这样就可能造成该对象被错误识别为存活对象,这个现象称为对堆的压迫。
保守式 GC 的“保守”二字体现在它将可疑的根看作是指针进行保守地处理。

死亡的对象被错误保留下来,挤占了堆空间,称其为“压迫”还听形象的。这个“保守”在理解以后才不感觉违和。

OopMap 的类型

  • 类型信息里记录了自己的 OopMap,这应该对应的是“一旦类加载动作完成,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来”,从对象向它引用的对象查找时使用。
  • 被 JIT 编译后的指令流,也会在特定的位置(安全点)记录下 OopMap。
  • 奇怪的是,暂时没有找到明确指出在 Java 字节码的特定位置(安全点)也记录着 OopMap。

方法区的类静态属性和类常量也是根据类型信息里的 OopMap 查找的吗?

安全点

为什么引入安全点,它解决了什么问题?

在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots。但是非常多的指令可能导致 OopMap 的内容发生变化。

书中“可能导致引用关系变化……的指令非常多”这句话让人困惑,OopMap 不是为了定位哪里是引用而非引用指向哪吧?如果只看“导致 OopMap 内容变化的指令非常多”更易理解。

在哪里生成 OopMap 呢?
既然指令会导致 OopMap 内容发生变化,最简单粗暴的就是为每一条指令生成 OopMap,但是这样会空间成本会急剧上升。
HotSpot 只在特定的位置生成 OopMap,这些位置被称为“安全点”(Safepoint)。
安全点的设定决定了并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是要求必须到达安全点才能够暂停。

安全点怎么选择呢?

  • 不能太少以至于让收集器等太久
  • 不能太频繁以至于过分增大运行时的内存负担

安全点的位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的:

  • 所有的非计数循环的末尾
  • 方法返回之前/调用方法的 call 指令后
  • 每条 Java 编译后的字节码的边界(不理解)
  • 可能抛异常的地方

有没有权威的标准描述啊。

如何在垃圾收集发生时,让所有线程(不包括 JNI 调用的线程)都跑到最近的安全点,然后停顿下来呢?

  • 抢先式中断(Preemptive Suspension),系统先中断全部用户线程,如果有用户线程中断的地方不是安全点,就恢复该线程执行,再中断,直到跑到安全点。(没人选)
  • 主动式中断(Voluntary Suspenstion),设置一个标志位,线程执行时不停主动轮询,发现中断标志为真就在最近的安全点主动挂起。

这个问题和如何通知多线程一起做一件事很像,设置标志位,再让线程运行过程中轮询。

什么时候进行轮询?
轮询标志的地方与安全点是重合的,另外还要加上创建对象和其他需要在堆上分配内存的地方,这样可以检查是否即将发生垃圾收集,避免没有足够内存分配给新对象。

仔细一想,与安全点重合似乎是理所当然,额外选取的地方也有恰当的理由。

轮询在印象里都意味着额外开销,虚拟机如何应对?
HotSpot 使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

很遗憾,看懂又不懂,总之,轮询操作优化得很高效。

安全区域

为什么引入安全区域,它解决了什么问题?

安全点机制保证了程序执行时,在不太长的时间内,就会遇到可进入垃圾收集过程的安全点,但是如果程序“不执行”的时候,比如线程处于 Sleep 或者 Blocked 时,就无法通过轮询获知中断标识。对于这种情况,引入了安全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。安全区域可以看作是扩展拉伸了的安全点。

  1. 当用户线程执行到安全区域里的代码,首先会标识自己进入了安全区域
  2. 虚拟机发起垃圾回收时,不去管这些写线程
  3. 当线程要离开安全区域,会检查虚拟机是否已完成根节点枚举(或者其他需要暂定用户线程的阶段),看起来是否处于用户线程暂停是可以检测的
  4. 如果已完成,继续;如果未完成,等待至收到可以离开安全区域的信号

不去管处于安全区域的线程的意思是什么,不是线程主动轮询是否中断的标志吗?这个标志不是全局共享的吗,是虚拟机为各个线程单独设置的?

记忆集和卡表

为什么引入记忆集,它解决了什么问题?

为了解决对象跨代引用所带来的问题,比如在标记新生代时,避免将整个老年代的对象都加入 GC Roots 进行扫描。

这个问题起初让我很迷惑,直到我认识到“老年代里有一些已经不可达但还没回收的对象,它们可能引用了新生代里的对象”这个现象,我在想我可能理解了跨代问题。但是此时,我又迟疑了,如果引用新生代对象的老年代对象已经不可达,回收掉就好了,如果老年代对象可达,那么从固定的 GC Roots 一定可以找到老年代对象,再找到新生代对象。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
如果不考虑效率和成本,最简单的方法可以用非收集区域中所有含跨代引用的对象数组来实现。

设计者通常选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,比如:

  • 字长精度:每个记录精确到一个机器字长,该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里包含跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

卡表(Card Table)是记忆集的一种具体实现。
卡表最简单的形式可以只是一个字节数组,HotSpot 虚拟机也是这样做的。

1
CARD_TABLE[this address >> 9] = 0;

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个或更多对象的字段存在跨代引用指针,就将卡表的数组元素的值标识为 1,成该元素变脏(Dirty)。

这是否以为着卡表长度为 (老年代容量 / 512),这内存占用还是不低欸。

区域内怎么判断出对象的开始和结束位置呢?虚拟机可以通过一个地址,知道该地址是一个对象的起始地址吗?

写屏障

卡表元素如何维护?它们何时变脏,谁来把它们变脏?

卡表元素何时变脏的答案是明确的——有其他分代区域中对象引用本区域对象时,其对应的卡表元素就应该变脏。

问题是如何在对象赋值的那一刻去更新维护卡表呢?
在解释执行字节码的情况中,虚拟机有充分的介入空间;但是在编译执行的场景中,即时编译后的代码已经是纯粹的机器指令流了。
HotSpot 虚拟机是通过写屏障(Write Barrier)技术维护卡表状态,在机器码层面介入每一个赋值操作。

又是一个看懂又不懂的事情,很多资料里尝试用伪代码表达,但是我很想知道它是怎么实现在机器码层面介入每一个赋值操作,是在 JVM 源码中用 C++ 实现的吗?是在解释器和 JIT 编译器共同支持下,在机器码层面加入了代码片段吗?
总之,理解为引用字段赋值的 AOP 切面吧。

  • 写前屏障(Pre-Write Barrier),直到 G1 收集器才使用到。
  • 写后屏障(Post-Write Barrier)。
1
2
3
4
5
6
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里完成卡表状态更新
post_write_barrier(field, new_value);
}

这应该只是为了表达思路的伪代码吧?

应用写屏障后,每次只要对引用更新,都会产生额外的开销,但是和 Minor GC 时扫描整个老年代的代价相比还是低很多的。

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。

现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能降低。
假设 64 个卡表元素共享一个缓存行,对应的卡页总内存为 32 KB,如果不同线程更新的对象都在这 32 KB 的内存区域内,就会导致更新卡表时发生伪共享问题。
一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有未被标记才标记为变脏。

在 JDK 7 后,新增 -XX:+UseCondCardMark 参数决定是否开启卡表更新的条件判断。开启后增加一次额外的判断开销,但能够避免伪共享问题。

并发的可达性分析

从 GC Roots 再继续向下遍历对象图,停顿时间必然与堆容量成正比。如果能够削减这部分的停顿时间,对于所有使用追踪式垃圾收集算法的收集器而言,“标记”阶段都能收益匪浅。

为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?
三色标记(Tri-color Marking),扫描过程就想灰色波峰从黑向白推进。
白色:表示对象尚未被垃圾收集器访问到。
黑色:表示对象已经被垃圾收集器访问到,且这个对象的所有引用都已经扫描过了。
灰色:表示对象已经被垃圾收集器访问到,但这个对象上至少有一个引用还没有被扫描过。

如果用户线程是冻结的,那么不会有问题。如果用户线程在收集过程中修改了引用关系,就会出现两种后果:

  • 把原本消亡的对象,错误标记为存活。(尚可以容忍,只是产生了浮动垃圾,下次收集即可)
  • 把原本存活的对象,错误标记为死亡。这是不允许发生的。

Wilson 于 1994 年在理论上证明,当且仅当以下两个条件同时满足,会产生“对象消失”的问题:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
  • 赋值器删除了全部从灰色到白色对象的直接或间接引用。

第二个条件的描述需要很精确,不能忽略关键字眼。

要解决并发扫描时的对象消亡问题,只需要破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:

  • 增量更新(Incremental Update),当黑色对象插入新的指向白色对象的引用时,记录,在并发扫描结束后,以这些黑色对象为根,重新扫描。
  • 原始快照(Snapshot At The Beginning,SATB),当灰色对象要删除指向白色对象的引用关系时,记录,在并发扫描结束后,以这些灰色对象为根,重新扫描。

和破坏死锁的思路类似。

不太理解“以这些灰色对象为根,重新扫描”,反而是“按照扫描的那一刻的对象图进行扫描”更容易理解。
应该是以这些灰色对象为根,根据记录的被删除引用,继续扫描。把继续扫描到的对象,统统视为存活对象。
理解起来有点绕,因为第一次扫描是沿着引用链进行,删除引用后,确实没办法继续往下。但是引用关系的变化通过写屏障记录了下来。第二次扫描需要 STW。
如果没有理解错的话,这时候会产生一种错误将白标记为黑的浮动垃圾;第一种方案不会出现该情况;但两种方案都不能避免已标记为黑的对象在并发时成为浮动垃圾。

理解了这部分,再看垃圾收集器的过程示意图中的停顿和并发,不再枯燥反觉有趣了。

其实我不太理解

参考文章

  1. 《深入理解Java虚拟机》
  2. JVM中的OopMap
  3. 图解 OopMap、Safe Point、Safe Region
  4. 详解GC(一)理论篇

内存区域

JVM 内存区域划分为:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

程序计数器

虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack),线程私有,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

可以使用 -Xss1024k 设置虚拟机栈的大小。默认情况下都是 1024k,只有 Windows 中取决于虚拟内存。

栈内存溢出

  1. 栈帧过多导致栈内存溢出
  2. 栈帧过大导致栈内存溢出(难复现)

不正确的递归调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StackTest_4 {

private static int count = 0;

// 改变栈的大小限制 -Xss256k,观察调用次数的变化
public static void main(String[] args) {
try {
method1();
} catch (Throwable t) {
t.printStackTrace();
} finally {
// 默认情况下经过 20000+ 次,改变参数后 3000+ 次
System.out.println(count);
}
}

private static void method1() {
count++;
method1();
}
}

循环引用导致 JSON 解析无限循环

并非只有自己写的递归方法可能引发栈内存溢出,有可能第三方库也会引发栈内存溢出。

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
public class StackTest_5 {

public static void main(String[] args) throws JsonProcessingException {
Department department = new Department();
department.setName("Tech");

Employee employee1 = new Employee();
employee1.setName("Tom");
employee1.setDepartment(department);

Employee employee2 = new Employee();
employee2.setName("Tim");
employee2.setDepartment(department);

department.setEmployees(Arrays.asList(employee1, employee2));

ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(department));
}

static class Department {
private String name;
private List<Employee> employees;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Employee> getEmployees() {
return employees;
}

public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
}

static class Employee {
private String name;
private Department department;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Department getDepartment() {
return department;
}

public void setDepartment(Department department) {
this.department = department;
}
}
}

局部变量的线程安全问题

  1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
  2. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
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
public class StackTest_3 {

public static void main(String[] args) {
method1();
}

// 线程安全
private static void method1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}

// 线程不安全
private static void method2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}

// 线程不安全,看到一个说法:发生指令重排,sb 的 append 操作发生在返回之后(有待确认)
private static StringBuilder method3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}

线程问题排查

CPU 占用率居高不下

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
public class ThreadTest_1 {

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("t1...");
while (true) {

}
}, "thread1").start();

new Thread(null, () -> {
System.out.println("t2...");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();

new Thread(null, () -> {
System.out.println("t3...");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}

当发现 CPU 占用率居高不下时,可以尝试以下步骤:

  1. top,定位 cpu 占用高的进程 id。
  2. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
  3. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
1
2
3
4
5
"thread1" #8 prio=5 os_prio=0 tid=0x00007f9bd0162800 nid=0x1061ad runnable [0x00007f9bd56eb000]
java.lang.Thread.State: RUNNABLE
at com.moralok.jvm.thread.ThreadTest_1.lambda$main$0(ThreadTest_1.java:10)
at com.moralok.jvm.thread.ThreadTest_1$$Lambda$1/250421012.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)

死锁,迟迟未返回结果

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
public class ThreadTest_2 {

private static final Object A = new Object();
private static final Object B = new Object();

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("t1...");
synchronized (A) {
System.out.println(Thread.currentThread().getName() + " get A");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println(Thread.currentThread().getName() + " get B");
}
}
}, "thread1").start();

new Thread(null, () -> {
System.out.println("t2...");
synchronized (B) {
System.out.println(Thread.currentThread().getName() + " get B");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println(Thread.currentThread().getName() + " get A");
}
}
}, "thread2").start();
}
}
  1. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
  2. 其他工具如 jconsole 也具有检测死锁的功能。

本地方法栈

堆(Heap)的特点:

  1. 线程共享,需要考虑线程安全问题。
  2. 存在垃圾回收机制。
  3. 使用 -Xmx8m 设置大小。

堆内存溢出

既然堆有垃圾回收机制,为什么还会发生内存溢出呢?最开始的时候,我也有这样的困惑。
后来我才认识到,还在使用中的对象是不能被强制回收的,不再使用的对象不是立刻回收的。当创建对象却没有足够的内存空间时,如果清理掉那些不再使用的对象就有足够的内存空间,就不会发生内存溢出,程序只是表现为卡顿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HeapTest_1 {  

// -Xmx8m
// 不设置可能不提示 Java heap space,出错地方不同,报错信息不同
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String s = "hello";
while (true) {
list.add(s);
s = s + s;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
System.out.println("运行次数 " + i);
}
}
}
1
2
3
4
5
6
7
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:141)
at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
运行次数 17

堆内存溢出的发生往往需要长时间的运行,因此在排查相关问题时,可以适当调小堆内存。

监测堆内存

  1. 使用 jps 查看 Java 进程列表
  2. 使用 jmap -heap pid 查看堆内存信息
  3. 还可以使用 jconsole 观察堆内存变化曲线
  4. 还可以使用 VisualVM 查看堆信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HeapTest_2 {

public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
TimeUnit.SECONDS.sleep(30);
// 堆空间占用上升 10MB
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println("2...");
TimeUnit.SECONDS.sleep(30);
bytes = null;
// 堆空间占用下降
System.gc();
System.out.println("3...");
TimeUnit.SECONDS.sleep(3000);
}
}

使用 jmap -heap pid 查看堆内存信息:

1
2
3
4
5
6
7
Eden Space:
capacity = 268435456 (256.0MB)
used = 32212360 (30.72010040283203MB)

used = 42698136 (40.720115661621094MB)

used = 5368728 (5.120018005371094MB)

使用 jconsole 查看堆内存信息:

堆内存占用居高不下

当你发现堆内存占用居高不下,经过 GC,下降也不明显,如果你想查看一下堆内的具体情况,可以将其 dump 查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HeapTest_3 {  

// jps 查进程,jmap 看堆内存,jconsole 执行GC,堆内存占用没有明显下降
// 使用 VisualVM 的堆 dump 功能,观察大对象
public static void main(String[] args) throws IOException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
System.in.read();
}

static class Student {
private byte[] score = new byte[1024 * 1024];
}
}

可使用 VisualVM 的 Heap Dump 功能:

也可使用 jmap -dump:format=b,file=filename.hprof pid,需要其他分析工具搭配。

方法区

根据《Java虚拟机规范》,方法区在逻辑上是堆的一部分,但是在具体实现上,各个虚拟机厂商并不相同。对于 Hotspot 而言:

  • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
  • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。

方法区溢出

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
public class MethodAreaTest_1 extends ClassLoader {

// -XX:MaxMetaspaceSize=8m MaxMetaspaceSize is too small.
// -XX:MaxMetaspaceSize=10m java.lang.OutOfMemoryError: Compressed class space
// 不是 Metaspace 应该是某个参数设置的问题
// JDK 6: -XX:MaxPermSize=8m PermGen space
public static void main(String[] args) {
int j = 0;
try {
MethodAreaTest_1 methodAreaTest1 = new MethodAreaTest_1();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter classWriter = new ClassWriter(0);
// 版本号,public,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回二进制字节码
byte[] code = classWriter.toByteArray();
// 加载类
methodAreaTest1.defineClass("Class" + i, code, 0, code.length);
}
} catch (ClassFormatError e) {
e.printStackTrace();
} finally {
System.out.println("次数 " + j);
}
}
}
  1. 当设置的值太小时 -XX:MaxMetaspaceSize=8m,提示 MaxMetaspaceSize is too small。
  2. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
  3. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
  4. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。

不要认为自己不会写动态生成字节码相关的代码就忽略这方面的问题,如今很多框架使用字节码技术大量地动态生成类。

运行时常量池

二进制字节码文件主要包含三类信息:

  1. 类的基本信息
  2. 类的常量池(Constant Pool)
  3. 类的方法信息

使用 javap 反编译

1
2
3
4
5
6
public class MethodAreaTest_2 {  

public static void main(String[] args) {
System.out.println("hello world");
}
}
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
Classfile /C:/Users/username/Documents/github/jvm-study/target/classes/com/moralok/jvm/memory/methodarea/MethodAreaTest_2.class
Last modified 2023-11-4; size 619 bytes
MD5 checksum 0ed10a8f0a03be54fd4159958ee7446c
Compiled from "MethodAreaTest_2.java"
public class com.moralok.jvm.memory.methodarea.MethodAreaTest_2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/moralok/jvm/memory/methodarea/MethodAreaTest_2
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 MethodAreaTest_2.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/moralok/jvm/memory/methodarea/MethodAreaTest_2
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.moralok.jvm.memory.methodarea.MethodAreaTest_2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "MethodAreaTest_2.java"
  1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
  2. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
  3. 这个过程其实就是信息从文件进入了内存。

虚拟机解释器(interpreter)需要解释的字节码指令如下:

1
2
3
0: getstatic     #2
3: ldc #3
5: invokevirtual #4

索引 #2 的意思就是去常量表里查找对应项代表的事物。

直接内存

  • 常见于 NIO 操作中的数据缓冲区。
  • 分配和回收的成本较高,但读写性能更高。
  • 不由 JVM 进行内存释放

NIO 和 IO 的拷贝性能

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
public class DirectMemoryTest_1 {  

private static final String FROM = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY.mp4";
private static final String TO = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY-copy.mp4";
private static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
io();
directBuffer();
}

private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(buffer);
if (len == -1) {
break;
}
buffer.flip();
to.write(buffer);
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时 " + (end - start) / 1000_000.0);
}

private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO)) {
byte[] buffer = new byte[_1Mb];
while (true) {
int len = from.read(buffer);
if (len == -1) {
break;
}
to.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时 " + (end - start) / 1000_000.0);
}
}
1
2
io 用时 1676.9797
directBuffer 用时 836.4796

直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DirectMemoryTest_2 {  

private static final int _100Mb = 1024 * 1024 * 100;

public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} catch (Throwable t) {
t.printStackTrace();
} System.out.println(i);
}
}
1
2
3
4
5
6
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
145

这似乎是代码中抛出的异常,而不是真正的直接内存溢出?

直接内存释放的原理

演示直接内存的释放受 GC 影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DirectMemoryTest_3 {

private static final int _1GB = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
// 随着 ByteBuffer 的释放,从任务管理器界面看到程序的内存的占用迅速下降 1GB。
System.gc();
System.in.read();
}
}

手动进行直接内存的分配和释放

在代码中实现手动进行直接内存的分配和释放。

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
public class DirectMemoryTest_4 {

private static final int _1GB = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();

// 分配内存
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base, _1GB, (byte) 0);
System.in.read();

// 释放内存
unsafe.freeMemory(base);
System.in.read();
}

private static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

如何将 GC 和直接内存的分配和释放关联

本质上,直接内存的自动释放是利用了虚引用的机制,间接调用了 unsafe 的分配和释放直接内存的方法。

DirectByteBuffer 就是使用 unsafe.allocateMemory(size) 分配直接内存。DirectByteBuffer 对象以及一个 Deallocator 对象(Runnable 类型)一起用于创建了一个虚引用类型的 Cleaner 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
DirectByteBuffer(int cap) {

// 省略
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 省略
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

根据虚引用的机制,如果 DirectByteBuffer 对象被回收,虚引用对象会被加入到 Cleanner 的引用队列,ReferenceHandler 线程会处理引用队列中的 Cleaner 对象,进而调用 Deallocator 对象的 run 方法。

1
2
3
4
5
6
7
8
9
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
0%