diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index 99cec07f..4b85cf2f 100644 --- a/2020/08/19/docker-frequently-used-commands/index.html +++ b/2020/08/19/docker-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

更新于 - + @@ -768,13 +768,13 @@

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/05/27/how-to-install-clash-on-ubuntu/index.html b/2023/05/27/how-to-install-clash-on-ubuntu/index.html index 170647b6..2781659a 100644 --- a/2023/05/27/how-to-install-clash-on-ubuntu/index.html +++ b/2023/05/27/how-to-install-clash-on-ubuntu/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -365,13 +365,13 @@

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html index 4708b1fb..642aafa5 100644 --- a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -346,13 +346,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html index 35212097..347f22a0 100644 --- a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -351,13 +351,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html index 761f9372..a94a9130 100644 --- a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html +++ b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -378,13 +378,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html index 6ff38ad4..89cfef05 100644 --- a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html +++ b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html @@ -27,10 +27,10 @@ - + - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -325,8 +325,8 @@

@@ -372,13 +372,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html index 9dff814d..e6ee5017 100644 --- a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html +++ b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -380,13 +380,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html index f90821f0..53690108 100644 --- a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html +++ b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -341,13 +341,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html index ea32844a..c4f773b9 100644 --- a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html +++ b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -364,13 +364,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/29/tmux-frequently-used-commands/index.html b/2023/06/29/tmux-frequently-used-commands/index.html index 5d08cda9..a6c7377c 100644 --- a/2023/06/29/tmux-frequently-used-commands/index.html +++ b/2023/06/29/tmux-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -384,13 +384,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/07/13/Java-class-loader-source-code-analysis/index.html b/2023/07/13/Java-class-loader-source-code-analysis/index.html index cf73c771..9d1e94d1 100644 --- a/2023/07/13/Java-class-loader-source-code-analysis/index.html +++ b/2023/07/13/Java-class-loader-source-code-analysis/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -518,13 +518,13 @@

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/08/10/how-does-Spring-load-beans/index.html b/2023/08/10/how-does-Spring-load-beans/index.html index b22c1d0b..58005c2d 100644 --- a/2023/08/10/how-does-Spring-load-beans/index.html +++ b/2023/08/10/how-does-Spring-load-beans/index.html @@ -28,7 +28,7 @@ - + @@ -159,13 +159,13 @@ @@ -240,7 +240,7 @@

- + @@ -394,13 +394,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html index e6837c36..c52dbde3 100644 --- a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html +++ b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -394,13 +394,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/03/testing-and-analysis-of-StringTable/index.html b/2023/11/03/testing-and-analysis-of-StringTable/index.html index 7ea4fa52..f25f19f2 100644 --- a/2023/11/03/testing-and-analysis-of-StringTable/index.html +++ b/2023/11/03/testing-and-analysis-of-StringTable/index.html @@ -31,7 +31,7 @@ - + @@ -162,13 +162,13 @@ @@ -243,7 +243,7 @@

- + @@ -437,13 +437,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html index 3b134968..526855f1 100644 --- a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html +++ b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html @@ -32,7 +32,7 @@ - + @@ -163,13 +163,13 @@ @@ -244,7 +244,7 @@

- + @@ -471,13 +471,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/07/garbage-collection-in-Java/index.html b/2023/11/07/garbage-collection-in-Java/index.html index f3ea3831..bf14205e 100644 --- a/2023/11/07/garbage-collection-in-Java/index.html +++ b/2023/11/07/garbage-collection-in-Java/index.html @@ -34,7 +34,7 @@ - + @@ -165,13 +165,13 @@ @@ -246,7 +246,7 @@

- + @@ -673,13 +673,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html index 48df2a8d..e2ff519e 100644 --- a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html +++ b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html @@ -28,7 +28,7 @@ - + @@ -159,13 +159,13 @@ @@ -240,7 +240,7 @@

- + @@ -402,13 +402,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html index 00a119b6..2c65e8ff 100644 --- a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html +++ b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html @@ -33,7 +33,7 @@ - + @@ -164,13 +164,13 @@ @@ -245,7 +245,7 @@

- + @@ -393,13 +393,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html b/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html index e7841ffa..070fe6bf 100644 --- a/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html +++ b/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html @@ -31,7 +31,7 @@ - + @@ -162,13 +162,13 @@ @@ -243,7 +243,7 @@

- + @@ -406,13 +406,13 @@

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html b/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html index 2b2af02a..376561df 100644 --- a/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html +++ b/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html @@ -27,7 +27,7 @@ - + @@ -158,13 +158,13 @@ @@ -231,9 +231,15 @@

- - + + + @@ -359,6 +365,9 @@

+

@@ -390,13 +399,13 @@

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Pasted image 20231120223506.png b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Pasted image 20231120223506.png new file mode 100644 index 00000000..01f4c5aa Binary files /dev/null and b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Pasted image 20231120223506.png differ diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-51-48.png b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-51-48.png new file mode 100644 index 00000000..94180c3d Binary files /dev/null and b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-51-48.png differ diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-56-15.png b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-56-15.png new file mode 100644 index 00000000..a6a267d7 Binary files /dev/null and b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_15-56-15.png differ diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_20-50-04.png b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_20-50-04.png new file mode 100644 index 00000000..e0db3b8c Binary files /dev/null and b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_20-50-04.png differ diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_21-33-34.png b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_21-33-34.png new file mode 100644 index 00000000..1b8ad339 Binary files /dev/null and b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/Snipaste_2023-11-20_21-33-34.png differ diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html new file mode 100644 index 00000000..414ff2c0 --- /dev/null +++ b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Spring AOP 如何创建代理 beans | Moralok + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + +
+ + 0% +
+ + + + +
+ + + + + +
+ + + +
+ + + + + + + +
+

+ Spring AOP 如何创建代理 beans +

+ + +
+ + + + +
+

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. 设置相关信息
  4. +
  5. 通过 ProxyFactory 获取代理
  6. +
+
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. +
  3. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
  4. +
+
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 的查找情况。

+ +
+ + + + + + +
+
+ + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html index a9c33bf8..946a50eb 100644 --- a/archives/2020/08/index.html +++ b/archives/2020/08/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2020/index.html b/archives/2020/index.html index f021bf2d..e889e830 100644 --- a/archives/2020/index.html +++ b/archives/2020/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html index 759afb1a..6774e62a 100644 --- a/archives/2023/05/index.html +++ b/archives/2023/05/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index 95384496..8e2e84bc 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -390,13 +390,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html index 2c2c5ac9..4ece365a 100644 --- a/archives/2023/07/index.html +++ b/archives/2023/07/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html index f8681164..2cb16038 100644 --- a/archives/2023/08/index.html +++ b/archives/2023/08/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index 66cb5643..a3486727 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/index.html b/archives/2023/index.html index ed05ed95..6d693c72 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
- -
@@ -433,13 +433,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index 14ca3484..6861fe94 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
Hexo & NexT.Muse 强力驱动 diff --git a/archives/index.html b/archives/index.html index cee3fc11..9763b6c5 100644 --- a/archives/index.html +++ b/archives/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
- -
@@ -410,7 +410,7 @@

Moralok

@@ -433,13 +433,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 95b7b3b3..3ed3ce71 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 20 篇日志。 继续努力。 + 嗯..! 目前共计 21 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
-
-
-
- 2020 -
- - @@ -413,7 +410,7 @@

Moralok

@@ -436,13 +433,13 @@

Moralok

- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 00000000..f42f0fd5 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +归档 | Moralok + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + +
+ + 0% +
+ + + + +
+ + + + + +
+
+
+ 嗯..! 目前共计 21 篇日志。 继续努力。 +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/main.css b/css/main.css index dcd7eb2b..4e2a1e18 100644 --- a/css/main.css +++ b/css/main.css @@ -2641,7 +2641,7 @@ mark.search-keyword { vertical-align: middle; } .links-of-author a::before { - background: #ffff45; + background: #ffc1ff; display: inline-block; margin-right: 3px; transform: translateY(-2px); diff --git a/index.html b/index.html index 1cb18cda..f9578c62 100644 --- a/index.html +++ b/index.html @@ -152,13 +152,13 @@

Moralok

@@ -191,6 +191,228 @@

Moralok

+
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

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. 设置相关信息
  4. +
  5. 通过 ProxyFactory 获取代理
  6. +
+
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. +
  3. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
  4. +
+
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 的查找情况。

+ + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
@@ -224,9 +446,15 @@

- - + + + @@ -391,7 +619,7 @@

- + @@ -560,7 +788,7 @@

- + @@ -714,7 +942,7 @@

- + @@ -882,7 +1110,7 @@

- + @@ -1315,7 +1543,7 @@

- + @@ -1548,7 +1776,7 @@

- + @@ -1748,7 +1976,7 @@

- + @@ -1910,7 +2138,7 @@

- + @@ -2026,294 +2254,8 @@

- - - -
- - - - - - - -
-

- -

- - -
- - - - -
-

组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

-
1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
- -

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

-
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- -

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

-
    -
  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. -
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. -
  5. 调用 findClass(String) 方法来查找该类。
  6. -
-

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

-
-

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

-
-

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

-
1
2
3
4
5
6
7
8
9
10
11
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
- -
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

-
1
2
3
static {
ClassLoader.registerAsParallelCapable();
}
- -

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

-
    -
  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. -
  3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
  4. -
-
怎么保证这两个条件成立呢?
    -
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. -
  3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
  4. -
-

你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

-
注册工作做了什么?

简单地说就是保存了类加载器所属 ClassSet

-
1
2
3
4
5
6
7
8
@CallerSensitive
protected static boolean registerAsParallelCapable() {
// 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
// 注册调用者的 Class 为具有并行能力
return ParallelLoaders.register(callerClass);
}
- -

方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

-
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
// private 修饰,只有其外部类 ClassLoader 才可以使用
// static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
private static class ParallelLoaders {
// private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
private ParallelLoaders() {}

// the set of parallel capable loader types
// 使用 loaderTypes 时通过 synchronized 加同步锁
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
// todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 将 ClassLoader 本身注册为具有并行能力
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
// 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
// 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}

/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
- -
“注册”怎么和锁产生联系?

但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

-
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
private ClassLoader(Void unused, ClassLoader parent) {
// 由 private 修饰,不允许子类重写
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
// 如果类加载器已经注册为具有并行能力,则做一些赋值操作
parallelLockMap = new ConcurrentHashMap<>();
// 保存 package->certs 的 map 映射,相关的工作也可以并行进行
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}

// 由 protect 修饰,允许子类重写,传递了父·类加载器。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

// 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
- -

ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。

-
为什么注册的代码不能写在构造器方法里?

使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。

-
-

todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

-
-

检查目标类是否已加载

加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?

-
1
2
3
4
5
6
7
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

private native final Class<?> findLoadedClass0(String name);
- -

保证核心类库的安全性:双亲委派模型

正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

-
-

在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

-
-
为什么要用这套奇怪的机制

这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

-

通常流程如下:

-
    -
  1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
  2. -
  3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
  4. -
  5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  6. -
  7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  8. -
  9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  10. -
  11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  12. -
-

注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

-

查找类资源:findClass

默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

-

过程如下:

-
    -
  1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
  2. -
  3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
  4. -
  5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
  6. -
-
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
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// todo:
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将 name 转换为 path
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 中查找 Resource
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
- -

查找类的目录列表:URLClassPath

URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
URLClassPath 的核心构造器方法:

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
push(urls);
if (factory != null) {
// 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
jarHandler = factory.createURLStreamHandler("jar");
}
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
- -
URLClassPath#getResource

URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Resource getResource(String name, boolean check) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + name + "\")");
}

Loader loader;
// 获取缓存(默认没有用)
int[] cache = getLookupCache(name);
// 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
Resource res = loader.getResource(name, check);
if (res != null) {
return res;
}
}
return null;
}
- -
URLClassPath#getNextLoader

获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private synchronized Loader getNextLoader(int[] cache, int index) {
if (closed) {
return null;
}
if (cache != null) {
if (index < cache.length) {
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 获取 Loader
return getLoader(index);
}
}
- -
URLClassPath#getLoader(int)
    -
  1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
  2. -
  3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
  4. -
  5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
  6. -
-
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
private synchronized Loader getLoader(int index) {
if (closed) {
return null;
}
// Expand URL search path until the request can be satisfied
// or the URL stack is empty.
while (loaders.size() < index + 1) {
// Pop the next URL from the URL stack
// 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
URL url;
synchronized (urls) {
if (urls.empty()) {
return null;
} else {
url = urls.pop();
}
}
// Skip this URL if it already has a Loader. (Loader
// may be null in the case where URL has not been opened
// but is referenced by a JAR index.)
String urlNoFragString = URLUtil.urlNoFragString(url);
if (lmap.containsKey(urlNoFragString)) {
continue;
}
// Otherwise, create a new Loader for the URL.
Loader loader;
try {
// 根据 URL 创建 Loader
loader = getLoader(url);
// If the loader defines a local class path then add the
// URLs to the list of URLs to be opened.
URL[] urls = loader.getClassPath();
if (urls != null) {
push(urls);
}
} catch (IOException e) {
// Silently ignore for now...
continue;
} catch (SecurityException se) {
// Always silently ignore. The context, if there is one, that
// this URLClassPath was given during construction will never
// have permission to access the URL.
if (DEBUG) {
System.err.println("Failed to access " + url + ", " + se );
}
continue;
}
// Finally, add the Loader to the search path.
validateLookupCache(loaders.size(), urlNoFragString);
loaders.add(loader);
lmap.put(urlNoFragString, loader);
}
if (DEBUG_LOOKUP_CACHE) {
System.out.println("NOCACHE: Loading from : " + index );
}
return loaders.get(index);
}
- -
URLClassPath#getLoader(java.net.URL)

根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

-
    -
  1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
  2. -
  3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
  4. -
  5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
  6. -
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}
- -

URLClassPath.FileLoader#getResource

FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

-
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
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}

if (check)
URLClassPath.check(url);

final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}

if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
- -

ClassLoader 的搜索路径

从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。

-

以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。

-
1
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest
- -
bootstrap class loader

启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

-
    -
  • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
  • -
  • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
  • -
  • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
  • -
-

IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

-
1
2
3
4
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

进程已结束,退出代码1
- -
ExtClassLoader

扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

-
AppClassLoader

应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

-

CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

-
    -
  • Windows:.;C:\path\to\classes
  • -
  • Linux:.:/path/to/classes
  • -
-

事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

-

字节数据转换为 Class 实例:defineClass

方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
以下是定义类时最终可能调用的 native 方法。

-
1
2
3
4
5
6
7
8
9
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);

private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
- -

其方法参数有:

-
    -
  • name,目标类的名称。
  • -
  • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
  • -
  • ProtectionDomain,保护域,todo:
  • -
  • sourceCodeSource 的位置。
  • -
-

defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

-

URLClassLoader#defineClass

此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

-
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
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
// 获取最后一个 . 的位置
int i = name.lastIndexOf('.');
// 返回资源的 CodeSourceURL
URL url = res.getCodeSourceURL();
if (i != -1) {
// 截取包名 com.example
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
// 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
// 不常用
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
return defineClass(name, bb, cs);
} else {
// 以字节数组的形式返回资源数据
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
// 必须再读取字节数据后读取证书,todo:
CodeSigner[] signers = res.getCodeSigners();
// 根据 URL 和签名者创建 CodeSource
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
return defineClass(name, b, 0, b.length, cs);
}
}
- -

Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public byte[] getBytes() throws IOException {
byte[] b;
// Get stream before content length so that a FileNotFoundException
// can propagate upwards without being caught too early
// 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
// 获取缓存的 InputStream
InputStream in = cachedInputStream();

// This code has been uglified to protect against interrupts.
// Even if a thread has been interrupted when loading resources,
// the IO should not abort, so must carefully retry, failing only
// if the retry leads to some other IO exception.
// 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
// 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
boolean isInterrupted = Thread.interrupted();
int len;
for (;;) {
try {
// 获取内容长度,顺利的话就跳出循环
len = getContentLength();
break;
} catch (InterruptedIOException iioe) {
// 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
}

try {
b = new byte[0];
if (len == -1) len = Integer.MAX_VALUE;
int pos = 0;
while (pos < len) {
int bytesToRead;
if (pos >= b.length) { // Only expand when there's no room
// 如果当前读取位置已经大于等于数组长度
// 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
bytesToRead = Math.min(len - pos, b.length + 1024);
if (b.length < pos + bytesToRead) {
// 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
b = Arrays.copyOf(b, pos + bytesToRead);
}
} else {
// 数组还有空间,待读取字节长度 = 数组剩余空间
bytesToRead = b.length - pos;
}
int cc = 0;
try {
// 读取数据
cc = in.read(b, pos, bytesToRead);
} catch (InterruptedIOException iioe) {
// 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
if (cc < 0) {
// 如果读取返回值 < 0
if (len != Integer.MAX_VALUE) {
// 且长度并未无限,表示提前检测到 EOF,抛出异常
throw new EOFException("Detect premature EOF");
} else {
// 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
if (b.length != pos) {
b = Arrays.copyOf(b, pos);
}
break;
}
}
pos += cc;
}
} finally {
try {
in.close();
} catch (InterruptedIOException iioe) {
isInterrupted = true;
} catch (IOException ignore) {}

if (isInterrupted) {
// 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
Thread.currentThread().interrupt();
}
}
return b;
}
- -

getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

-
1
2
3
4
5
6
private synchronized InputStream cachedInputStream() throws IOException {
if (cis == null) {
cis = getInputStream();
}
return cis;
}
- -

在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

-
1
2
3
4
5
6
7
8
9
10
11
public InputStream getInputStream() throws IOException
{
// 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
return new FileInputStream(file);
}

public int getContentLength() throws IOException
{
// 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
return (int)file.length();
};
- -

SecureClassLoader#defineClass

URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

-
1
2
3
4
5
6
7
8
9
10
11
12
protected final Class<?> defineClass(String name,
byte[] b, int off, int len,
CodeSource cs)
{
return defineClass(name, b, off, len, getProtectionDomain(cs));
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
CodeSource cs)
{
return defineClass(name, b, getProtectionDomain(cs));
}
- -

方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ProtectionDomain getProtectionDomain(CodeSource cs) {
// 如果 CodeSource 为 null,直接返回 null
if (cs == null)
return null;

ProtectionDomain pd = null;
synchronized (pdcache) {
// 先从 Map 缓存中获取 ProtectionDomain
pd = pdcache.get(cs);
if (pd == null) {
// 从 CodeSource 中获取 PermissionCollection
PermissionCollection perms = getPermissions(cs);
// 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
pd = new ProtectionDomain(cs, perms, this, null);
pdcache.put(cs, pd);
if (debug != null) {
debug.println(" getPermissions "+ pd);
debug.println("");
}
}
}
return pd;
}
- -
getPermissions

根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

-

AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

-
1
2
3
4
5
6
7
8
9
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 URLClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);
// 允许从类路径加载的任何类退出 VM的权限。
// todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
perms.add(new RuntimePermission("exitVM"));
return perms;
}
- -

SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

-
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
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 SecureClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);

URL url = codesource.getLocation();

Permission p;
URLConnection urlConnection;

try {
// FileURLConnection 实例
urlConnection = url.openConnection();
// 允许 read 的 FilePermission 实例
p = urlConnection.getPermission();
} catch (java.io.IOException ioe) {
p = null;
urlConnection = null;
}

if (p instanceof FilePermission) {
// if the permission has a separator char on the end,
// it means the codebase is a directory, and we need
// to add an additional permission to read recursively
// 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
String path = p.getName();
if (path.endsWith(File.separator)) {
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
}
} else if ((p == null) && (url.getProtocol().equals("file"))) {
String path = url.getFile().replace('/', File.separatorChar);
path = ParseUtil.decode(path);
if (path.endsWith(File.separator))
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
} else {
/**
* Not loading from a 'file:' URL so we want to give the class
* permission to connect to and accept from the remote host
* after we've made sure the host is the correct one and is valid.
*/
URL locUrl = url;
if (urlConnection instanceof JarURLConnection) {
locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
}
String host = locUrl.getHost();
if (host != null && (host.length() > 0))
p = new SocketPermission(host,
SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
}

// make sure the person that created this class loader
// would have this permission

if (p != null) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final Permission fp = p;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() throws SecurityException {
sm.checkPermission(fp);
return null;
}
}, acc);
}
perms.add(p);
}
return perms;
}
- -

SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

-
1
2
3
4
5
6
7
8
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
// 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
check();
// ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
return new Permissions(); // ProtectionDomain defers the binding
}
- -
ProtectionDomain

ProtectionDomain 的相关构造器参数:

-
    -
  • CodeSource
  • -
  • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
  • -
  • ClassLoader
  • -
  • Principal[]
  • -
-

这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

-

ClassLoader#defineClass

抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

-
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
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();

// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}

protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
- -

主要步骤:

-
    -
  1. preDefineClass 前置处理
  2. -
  3. defineClassX
  4. -
  5. postDefineClass 后置处理
  6. -
-
preDefineClass

确定保护域 ProtectionDomain,并检查:

-
    -
  1. 未定义 java.*
  2. -
  3. 该类的签名者与包(package)中其余类的签名者相匹配
  4. -
-
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
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
// 检查 name 为 null 或者有可能是有效的二进制名称
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 如果 name 以 java. 开头,则抛出异常
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
// 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
pd = defaultDomain;
}

// 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
// todo: certs
if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}
- -
defineClassSourceLocation

确定 ClassCodeSource 位置。

-
1
2
3
4
5
6
7
8
9
private String defineClassSourceLocation(ProtectionDomain pd)
{
CodeSource cs = pd.getCodeSource();
String source = null;
if (cs != null && cs.getLocation() != null) {
source = cs.getLocation().toString();
}
return source;
}
- -
defineClassX 方法

这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

-
postDefineClass

在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

-
1
2
3
4
5
6
7
8
9
private void postDefineClass(Class<?> c, ProtectionDomain pd)
{
if (pd.getCodeSource() != null) {
// 获取证书
Certificate certs[] = pd.getCodeSource().getCertificates();
if (certs != null)
setSigners(c, certs);
}
}
- -
- - - - - -
-
- -
-
-

- - - -
@@ -2336,13 +2278,13 @@
- 42k + 45k
Hexo & NexT.Muse 强力驱动 diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 81899462..6dcb8dac 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"使用 Grafana 和 Prometheus 搭建监控","url":"/2023/11/18/setup-monitoring-using-grafana-and-prometheus/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"JDK 动态代理和 CGLib","url":"/2023/11/19/JDK-dynamic-proxy-and-CGLib/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"}] \ No newline at end of file +[{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"使用 Grafana 和 Prometheus 搭建监控","url":"/2023/11/18/setup-monitoring-using-grafana-and-prometheus/"},{"title":"JDK 动态代理和 CGLib","url":"/2023/11/19/JDK-dynamic-proxy-and-CGLib/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"Spring AOP 如何创建代理 beans","url":"/2023/11/19/how-does-Spring-AOP-create-proxy-beans/"}] \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index 00d9fd1a..82852240 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -191,6 +191,292 @@

Moralok

+
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

+
1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
+ +

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

+
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
+ +

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

+
    +
  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. +
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. +
  5. 调用 findClass(String) 方法来查找该类。
  6. +
+

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

+
+

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

+
+

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

+
1
2
3
4
5
6
7
8
9
10
11
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
+ +
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

+
1
2
3
static {
ClassLoader.registerAsParallelCapable();
}
+ +

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

+
    +
  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. +
  3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
  4. +
+
怎么保证这两个条件成立呢?
    +
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. +
  3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
  4. +
+

你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

+
注册工作做了什么?

简单地说就是保存了类加载器所属 ClassSet

+
1
2
3
4
5
6
7
8
@CallerSensitive
protected static boolean registerAsParallelCapable() {
// 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
// 注册调用者的 Class 为具有并行能力
return ParallelLoaders.register(callerClass);
}
+ +

方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

+
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
// private 修饰,只有其外部类 ClassLoader 才可以使用
// static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
private static class ParallelLoaders {
// private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
private ParallelLoaders() {}

// the set of parallel capable loader types
// 使用 loaderTypes 时通过 synchronized 加同步锁
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
// todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 将 ClassLoader 本身注册为具有并行能力
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
// 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
// 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}

/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
+ +
“注册”怎么和锁产生联系?

但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

+
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
private ClassLoader(Void unused, ClassLoader parent) {
// 由 private 修饰,不允许子类重写
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
// 如果类加载器已经注册为具有并行能力,则做一些赋值操作
parallelLockMap = new ConcurrentHashMap<>();
// 保存 package->certs 的 map 映射,相关的工作也可以并行进行
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}

// 由 protect 修饰,允许子类重写,传递了父·类加载器。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

// 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
+ +

ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。

+
为什么注册的代码不能写在构造器方法里?

使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。

+
+

todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

+
+

检查目标类是否已加载

加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?

+
1
2
3
4
5
6
7
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

private native final Class<?> findLoadedClass0(String name);
+ +

保证核心类库的安全性:双亲委派模型

正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

+
+

在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

+
+
为什么要用这套奇怪的机制

这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

+

通常流程如下:

+
    +
  1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
  2. +
  3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
  4. +
  5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  6. +
  7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  8. +
  9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  10. +
  11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  12. +
+

注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

+

查找类资源:findClass

默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

+

过程如下:

+
    +
  1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
  2. +
  3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
  4. +
  5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
  6. +
+
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
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// todo:
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将 name 转换为 path
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 中查找 Resource
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
+ +

查找类的目录列表:URLClassPath

URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
URLClassPath 的核心构造器方法:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
push(urls);
if (factory != null) {
// 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
jarHandler = factory.createURLStreamHandler("jar");
}
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
+ +
URLClassPath#getResource

URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Resource getResource(String name, boolean check) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + name + "\")");
}

Loader loader;
// 获取缓存(默认没有用)
int[] cache = getLookupCache(name);
// 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
Resource res = loader.getResource(name, check);
if (res != null) {
return res;
}
}
return null;
}
+ +
URLClassPath#getNextLoader

获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private synchronized Loader getNextLoader(int[] cache, int index) {
if (closed) {
return null;
}
if (cache != null) {
if (index < cache.length) {
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 获取 Loader
return getLoader(index);
}
}
+ +
URLClassPath#getLoader(int)
    +
  1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
  2. +
  3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
  4. +
  5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
  6. +
+
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
private synchronized Loader getLoader(int index) {
if (closed) {
return null;
}
// Expand URL search path until the request can be satisfied
// or the URL stack is empty.
while (loaders.size() < index + 1) {
// Pop the next URL from the URL stack
// 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
URL url;
synchronized (urls) {
if (urls.empty()) {
return null;
} else {
url = urls.pop();
}
}
// Skip this URL if it already has a Loader. (Loader
// may be null in the case where URL has not been opened
// but is referenced by a JAR index.)
String urlNoFragString = URLUtil.urlNoFragString(url);
if (lmap.containsKey(urlNoFragString)) {
continue;
}
// Otherwise, create a new Loader for the URL.
Loader loader;
try {
// 根据 URL 创建 Loader
loader = getLoader(url);
// If the loader defines a local class path then add the
// URLs to the list of URLs to be opened.
URL[] urls = loader.getClassPath();
if (urls != null) {
push(urls);
}
} catch (IOException e) {
// Silently ignore for now...
continue;
} catch (SecurityException se) {
// Always silently ignore. The context, if there is one, that
// this URLClassPath was given during construction will never
// have permission to access the URL.
if (DEBUG) {
System.err.println("Failed to access " + url + ", " + se );
}
continue;
}
// Finally, add the Loader to the search path.
validateLookupCache(loaders.size(), urlNoFragString);
loaders.add(loader);
lmap.put(urlNoFragString, loader);
}
if (DEBUG_LOOKUP_CACHE) {
System.out.println("NOCACHE: Loading from : " + index );
}
return loaders.get(index);
}
+ +
URLClassPath#getLoader(java.net.URL)

根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

+
    +
  1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
  2. +
  3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
  4. +
  5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
  6. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}
+ +

URLClassPath.FileLoader#getResource

FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

+
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
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}

if (check)
URLClassPath.check(url);

final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}

if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
+ +

ClassLoader 的搜索路径

从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。

+

以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。

+
1
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest
+ +
bootstrap class loader

启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

+
    +
  • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
  • +
  • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
  • +
  • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
  • +
+

IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

+
1
2
3
4
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

进程已结束,退出代码1
+ +
ExtClassLoader

扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

+
AppClassLoader

应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

+

CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

+
    +
  • Windows:.;C:\path\to\classes
  • +
  • Linux:.:/path/to/classes
  • +
+

事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

+

字节数据转换为 Class 实例:defineClass

方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
以下是定义类时最终可能调用的 native 方法。

+
1
2
3
4
5
6
7
8
9
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);

private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
+ +

其方法参数有:

+
    +
  • name,目标类的名称。
  • +
  • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
  • +
  • ProtectionDomain,保护域,todo:
  • +
  • sourceCodeSource 的位置。
  • +
+

defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

+

URLClassLoader#defineClass

此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

+
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
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
// 获取最后一个 . 的位置
int i = name.lastIndexOf('.');
// 返回资源的 CodeSourceURL
URL url = res.getCodeSourceURL();
if (i != -1) {
// 截取包名 com.example
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
// 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
// 不常用
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
return defineClass(name, bb, cs);
} else {
// 以字节数组的形式返回资源数据
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
// 必须再读取字节数据后读取证书,todo:
CodeSigner[] signers = res.getCodeSigners();
// 根据 URL 和签名者创建 CodeSource
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
return defineClass(name, b, 0, b.length, cs);
}
}
+ +

Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public byte[] getBytes() throws IOException {
byte[] b;
// Get stream before content length so that a FileNotFoundException
// can propagate upwards without being caught too early
// 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
// 获取缓存的 InputStream
InputStream in = cachedInputStream();

// This code has been uglified to protect against interrupts.
// Even if a thread has been interrupted when loading resources,
// the IO should not abort, so must carefully retry, failing only
// if the retry leads to some other IO exception.
// 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
// 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
boolean isInterrupted = Thread.interrupted();
int len;
for (;;) {
try {
// 获取内容长度,顺利的话就跳出循环
len = getContentLength();
break;
} catch (InterruptedIOException iioe) {
// 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
}

try {
b = new byte[0];
if (len == -1) len = Integer.MAX_VALUE;
int pos = 0;
while (pos < len) {
int bytesToRead;
if (pos >= b.length) { // Only expand when there's no room
// 如果当前读取位置已经大于等于数组长度
// 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
bytesToRead = Math.min(len - pos, b.length + 1024);
if (b.length < pos + bytesToRead) {
// 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
b = Arrays.copyOf(b, pos + bytesToRead);
}
} else {
// 数组还有空间,待读取字节长度 = 数组剩余空间
bytesToRead = b.length - pos;
}
int cc = 0;
try {
// 读取数据
cc = in.read(b, pos, bytesToRead);
} catch (InterruptedIOException iioe) {
// 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
if (cc < 0) {
// 如果读取返回值 < 0
if (len != Integer.MAX_VALUE) {
// 且长度并未无限,表示提前检测到 EOF,抛出异常
throw new EOFException("Detect premature EOF");
} else {
// 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
if (b.length != pos) {
b = Arrays.copyOf(b, pos);
}
break;
}
}
pos += cc;
}
} finally {
try {
in.close();
} catch (InterruptedIOException iioe) {
isInterrupted = true;
} catch (IOException ignore) {}

if (isInterrupted) {
// 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
Thread.currentThread().interrupt();
}
}
return b;
}
+ +

getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

+
1
2
3
4
5
6
private synchronized InputStream cachedInputStream() throws IOException {
if (cis == null) {
cis = getInputStream();
}
return cis;
}
+ +

在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

+
1
2
3
4
5
6
7
8
9
10
11
public InputStream getInputStream() throws IOException
{
// 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
return new FileInputStream(file);
}

public int getContentLength() throws IOException
{
// 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
return (int)file.length();
};
+ +

SecureClassLoader#defineClass

URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

+
1
2
3
4
5
6
7
8
9
10
11
12
protected final Class<?> defineClass(String name,
byte[] b, int off, int len,
CodeSource cs)
{
return defineClass(name, b, off, len, getProtectionDomain(cs));
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
CodeSource cs)
{
return defineClass(name, b, getProtectionDomain(cs));
}
+ +

方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ProtectionDomain getProtectionDomain(CodeSource cs) {
// 如果 CodeSource 为 null,直接返回 null
if (cs == null)
return null;

ProtectionDomain pd = null;
synchronized (pdcache) {
// 先从 Map 缓存中获取 ProtectionDomain
pd = pdcache.get(cs);
if (pd == null) {
// 从 CodeSource 中获取 PermissionCollection
PermissionCollection perms = getPermissions(cs);
// 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
pd = new ProtectionDomain(cs, perms, this, null);
pdcache.put(cs, pd);
if (debug != null) {
debug.println(" getPermissions "+ pd);
debug.println("");
}
}
}
return pd;
}
+ +
getPermissions

根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

+

AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

+
1
2
3
4
5
6
7
8
9
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 URLClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);
// 允许从类路径加载的任何类退出 VM的权限。
// todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
perms.add(new RuntimePermission("exitVM"));
return perms;
}
+ +

SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

+
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
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 SecureClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);

URL url = codesource.getLocation();

Permission p;
URLConnection urlConnection;

try {
// FileURLConnection 实例
urlConnection = url.openConnection();
// 允许 read 的 FilePermission 实例
p = urlConnection.getPermission();
} catch (java.io.IOException ioe) {
p = null;
urlConnection = null;
}

if (p instanceof FilePermission) {
// if the permission has a separator char on the end,
// it means the codebase is a directory, and we need
// to add an additional permission to read recursively
// 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
String path = p.getName();
if (path.endsWith(File.separator)) {
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
}
} else if ((p == null) && (url.getProtocol().equals("file"))) {
String path = url.getFile().replace('/', File.separatorChar);
path = ParseUtil.decode(path);
if (path.endsWith(File.separator))
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
} else {
/**
* Not loading from a 'file:' URL so we want to give the class
* permission to connect to and accept from the remote host
* after we've made sure the host is the correct one and is valid.
*/
URL locUrl = url;
if (urlConnection instanceof JarURLConnection) {
locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
}
String host = locUrl.getHost();
if (host != null && (host.length() > 0))
p = new SocketPermission(host,
SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
}

// make sure the person that created this class loader
// would have this permission

if (p != null) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final Permission fp = p;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() throws SecurityException {
sm.checkPermission(fp);
return null;
}
}, acc);
}
perms.add(p);
}
return perms;
}
+ +

SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

+
1
2
3
4
5
6
7
8
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
// 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
check();
// ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
return new Permissions(); // ProtectionDomain defers the binding
}
+ +
ProtectionDomain

ProtectionDomain 的相关构造器参数:

+
    +
  • CodeSource
  • +
  • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
  • +
  • ClassLoader
  • +
  • Principal[]
  • +
+

这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

+

ClassLoader#defineClass

抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

+
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
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();

// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}

protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
+ +

主要步骤:

+
    +
  1. preDefineClass 前置处理
  2. +
  3. defineClassX
  4. +
  5. postDefineClass 后置处理
  6. +
+
preDefineClass

确定保护域 ProtectionDomain,并检查:

+
    +
  1. 未定义 java.*
  2. +
  3. 该类的签名者与包(package)中其余类的签名者相匹配
  4. +
+
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
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
// 检查 name 为 null 或者有可能是有效的二进制名称
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 如果 name 以 java. 开头,则抛出异常
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
// 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
pd = defaultDomain;
}

// 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
// todo: certs
if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}
+ +
defineClassSourceLocation

确定 ClassCodeSource 位置。

+
1
2
3
4
5
6
7
8
9
private String defineClassSourceLocation(ProtectionDomain pd)
{
CodeSource cs = pd.getCodeSource();
String source = null;
if (cs != null && cs.getLocation() != null) {
source = cs.getLocation().toString();
}
return source;
}
+ +
defineClassX 方法

这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

+
postDefineClass

在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

+
1
2
3
4
5
6
7
8
9
private void postDefineClass(Class<?> c, ProtectionDomain pd)
{
if (pd.getCodeSource() != null) {
// 获取证书
Certificate certs[] = pd.getCodeSource().getCertificates();
if (certs != null)
setSigners(c, certs);
}
}
+ +
+ + + + + +
+
+ +
+
+
+ + + + + + +
@@ -232,7 +518,7 @@

- + @@ -386,7 +672,7 @@

- + @@ -520,7 +806,7 @@

- + @@ -631,7 +917,7 @@

- + @@ -781,7 +1067,7 @@

- + @@ -921,7 +1207,7 @@

- + @@ -1069,7 +1355,7 @@

- + @@ -1185,7 +1471,7 @@

- + @@ -1306,7 +1592,7 @@

- + @@ -1395,549 +1681,8 @@

- - - -
- - - - - - - -
-

- -

- - -
- - - - -
-

常用命令

从 Docker 官方命令行参考中按顺序选取常用的命令和用法。

-

docker attach

将本地标准输入、输出和错误流附加到一个正在运行的容器。

-

用法

1
docker attach [OPTIONS] CONTAINER
- -

描述

使用 docker attach 通过容器 ID 或名称将终端的标准输入、输出和错误附加到正在运行的容器。这允许你查看其正在进行的输出或以交互方式控制它,就好像命令直接在你的终端中运行一样。

-

如果 docker run 同时指定 -it 选项,使用 CTRL-p CTRL-q 退出 docker attach 时,不影响容器继续运行。

-

选项

- - - - - - - - - - - - - - - - - -
名称和缩写默认值描述
--detach-keys覆盖用于从容器分离的键序列
--sig-proxytrue代理转发所有收到的信号给进程
-

docker exec

在正在运行中的容器里执行命令。

-

用法

1
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
- -

选项

- - - - - - - - - - - - - - - - - - - - - - - - - - -
名称和缩写描述
--detach,-d分离模式:在后台运行命令
--env,-e设置环境变量
--interactive,-i即使未附加也保持 STDIN 打开
--tty,-t分配伪 TTY
--workdir,-w容器内的工作目录
-

docker images

列出镜像。

-

用法

1
docker images [OPTIONS] [REPOSITORY[:TAG]]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--all,-a展示所有镜像
-

docker info

展示系统范围的信息。

-

用法

1
docker info [OPTIONS]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--format,-f使用指定模板格式化输出
-

docker inspect

返回有关 Docker 对象的低级信息。

-

用法

1
docker inspect [OPTIONS] NAME|ID [NAME|ID...]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--format,-f使用指定模板格式化输出
-

docker logs

获取容器的日志。

-

用法

1
docker logs [OPTIONS] CONTAINER
- -

选项

- - - - - - - - - - - - - - - - - - - - - - - - - - -
名称和缩写描述
--follow,-f按照日志输出
--since显示指定时间戳(2013-01-02T13:23:37Z)或相对时间(42m)后的日志
--tail,-n从日志末尾开始显示的行数
--timestamps,-t显示时间戳
--until显示指定时间戳(2013-01-02T13:23:37Z)或相对时间(42m)前的日志
-

docker network

管理网络。

-

用法

1
docker network COMMAND
- -

docker network connect

将容器连接到网络。

-
用法
1
docker network connect [OPTIONS] NETWORK CONTAINER
- -

docker network create

创建网络。

-
用法
1
docker network create [OPTIONS] NETWORK
- -
选项
- - - - - - - - - - - - -
名称和缩写默认值描述
--driver,-dbridge按照日志输出
-

docker network disconnect

断开容器与网络的连接。

-
用法
1
docker network create [OPTIONS] NETWORK
- -

docker network inspect

显示一个或多个网络的详细信息。

-
用法
1
docker network inspect [OPTIONS] NETWORK [NETWORK...]
- -

docker network ls

列出网络。

-
用法
1
docker network ls [OPTIONS]
- -
选项
- - - - - - - - - - - - -
名称和缩写默认值描述
--filter,-f提供过滤值(例如 driver=bridge)
-

docker network rm

删除一个或多个网络。

-
用法
1
docker network rm NETWORK [NETWORK...]
- -
选项
- - - - - - - - - - - - -
名称和缩写默认值描述
--filter,-f提供过滤值(例如 driver=bridge)
-

docker port

列出容器的端口映射或特定映射。

-

用法

1
docker port CONTAINER [PRIVATE_PORT[/PROTO]]
- -

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b650456536c7 busybox:latest top 54 minutes ago Up 54 minutes 0.0.0.0:1234->9876/tcp, 0.0.0.0:4321->7890/tcp test
docker port test

7890/tcp -> 0.0.0.0:4321
9876/tcp -> 0.0.0.0:1234
docker port test 7890/tcp

0.0.0.0:4321
docker port test 7890/udp

2014/06/24 11:53:36 Error: No public port '7890/udp' published for test
docker port test 7890

0.0.0.0:4321
- - -

docker ps

列出容器。

-

用法

1
docker ps [OPTIONS]
- -

选项

- - - - - - - - - - - - - - -
名称和缩写描述
--all,-a显示所有容器(默认只显示运行中的容器)
--filter,-f根据提供的条件过滤输出
-

docker pull

从仓库下载镜像。

-

用法

1
docker pull [OPTIONS] NAME[:TAG|@DIGEST]
- - -

docker rename

重命名容器。

-

用法

1
docker rename CONTAINER NEW_NAME
- - -

docker restart

重启一个或多个容器。

-

用法

1
docker restart [OPTIONS] CONTAINER [CONTAINER...]
- - -

docker rm

移除一个或多个容器。

-

用法

1
docker rm [OPTIONS] CONTAINER [CONTAINER...]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--force,-f强制删除正在运行的容器(使用 SIGKILL)
-

docker rmi

删除一个或多个镜像。

-

用法

1
docker rmi [OPTIONS] IMAGE [IMAGE...]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--force,-f强制删除镜像(当其正在被容器使用中)
-

docker run

从镜像创建并运行新容器。

-

用法

1
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
- -

选项

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
名称和缩写默认值描述
--attach,-a附加到 STDIN、STDOUT 或 STDERR
--detach,-d在后台运行容器并打印容器 ID
--env,-e设置环境变量
--expose公开一个端口或一系列端口
--interactive,-i即使未附加,也要保持 STDIN 打开
--name为容器命名
--network将容器连接到网络
--publish,-p将容器的端口发布到主机
--restartno容器退出时应用的重启策略
--tty,-t分配伪 TTY
--volume,-v绑定挂载卷
-

在 Docker Hub 中搜索镜像。

-

用法

1
docker search [OPTIONS] TERM
- -

选项

- - - - - - - - - - - - - - -
名称和缩写描述
--filter,-f根据提供的条件过滤输出
--limit最大搜索结果数量
-

docker start

启动一个或多个容器。

-

用法

1
docker start [OPTIONS] CONTAINER [CONTAINER...]
- -

选项

- - - - - - - - - - - - - - -
名称和缩写描述
--attach,-a附加 STDOUT/STDERR 和转发信号
--interactive,-i附加容器的 STDIN
-

docker stats

显示容器资源使用统计的实时流。

-

用法

1
docker stats [OPTIONS] [CONTAINER...]
- -

选项

- - - - - - - - - - -
名称和缩写描述
--all,-a显示所有容器(默认只显示运行中的容器)
-

例子

1
2
3
4
5
docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
3d2180bad26e zoo1 0.05% 34.98MiB / 15.59GiB 0.22% 38.6MB / 0B 5.39MB / 656MB 39
828987756ee6 zoo3 0.04% 80.3MiB / 15.59GiB 0.50% 38.6MB / 0B 20.9MB / 522MB 32
d6e23f304046 zoo2 0.04% 78.84MiB / 15.59GiB 0.49% 38.6MB / 0B 19.8MB / 883MB 32
- - -

docker stop

停止一个或多个正在运行的容器。

-

用法

1
docker stop [OPTIONS] CONTAINER [CONTAINER...]
- - -

docker top

显示容器中的运行进程。

-

用法

1
docker top CONTAINER [ps OPTIONS]
- -

例子

1
2
3
4
5
6
7
8
9
10
docker top jellyfin
UID PID PPID C STIME TTY TIME CMD
root 652954 652845 0 6月16 ? 00:00:00 /package/admin/s6/command/s6-svscan -d4 -- /run/service
root 653039 652954 0 6月16 ? 00:00:00 s6-supervise s6-linux-init-shutdownd
root 653040 653039 0 6月16 ? 00:00:00 /package/admin/s6-linux-init/command/s6-linux-init-shutdownd -c /run/s6/basedir -g 3000 -C -B
root 653071 652954 0 6月16 ? 00:00:00 s6-supervise s6rc-fdholder
root 653072 652954 0 6月16 ? 00:00:00 s6-supervise svc-jellyfin
root 653073 652954 0 6月16 ? 00:00:00 s6-supervise s6rc-oneshot-runner
root 653081 653073 0 6月16 ? 00:00:00 /package/admin/s6/command/s6-ipcserverd -1 -- /package/admin/s6/command/s6-ipcserver-access -v0 -E -l0 -i data/rules -- /package/admin/s6/command/s6-sudod -t 30000 -- /package/admin/s6-rc/command/s6-rc-oneshot-run -l ../.. --
user 653197 653072 0 6月16 ? 00:11:38 /usr/bin/jellyfin --ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg
- - -

docker version

显示 Docker 版本信息。

-

用法

1
docker version [OPTIONS]
- - -

docker volume

管理卷。

-

用法

1
docker volume COMMAND
- -

docker volume create

创建卷。

-
用法
1
docker volume create [OPTIONS] [VOLUME]
- -
选项
- - - - - - - - - - - - -
名称和缩写默认值描述
--driver,-dlocal指定卷驱动程序名称
-

docker volume inspect

显示一个或多个卷的详细信息。

-
用法
1
docker volume inspect [OPTIONS] VOLUME [VOLUME...]
- -

docker volume ls

列出卷。

-
用法
1
docker volume ls [OPTIONS]
- -
选项
- - - - - - - - - - - - -
名称和缩写默认值描述
--filter,-flocal提供过滤值(例如 dangling=true)
-

docker volume rm

删除一个或多个卷。

-
用法
1
docker volume rm [OPTIONS] VOLUME [VOLUME...]
- - -

参考链接

Docker 命令行参考
深入探究docker attach的退出方式

- - -
- - - - - -
-
- -
-
-

- - - -
@@ -1960,13 +1705,13 @@

- 42k + 45k

Hexo & NexT.Muse 强力驱动 diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 00000000..65d13c00 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,792 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Moralok + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + +
+ + 0% +
+ + + + +
+ + + + +
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

常用命令

从 Docker 官方命令行参考中按顺序选取常用的命令和用法。

+

docker attach

将本地标准输入、输出和错误流附加到一个正在运行的容器。

+

用法

1
docker attach [OPTIONS] CONTAINER
+ +

描述

使用 docker attach 通过容器 ID 或名称将终端的标准输入、输出和错误附加到正在运行的容器。这允许你查看其正在进行的输出或以交互方式控制它,就好像命令直接在你的终端中运行一样。

+

如果 docker run 同时指定 -it 选项,使用 CTRL-p CTRL-q 退出 docker attach 时,不影响容器继续运行。

+

选项

+ + + + + + + + + + + + + + + + + +
名称和缩写默认值描述
--detach-keys覆盖用于从容器分离的键序列
--sig-proxytrue代理转发所有收到的信号给进程
+

docker exec

在正在运行中的容器里执行命令。

+

用法

1
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
+ +

选项

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
名称和缩写描述
--detach,-d分离模式:在后台运行命令
--env,-e设置环境变量
--interactive,-i即使未附加也保持 STDIN 打开
--tty,-t分配伪 TTY
--workdir,-w容器内的工作目录
+

docker images

列出镜像。

+

用法

1
docker images [OPTIONS] [REPOSITORY[:TAG]]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--all,-a展示所有镜像
+

docker info

展示系统范围的信息。

+

用法

1
docker info [OPTIONS]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--format,-f使用指定模板格式化输出
+

docker inspect

返回有关 Docker 对象的低级信息。

+

用法

1
docker inspect [OPTIONS] NAME|ID [NAME|ID...]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--format,-f使用指定模板格式化输出
+

docker logs

获取容器的日志。

+

用法

1
docker logs [OPTIONS] CONTAINER
+ +

选项

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
名称和缩写描述
--follow,-f按照日志输出
--since显示指定时间戳(2013-01-02T13:23:37Z)或相对时间(42m)后的日志
--tail,-n从日志末尾开始显示的行数
--timestamps,-t显示时间戳
--until显示指定时间戳(2013-01-02T13:23:37Z)或相对时间(42m)前的日志
+

docker network

管理网络。

+

用法

1
docker network COMMAND
+ +

docker network connect

将容器连接到网络。

+
用法
1
docker network connect [OPTIONS] NETWORK CONTAINER
+ +

docker network create

创建网络。

+
用法
1
docker network create [OPTIONS] NETWORK
+ +
选项
+ + + + + + + + + + + + +
名称和缩写默认值描述
--driver,-dbridge按照日志输出
+

docker network disconnect

断开容器与网络的连接。

+
用法
1
docker network create [OPTIONS] NETWORK
+ +

docker network inspect

显示一个或多个网络的详细信息。

+
用法
1
docker network inspect [OPTIONS] NETWORK [NETWORK...]
+ +

docker network ls

列出网络。

+
用法
1
docker network ls [OPTIONS]
+ +
选项
+ + + + + + + + + + + + +
名称和缩写默认值描述
--filter,-f提供过滤值(例如 driver=bridge)
+

docker network rm

删除一个或多个网络。

+
用法
1
docker network rm NETWORK [NETWORK...]
+ +
选项
+ + + + + + + + + + + + +
名称和缩写默认值描述
--filter,-f提供过滤值(例如 driver=bridge)
+

docker port

列出容器的端口映射或特定映射。

+

用法

1
docker port CONTAINER [PRIVATE_PORT[/PROTO]]
+ +

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b650456536c7 busybox:latest top 54 minutes ago Up 54 minutes 0.0.0.0:1234->9876/tcp, 0.0.0.0:4321->7890/tcp test
docker port test

7890/tcp -> 0.0.0.0:4321
9876/tcp -> 0.0.0.0:1234
docker port test 7890/tcp

0.0.0.0:4321
docker port test 7890/udp

2014/06/24 11:53:36 Error: No public port '7890/udp' published for test
docker port test 7890

0.0.0.0:4321
+ + +

docker ps

列出容器。

+

用法

1
docker ps [OPTIONS]
+ +

选项

+ + + + + + + + + + + + + + +
名称和缩写描述
--all,-a显示所有容器(默认只显示运行中的容器)
--filter,-f根据提供的条件过滤输出
+

docker pull

从仓库下载镜像。

+

用法

1
docker pull [OPTIONS] NAME[:TAG|@DIGEST]
+ + +

docker rename

重命名容器。

+

用法

1
docker rename CONTAINER NEW_NAME
+ + +

docker restart

重启一个或多个容器。

+

用法

1
docker restart [OPTIONS] CONTAINER [CONTAINER...]
+ + +

docker rm

移除一个或多个容器。

+

用法

1
docker rm [OPTIONS] CONTAINER [CONTAINER...]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--force,-f强制删除正在运行的容器(使用 SIGKILL)
+

docker rmi

删除一个或多个镜像。

+

用法

1
docker rmi [OPTIONS] IMAGE [IMAGE...]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--force,-f强制删除镜像(当其正在被容器使用中)
+

docker run

从镜像创建并运行新容器。

+

用法

1
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
+ +

选项

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称和缩写默认值描述
--attach,-a附加到 STDIN、STDOUT 或 STDERR
--detach,-d在后台运行容器并打印容器 ID
--env,-e设置环境变量
--expose公开一个端口或一系列端口
--interactive,-i即使未附加,也要保持 STDIN 打开
--name为容器命名
--network将容器连接到网络
--publish,-p将容器的端口发布到主机
--restartno容器退出时应用的重启策略
--tty,-t分配伪 TTY
--volume,-v绑定挂载卷
+

在 Docker Hub 中搜索镜像。

+

用法

1
docker search [OPTIONS] TERM
+ +

选项

+ + + + + + + + + + + + + + +
名称和缩写描述
--filter,-f根据提供的条件过滤输出
--limit最大搜索结果数量
+

docker start

启动一个或多个容器。

+

用法

1
docker start [OPTIONS] CONTAINER [CONTAINER...]
+ +

选项

+ + + + + + + + + + + + + + +
名称和缩写描述
--attach,-a附加 STDOUT/STDERR 和转发信号
--interactive,-i附加容器的 STDIN
+

docker stats

显示容器资源使用统计的实时流。

+

用法

1
docker stats [OPTIONS] [CONTAINER...]
+ +

选项

+ + + + + + + + + + +
名称和缩写描述
--all,-a显示所有容器(默认只显示运行中的容器)
+

例子

1
2
3
4
5
docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
3d2180bad26e zoo1 0.05% 34.98MiB / 15.59GiB 0.22% 38.6MB / 0B 5.39MB / 656MB 39
828987756ee6 zoo3 0.04% 80.3MiB / 15.59GiB 0.50% 38.6MB / 0B 20.9MB / 522MB 32
d6e23f304046 zoo2 0.04% 78.84MiB / 15.59GiB 0.49% 38.6MB / 0B 19.8MB / 883MB 32
+ + +

docker stop

停止一个或多个正在运行的容器。

+

用法

1
docker stop [OPTIONS] CONTAINER [CONTAINER...]
+ + +

docker top

显示容器中的运行进程。

+

用法

1
docker top CONTAINER [ps OPTIONS]
+ +

例子

1
2
3
4
5
6
7
8
9
10
docker top jellyfin
UID PID PPID C STIME TTY TIME CMD
root 652954 652845 0 6月16 ? 00:00:00 /package/admin/s6/command/s6-svscan -d4 -- /run/service
root 653039 652954 0 6月16 ? 00:00:00 s6-supervise s6-linux-init-shutdownd
root 653040 653039 0 6月16 ? 00:00:00 /package/admin/s6-linux-init/command/s6-linux-init-shutdownd -c /run/s6/basedir -g 3000 -C -B
root 653071 652954 0 6月16 ? 00:00:00 s6-supervise s6rc-fdholder
root 653072 652954 0 6月16 ? 00:00:00 s6-supervise svc-jellyfin
root 653073 652954 0 6月16 ? 00:00:00 s6-supervise s6rc-oneshot-runner
root 653081 653073 0 6月16 ? 00:00:00 /package/admin/s6/command/s6-ipcserverd -1 -- /package/admin/s6/command/s6-ipcserver-access -v0 -E -l0 -i data/rules -- /package/admin/s6/command/s6-sudod -t 30000 -- /package/admin/s6-rc/command/s6-rc-oneshot-run -l ../.. --
user 653197 653072 0 6月16 ? 00:11:38 /usr/bin/jellyfin --ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg
+ + +

docker version

显示 Docker 版本信息。

+

用法

1
docker version [OPTIONS]
+ + +

docker volume

管理卷。

+

用法

1
docker volume COMMAND
+ +

docker volume create

创建卷。

+
用法
1
docker volume create [OPTIONS] [VOLUME]
+ +
选项
+ + + + + + + + + + + + +
名称和缩写默认值描述
--driver,-dlocal指定卷驱动程序名称
+

docker volume inspect

显示一个或多个卷的详细信息。

+
用法
1
docker volume inspect [OPTIONS] VOLUME [VOLUME...]
+ +

docker volume ls

列出卷。

+
用法
1
docker volume ls [OPTIONS]
+ +
选项
+ + + + + + + + + + + + +
名称和缩写默认值描述
--filter,-flocal提供过滤值(例如 dangling=true)
+

docker volume rm

删除一个或多个卷。

+
用法
1
docker volume rm [OPTIONS] VOLUME [VOLUME...]
+ + +

参考链接

Docker 命令行参考
深入探究docker attach的退出方式

+ + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/search.xml b/search.xml index ee0a3576..1a6e3e62 100644 --- a/search.xml +++ b/search.xml @@ -1,5 +1,50 @@ + + 在 Ubuntu 上安装 Clash + /2023/05/27/how-to-install-clash-on-ubuntu/ + 安装 Clash

GitHub 下载预构建的二进制文件。

+
:/mnt/hgfs/share$ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
+ +

使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。

+
:/mnt/hgfs/share$ gzip -d clash-linux-amd64-v1.16.0.gz 
gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
+

忽略提示。

+

移动二进制文件到目录 /usr/local/bin 并且重命名为 clash。

+
:/mnt/hgfs/share$ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
[sudo] password for wrmao:
+ +

现在可以通过 clash -v 查看 Clash 的版本信息。

+
:~$ clash -v
Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
+ +

使用 Clash

使用命令 clash 启动,可以看到日志。

+
:~$ clash
INFO[0000] Can't find config, create a initial config file
INFO[0000] Can't find MMDB, start download
INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
+ +

在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。

+
:~/.config/clash$ ls
cache.db config.yaml Country.mmdb
+

其中 config.yaml 是 Clash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。
config.yaml 的内容直接从已有的配置文件复制过来。

+

可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。

+

设置 Clash 为后台服务

参考官方文档 Clash as a service

+

拷贝配置文件到 /etc/clash

+
:~/.config/clash$ sudo cp config.yaml /etc/clash/
:~/.config/clash$ sudo cp Country.mmdb /etc/clash/
+ +

/etc/systemd/system/clash.service 创建 systemd 配置文件:

+
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target
+ +

之后重载 systemd:

+
systemctl daemon-reload
+ +

设置系统开机时启动 clashd:

+
systemctl enable clash
+ +

马上启动 clashd:

+
systemctl start clash
+ +

检查 Clash 的健康状态和日志:

+
systemctl status clash
journalctl -xe
]]> + + clash + proxy + + Docker 常用命令列表 /2020/08/19/docker-frequently-used-commands/ @@ -479,51 +524,6 @@ openvpn - - 在 Ubuntu 上安装 Clash - /2023/05/27/how-to-install-clash-on-ubuntu/ - 安装 Clash

GitHub 下载预构建的二进制文件。

-
:/mnt/hgfs/share$ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
- -

使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。

-
:/mnt/hgfs/share$ gzip -d clash-linux-amd64-v1.16.0.gz 
gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
-

忽略提示。

-

移动二进制文件到目录 /usr/local/bin 并且重命名为 clash。

-
:/mnt/hgfs/share$ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
[sudo] password for wrmao:
- -

现在可以通过 clash -v 查看 Clash 的版本信息。

-
:~$ clash -v
Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
- -

使用 Clash

使用命令 clash 启动,可以看到日志。

-
:~$ clash
INFO[0000] Can't find config, create a initial config file
INFO[0000] Can't find MMDB, start download
INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
- -

在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。

-
:~/.config/clash$ ls
cache.db config.yaml Country.mmdb
-

其中 config.yaml 是 Clash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。
config.yaml 的内容直接从已有的配置文件复制过来。

-

可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。

-

设置 Clash 为后台服务

参考官方文档 Clash as a service

-

拷贝配置文件到 /etc/clash

-
:~/.config/clash$ sudo cp config.yaml /etc/clash/
:~/.config/clash$ sudo cp Country.mmdb /etc/clash/
- -

/etc/systemd/system/clash.service 创建 systemd 配置文件:

-
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target
- -

之后重载 systemd:

-
systemctl daemon-reload
- -

设置系统开机时启动 clashd:

-
systemctl enable clash
- -

马上启动 clashd:

-
systemctl start clash
- -

检查 Clash 的健康状态和日志:

-
systemctl status clash
journalctl -xe
]]>
- - clash - proxy - -
在 Windows 10 上安装 OpenVPN 服务器 /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ @@ -663,8 +663,8 @@

参考链接

Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置

]]> - docker proxy + docker
@@ -933,91 +933,289 @@ - JVM GC 的测试和分析 - /2023/11/01/testing-and-analysis-of-jvm-gc/ - 堆的组成
public class JvmGcTest_1 {

public static final int _512KB = 512 * 1024;
public static final int _1MB = 1024 * 1024;
public static final int _6MB = 6 * 1024 * 1024;
public static final int _7MB = 7 * 1024 * 1024;
public static final int _8MB = 8 * 1024 * 1024;

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// -XX:+UseSerialGC 避免幸存区比例动态调整
public static void main(String[] args) {

}
}
+ Java 类加载器源码分析 + /2023/07/13/Java-class-loader-source-code-analysis/ + 组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

+
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
-
Heap
def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf68c8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3288K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
+

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

+
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
-

根据打印的信息,组成如下:

-
    -
  • Heap: 堆。
      -
    • def new generation: 新生代。
    • -
    • tenured generation: 老年代。
    • -
    • Metaspace: 元空间,实际上并不属于堆, -XX:+PrintGCDetails 将它的信息一起输出。
    • -
    -
  • -
-

堆空间的比例

新生代中的空间占比 eden:from:to 在默认情况下是 8:1:1,与观察到的数据 8192K:1024K:1024K 一致。
新生代的空间 eden + from + to 为 10240K,符合 -Xmn10M 设置的大小。
total 显示为 9216K,即 eden + from 的大小,是因为 to 的空间不计算在内。新生代可用的空间只有 eden + fromto 空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden 中已用 2010K,约占 eden 空间的 24%。

-

从内存地址分析堆空间

内存地址为 16 位的 16 进制的数字,64 位机器。
[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000),老年代 [0x00000000ff600000, 0x0000000100000000),计算可得空间大小均为 10MB。
eden 中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8),空间大小为 2058440 byte,约等于 2010K。

-

显而易见,新生代和老生代是一片完全连续的地址空间。

-

堆的垃圾回收

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
+

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

+
    +
  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. +
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. +
  5. 调用 findClass(String) 方法来查找该类。
  6. +
+

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

+
+

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

+
+

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

+
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
-
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
def new generation total 9216K, used 8135K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
+
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

+
static {
ClassLoader.registerAsParallelCapable();
}
-

Allocation Failure,正常情况下,新对象总是分配在 Eden,分配空间失败,eden 的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
[DefNew: 2013K->721K(9216K), 0.0105099 secs],新生代在垃圾回收前后空间的占用变化和耗时。
2013K->721K(19456K), 0.0105455 secs,整个堆在垃圾回收前后空间的占用变化和耗时。

-

GC 类型

    -
  • GC: minor GC。
  • -
  • Fulle GC: full GC。
  • -
-

from 和 to 的角色变换

from 的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0),空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden 区域存活的对象全部转移到了原 to 空间,fromto 空间的角色相互转换(从地址空间的信息可以看到此时 to 的地址指针比 from 的地址指针小)。
eden 的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0),空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden 区域除了 byte[] 对象外,还存储了其他对象,比如为了创建 List<byte[]> 对象而新加载的类对象。

-

eden 空间足够时不发生 GC

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
}
+

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

+
    +
  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. +
  3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
  4. +
+
怎么保证这两个条件成立呢?
    +
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. +
  3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
  4. +
+

你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

+
注册工作做了什么?

简单地说就是保存了类加载器所属 ClassSet

+
@CallerSensitive
protected static boolean registerAsParallelCapable() {
// 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
// 注册调用者的 Class 为具有并行能力
return ParallelLoaders.register(callerClass);
}
-
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
def new generation total 9216K, used 8647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 96% used [0x00000000fec00000, 0x00000000ff3bd8d0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
+

方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

+
// private 修饰,只有其外部类 ClassLoader 才可以使用
// static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
private static class ParallelLoaders {
// private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
private ParallelLoaders() {}

// the set of parallel capable loader types
// 使用 loaderTypes 时通过 synchronized 加同步锁
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
// todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 将 ClassLoader 本身注册为具有并行能力
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
// 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
// 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}

/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
-

由于 eden 区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。
eden 区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0),空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16。显然第二次添加时,不再因为创建 List<byte[]> 而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh

-

新生代空间不足,部分对象提前晋升到老年代

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
+
“注册”怎么和锁产生联系?

但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

+
private ClassLoader(Void unused, ClassLoader parent) {
// 由 private 修饰,不允许子类重写
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
// 如果类加载器已经注册为具有并行能力,则做一些赋值操作
parallelLockMap = new ConcurrentHashMap<>();
// 保存 package->certs 的 map 映射,相关的工作也可以并行进行
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}

// 由 protect 修饰,允许子类重写,传递了父·类加载器。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

// 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
-
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8565K->512K(9216K), 0.0046378 secs] 8565K->8396K(19456K), 0.0046540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 1350K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd1a20, 0x00000000ff400000)
from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7884K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb33a0, 0x00000000ffdb3400, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
+

ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。

+
为什么注册的代码不能写在构造器方法里?

使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。

+
+

todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

+
+

检查目标类是否已加载

加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?

+
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

private native final Class<?> findLoadedClass0(String name);
-

在第三次添加时,由于 eden 空间不足,因此又发生了第二次垃圾回收。
[DefNew: 8565K->512K(9216K), 0.0046378 secs],新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
在第二次添加完成后,eden [0x00000000fec00000, 0x00000000ff3bd8d0)from [0x00000000ff500000, 0x00000000ff5b45f0) 占用的空间为 8116432 + 738800 = 8855232 约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。
8565K->8396K(19456K), 0.0046540 secs,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。
eden 中加入了第三次添加时的对象,大于 512K 不少。
此时 edenfromtenured 中均有不好确认成分的空间占用,比如 from 中多了 56 字节。

-

新生代空间不足,大对象直接在老年代创建

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
+

保证核心类库的安全性:双亲委派模型

正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

+
+

在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

+
+
为什么要用这套奇怪的机制

这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

+

通常流程如下:

+
    +
  1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
  2. +
  3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
  4. +
  5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  6. +
  7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  8. +
  9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  10. +
  11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  12. +
+

注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

+

查找类资源:findClass

默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

+

过程如下:

+
    +
  1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
  2. +
  3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
  4. +
  5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
  6. +
+
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// todo:
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将 name 转换为 path
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 中查找 Resource
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
-
Heap
def new generation total 9216K, used 2177K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee20730, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3353K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
+

查找类的目录列表:URLClassPath

URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
URLClassPath 的核心构造器方法:

+
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
push(urls);
if (factory != null) {
// 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
jarHandler = factory.createURLStreamHandler("jar");
}
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
-

在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。

-

内存不足 OOM

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
+
URLClassPath#getResource

URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

+
public Resource getResource(String name, boolean check) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + name + "\")");
}

Loader loader;
// 获取缓存(默认没有用)
int[] cache = getLookupCache(name);
// 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
Resource res = loader.getResource(name, check);
if (res != null) {
return res;
}
}
return null;
}
-
waiting...
[GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)

Heap
def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
class space used 522K, capacity 558K, committed 640K, reserved 1048576K
+
URLClassPath#getNextLoader

获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

+
private synchronized Loader getNextLoader(int[] cache, int index) {
if (closed) {
return null;
}
if (cache != null) {
if (index < cache.length) {
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 获取 Loader
return getLoader(index);
}
}
-

当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError

-

线程中发生内存不足,不会影响其他线程

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();

new Thread(() -> {
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();

System.out.println("waiting...");
try {
System.in.read();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
+
URLClassPath#getLoader(int)
    +
  1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
  2. +
  3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
  4. +
  5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
  6. +
+
private synchronized Loader getLoader(int index) {
if (closed) {
return null;
}
// Expand URL search path until the request can be satisfied
// or the URL stack is empty.
while (loaders.size() < index + 1) {
// Pop the next URL from the URL stack
// 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
URL url;
synchronized (urls) {
if (urls.empty()) {
return null;
} else {
url = urls.pop();
}
}
// Skip this URL if it already has a Loader. (Loader
// may be null in the case where URL has not been opened
// but is referenced by a JAR index.)
String urlNoFragString = URLUtil.urlNoFragString(url);
if (lmap.containsKey(urlNoFragString)) {
continue;
}
// Otherwise, create a new Loader for the URL.
Loader loader;
try {
// 根据 URL 创建 Loader
loader = getLoader(url);
// If the loader defines a local class path then add the
// URLs to the list of URLs to be opened.
URL[] urls = loader.getClassPath();
if (urls != null) {
push(urls);
}
} catch (IOException e) {
// Silently ignore for now...
continue;
} catch (SecurityException se) {
// Always silently ignore. The context, if there is one, that
// this URLClassPath was given during construction will never
// have permission to access the URL.
if (DEBUG) {
System.err.println("Failed to access " + url + ", " + se );
}
continue;
}
// Finally, add the Loader to the search path.
validateLookupCache(loaders.size(), urlNoFragString);
loaders.add(loader);
lmap.put(urlNoFragString, loader);
}
if (DEBUG_LOOKUP_CACHE) {
System.out.println("NOCACHE: Loading from : " + index );
}
return loaders.get(index);
}
-
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 8912K->8895K(10240K), 0.0011880 secs] 8912K->8895K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0012009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8895K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffeafce0, 0x00000000ffeafe00, 0x0000000100000000)
Metaspace used 3380K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 363K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.main(JvmGcTest.java:21)
+
URLClassPath#getLoader(java.net.URL)

根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

+
    +
  1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
  2. +
  3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
  4. +
  5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
  6. +
+
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}
-

Thread-0 发生 OutOfMemoryError 后,main 线程仍然正常运行。

-

大对象的划分指标

当创建的大对象 + 对象头的容量小于等于 eden,如果 GC 后的存活对象可以放入 to,那么还是会先在 eden 中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。

-
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB - 16]);
}
+

URLClassPath.FileLoader#getResource

FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

+
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}

if (check)
URLClassPath.check(url);

final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}

if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
-
[GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
Heap
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
+

ClassLoader 的搜索路径

从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。

+

以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。

+
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest
-

尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。

-

注意事项

    -
  • 正常情况下,新对象都是在 eden 中创建。
  • -
  • 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。
  • -
  • 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。
  • -
  • 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。
  • -
  • 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。
  • +
    bootstrap class loader

    启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

    +
      +
    • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
    • +
    • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
    • +
    • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
    -

    尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

    -]]> - - java - jvm - - - - 字符串常量池的测试和分析 - /2023/11/03/testing-and-analysis-of-StringTable/ - 如果你准备过 Java 的面试,应该看到过一个问题:“String s1 = new String("abc"); 这个语句创建了几个字符串对象”。这个问题曾经困扰我,当时的我不能理解这个问题想要考察的是什么?
    答案中或许提及了字符串常量池,但是如果细究起来,会发现答案并不完善,有些令人困惑,甚至问题本身就有一定的误导作用。它很容易让初学者以为创建一个字符串对象和创建一个其他类型的对象在过程上是有一些区别的。
    其实关键的地方在于 “abc” 而不是 new String("abc")

    -

    字符串常量池的作用

    字符串字面量

    字面量(literal)是用于表达源代码中的一个固定值的表示法(notion),比如代码中的整数、浮点数、字符串。简而言之,字符串字面量就是双引号包裹的字符串,例如:

    -
    String s1 = "a";
    +

    IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

    +
    Error occurred during initialization of VM
    java/lang/NoClassDefFoundError: java/lang/Object

    进程已结束,退出代码1
    -

    在 Java 中,字符串对象就是一个 String 类型的对象,因此在程序运行时,String 类型的变量 s1 指向的一定是一个 String 对象。字面量 “a” 在某一个时刻,没有经过 new 关键字,变成了一个 String 对象

    -

    接下来我们来思考一个问题,程序中每一个字符串字面量都要对应着生成一个单独的 String 对象吗?考虑到 Java 中 String 对象是不可变的,显然相同的字符串字面量完全可以共用一个 String 对象从而避免重复创建对象。JVM 也是这样设计的,这些可以共用的 String 对象组成了一个字符串常量池。

    -
      -
    1. 第一次遇到某一个字符串字面量时,会在字符串常量池中创建一个 String 对象,以后遇到相同的字符串字面量,就复用该对象,不再重复创建。
    2. -
    3. 每一次 new 都会创建一个新的 String 对象。
    4. +
      ExtClassLoader

      扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

      +
      AppClassLoader

      应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

      +

      CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

      +
        +
      • Windows:.;C:\path\to\classes
      • +
      • Linux:.:/path/to/classes
      • +
      +

      事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

      +

      字节数据转换为 Class 实例:defineClass

      方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
      以下是定义类时最终可能调用的 native 方法。

      +
      private native Class<?> defineClass0(String name, byte[] b, int off, int len,
      ProtectionDomain pd);

      private native Class<?> defineClass1(String name, byte[] b, int off, int len,
      ProtectionDomain pd, String source);

      private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
      int off, int len, ProtectionDomain pd,
      String source);
      + +

      其方法参数有:

      +
        +
      • name,目标类的名称。
      • +
      • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
      • +
      • ProtectionDomain,保护域,todo:
      • +
      • sourceCodeSource 的位置。
      • +
      +

      defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

      +

      URLClassLoader#defineClass

      此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

      +
      private Class<?> defineClass(String name, Resource res) throws IOException {
      long t0 = System.nanoTime();
      // 获取最后一个 . 的位置
      int i = name.lastIndexOf('.');
      // 返回资源的 CodeSourceURL
      URL url = res.getCodeSourceURL();
      if (i != -1) {
      // 截取包名 com.example
      String pkgname = name.substring(0, i);
      // Check if package already loaded.
      Manifest man = res.getManifest();
      definePackageInternal(pkgname, man, url);
      }
      // Now read the class bytes and define the class
      // 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
      java.nio.ByteBuffer bb = res.getByteBuffer();
      if (bb != null) {
      // Use (direct) ByteBuffer:
      // 不常用
      CodeSigner[] signers = res.getCodeSigners();
      CodeSource cs = new CodeSource(url, signers);
      sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
      // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
      return defineClass(name, bb, cs);
      } else {
      // 以字节数组的形式返回资源数据
      byte[] b = res.getBytes();
      // must read certificates AFTER reading bytes.
      // 必须再读取字节数据后读取证书,todo:
      CodeSigner[] signers = res.getCodeSigners();
      // 根据 URL 和签名者创建 CodeSource
      CodeSource cs = new CodeSource(url, signers);
      sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
      // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
      return defineClass(name, b, 0, b.length, cs);
      }
      }
      + +

      Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

      +
      public byte[] getBytes() throws IOException {
      byte[] b;
      // Get stream before content length so that a FileNotFoundException
      // can propagate upwards without being caught too early
      // 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
      // 获取缓存的 InputStream
      InputStream in = cachedInputStream();

      // This code has been uglified to protect against interrupts.
      // Even if a thread has been interrupted when loading resources,
      // the IO should not abort, so must carefully retry, failing only
      // if the retry leads to some other IO exception.
      // 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
      // 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
      boolean isInterrupted = Thread.interrupted();
      int len;
      for (;;) {
      try {
      // 获取内容长度,顺利的话就跳出循环
      len = getContentLength();
      break;
      } catch (InterruptedIOException iioe) {
      // 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
      Thread.interrupted();
      isInterrupted = true;
      }
      }

      try {
      b = new byte[0];
      if (len == -1) len = Integer.MAX_VALUE;
      int pos = 0;
      while (pos < len) {
      int bytesToRead;
      if (pos >= b.length) { // Only expand when there's no room
      // 如果当前读取位置已经大于等于数组长度
      // 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
      bytesToRead = Math.min(len - pos, b.length + 1024);
      if (b.length < pos + bytesToRead) {
      // 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
      b = Arrays.copyOf(b, pos + bytesToRead);
      }
      } else {
      // 数组还有空间,待读取字节长度 = 数组剩余空间
      bytesToRead = b.length - pos;
      }
      int cc = 0;
      try {
      // 读取数据
      cc = in.read(b, pos, bytesToRead);
      } catch (InterruptedIOException iioe) {
      // 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
      Thread.interrupted();
      isInterrupted = true;
      }
      if (cc < 0) {
      // 如果读取返回值 < 0
      if (len != Integer.MAX_VALUE) {
      // 且长度并未无限,表示提前检测到 EOF,抛出异常
      throw new EOFException("Detect premature EOF");
      } else {
      // 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
      if (b.length != pos) {
      b = Arrays.copyOf(b, pos);
      }
      break;
      }
      }
      pos += cc;
      }
      } finally {
      try {
      in.close();
      } catch (InterruptedIOException iioe) {
      isInterrupted = true;
      } catch (IOException ignore) {}

      if (isInterrupted) {
      // 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
      Thread.currentThread().interrupt();
      }
      }
      return b;
      }
      + +

      getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

      +
      private synchronized InputStream cachedInputStream() throws IOException {
      if (cis == null) {
      cis = getInputStream();
      }
      return cis;
      }
      + +

      在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

      +
      public InputStream getInputStream() throws IOException
      {
      // 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
      return new FileInputStream(file);
      }

      public int getContentLength() throws IOException
      {
      // 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
      return (int)file.length();
      };
      + +

      SecureClassLoader#defineClass

      URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
      由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

      +
      protected final Class<?> defineClass(String name,
      byte[] b, int off, int len,
      CodeSource cs)
      {
      return defineClass(name, b, off, len, getProtectionDomain(cs));
      }

      protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
      CodeSource cs)
      {
      return defineClass(name, b, getProtectionDomain(cs));
      }
      + +

      方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

      +
      private ProtectionDomain getProtectionDomain(CodeSource cs) {
      // 如果 CodeSource 为 null,直接返回 null
      if (cs == null)
      return null;

      ProtectionDomain pd = null;
      synchronized (pdcache) {
      // 先从 Map 缓存中获取 ProtectionDomain
      pd = pdcache.get(cs);
      if (pd == null) {
      // 从 CodeSource 中获取 PermissionCollection
      PermissionCollection perms = getPermissions(cs);
      // 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
      pd = new ProtectionDomain(cs, perms, this, null);
      pdcache.put(cs, pd);
      if (debug != null) {
      debug.println(" getPermissions "+ pd);
      debug.println("");
      }
      }
      }
      return pd;
      }
      + +
      getPermissions

      根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

      +

      AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

      +
      protected PermissionCollection getPermissions(CodeSource codesource)
      {
      // 调用父类 URLClassLoader 的 getPermissions
      PermissionCollection perms = super.getPermissions(codesource);
      // 允许从类路径加载的任何类退出 VM的权限。
      // todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
      perms.add(new RuntimePermission("exitVM"));
      return perms;
      }
      + +

      SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

      +
      protected PermissionCollection getPermissions(CodeSource codesource)
      {
      // 调用父类 SecureClassLoader 的 getPermissions
      PermissionCollection perms = super.getPermissions(codesource);

      URL url = codesource.getLocation();

      Permission p;
      URLConnection urlConnection;

      try {
      // FileURLConnection 实例
      urlConnection = url.openConnection();
      // 允许 read 的 FilePermission 实例
      p = urlConnection.getPermission();
      } catch (java.io.IOException ioe) {
      p = null;
      urlConnection = null;
      }

      if (p instanceof FilePermission) {
      // if the permission has a separator char on the end,
      // it means the codebase is a directory, and we need
      // to add an additional permission to read recursively
      // 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
      String path = p.getName();
      if (path.endsWith(File.separator)) {
      path += "-";
      p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
      }
      } else if ((p == null) && (url.getProtocol().equals("file"))) {
      String path = url.getFile().replace('/', File.separatorChar);
      path = ParseUtil.decode(path);
      if (path.endsWith(File.separator))
      path += "-";
      p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
      } else {
      /**
      * Not loading from a 'file:' URL so we want to give the class
      * permission to connect to and accept from the remote host
      * after we've made sure the host is the correct one and is valid.
      */
      URL locUrl = url;
      if (urlConnection instanceof JarURLConnection) {
      locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
      }
      String host = locUrl.getHost();
      if (host != null && (host.length() > 0))
      p = new SocketPermission(host,
      SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
      }

      // make sure the person that created this class loader
      // would have this permission

      if (p != null) {
      final SecurityManager sm = System.getSecurityManager();
      if (sm != null) {
      final Permission fp = p;
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
      public Void run() throws SecurityException {
      sm.checkPermission(fp);
      return null;
      }
      }, acc);
      }
      perms.add(p);
      }
      return perms;
      }
      + +

      SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

      +
      protected PermissionCollection getPermissions(CodeSource codesource)
      {
      // 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
      // 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
      check();
      // ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
      return new Permissions(); // ProtectionDomain defers the binding
      }
      + +
      ProtectionDomain

      ProtectionDomain 的相关构造器参数:

      +
        +
      • CodeSource
      • +
      • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
      • +
      • ClassLoader
      • +
      • Principal[]
      • +
      +

      这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

      +

      ClassLoader#defineClass

      抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

      +
      protected final Class<?> defineClass(String name, byte[] b, int off, int len)
      throws ClassFormatError
      {
      return defineClass(name, b, off, len, null);
      }

      protected final Class<?> defineClass(String name, byte[] b, int off, int len,
      ProtectionDomain protectionDomain)
      throws ClassFormatError
      {
      protectionDomain = preDefineClass(name, protectionDomain);
      String source = defineClassSourceLocation(protectionDomain);
      Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
      postDefineClass(c, protectionDomain);
      return c;
      }

      protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
      ProtectionDomain protectionDomain)
      throws ClassFormatError
      {
      int len = b.remaining();

      // Use byte[] if not a direct ByteBufer:
      if (!b.isDirect()) {
      if (b.hasArray()) {
      return defineClass(name, b.array(),
      b.position() + b.arrayOffset(), len,
      protectionDomain);
      } else {
      // no array, or read-only array
      byte[] tb = new byte[len];
      b.get(tb); // get bytes out of byte buffer.
      return defineClass(name, tb, 0, len, protectionDomain);
      }
      }

      protectionDomain = preDefineClass(name, protectionDomain);
      String source = defineClassSourceLocation(protectionDomain);
      Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
      postDefineClass(c, protectionDomain);
      return c;
      }
      + +

      主要步骤:

      +
        +
      1. preDefineClass 前置处理
      2. +
      3. defineClassX
      4. +
      5. postDefineClass 后置处理
      6. +
      +
      preDefineClass

      确定保护域 ProtectionDomain,并检查:

      +
        +
      1. 未定义 java.*
      2. +
      3. 该类的签名者与包(package)中其余类的签名者相匹配
      4. +
      +
      private ProtectionDomain preDefineClass(String name,
      ProtectionDomain pd)
      {
      // 检查 name 为 null 或者有可能是有效的二进制名称
      if (!checkName(name))
      throw new NoClassDefFoundError("IllegalName: " + name);

      // Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
      // relies on the fact that spoofing is impossible if a class has a name
      // of the form "java.*"
      // 如果 name 以 java. 开头,则抛出异常
      if ((name != null) && name.startsWith("java.")) {
      throw new SecurityException
      ("Prohibited package name: " +
      name.substring(0, name.lastIndexOf('.')));
      }
      if (pd == null) {
      // 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
      pd = defaultDomain;
      }

      // 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
      // todo: certs
      if (name != null) checkCerts(name, pd.getCodeSource());

      return pd;
      }
      + +
      defineClassSourceLocation

      确定 ClassCodeSource 位置。

      +
      private String defineClassSourceLocation(ProtectionDomain pd)
      {
      CodeSource cs = pd.getCodeSource();
      String source = null;
      if (cs != null && cs.getLocation() != null) {
      source = cs.getLocation().toString();
      }
      return source;
      }
      + +
      defineClassX 方法

      这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

      +
      postDefineClass

      在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

      +
      private void postDefineClass(Class<?> c, ProtectionDomain pd)
      {
      if (pd.getCodeSource() != null) {
      // 获取证书
      Certificate certs[] = pd.getCodeSource().getCertificates();
      if (certs != null)
      setSigners(c, certs);
      }
      }
      ]]> + + java + class loader + + + + JVM GC 的测试和分析 + /2023/11/01/testing-and-analysis-of-jvm-gc/ + 堆的组成
      public class JvmGcTest_1 {

      public static final int _512KB = 512 * 1024;
      public static final int _1MB = 1024 * 1024;
      public static final int _6MB = 6 * 1024 * 1024;
      public static final int _7MB = 7 * 1024 * 1024;
      public static final int _8MB = 8 * 1024 * 1024;

      // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
      // -XX:+UseSerialGC 避免幸存区比例动态调整
      public static void main(String[] args) {

      }
      }
      + +
      Heap
      def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf68c8, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
      Metaspace used 3288K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 348K, capacity 388K, committed 512K, reserved 1048576K
      + +

      根据打印的信息,组成如下:

      +
        +
      • Heap: 堆。
          +
        • def new generation: 新生代。
        • +
        • tenured generation: 老年代。
        • +
        • Metaspace: 元空间,实际上并不属于堆, -XX:+PrintGCDetails 将它的信息一起输出。
        • +
        +
      • +
      +

      堆空间的比例

      新生代中的空间占比 eden:from:to 在默认情况下是 8:1:1,与观察到的数据 8192K:1024K:1024K 一致。
      新生代的空间 eden + from + to 为 10240K,符合 -Xmn10M 设置的大小。
      total 显示为 9216K,即 eden + from 的大小,是因为 to 的空间不计算在内。新生代可用的空间只有 eden + fromto 空间只是在使用标记-复制算法进行垃圾回收时使用。
      老年代的空间为 10240K。
      目前仅 eden 中已用 2010K,约占 eden 空间的 24%。

      +

      从内存地址分析堆空间

      内存地址为 16 位的 16 进制的数字,64 位机器。
      [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 分别表示地址空间的开始、已用、结束的地址指针。
      新生代 [0x00000000fec00000, 0x00000000ff600000),老年代 [0x00000000ff600000, 0x0000000100000000),计算可得空间大小均为 10MB。
      eden 中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8),空间大小为 2058440 byte,约等于 2010K。

      +

      显而易见,新生代和老生代是一片完全连续的地址空间。

      +

      堆的垃圾回收

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      }
      + +
      [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
      Heap
      def new generation total 9216K, used 8135K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
      from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
      Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 360K, capacity 388K, committed 512K, reserved 1048576K
      + +

      Allocation Failure,正常情况下,新对象总是分配在 Eden,分配空间失败,eden 的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
      [DefNew: 2013K->721K(9216K), 0.0105099 secs],新生代在垃圾回收前后空间的占用变化和耗时。
      2013K->721K(19456K), 0.0105455 secs,整个堆在垃圾回收前后空间的占用变化和耗时。

      +

      GC 类型

        +
      • GC: minor GC。
      • +
      • Fulle GC: full GC。
      • +
      +

      from 和 to 的角色变换

      from 的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0),空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden 区域存活的对象全部转移到了原 to 空间,fromto 空间的角色相互转换(从地址空间的信息可以看到此时 to 的地址指针比 from 的地址指针小)。
      eden 的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0),空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden 区域除了 byte[] 对象外,还存储了其他对象,比如为了创建 List<byte[]> 对象而新加载的类对象。

      +

      eden 空间足够时不发生 GC

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      }
      + +
      [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      Heap
      def new generation total 9216K, used 8647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 96% used [0x00000000fec00000, 0x00000000ff3bd8d0, 0x00000000ff400000)
      from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
      Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 360K, capacity 388K, committed 512K, reserved 1048576K
      + +

      由于 eden 区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。
      eden 区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0),空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16。显然第二次添加时,不再因为创建 List<byte[]> 而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh

      +

      新生代空间不足,部分对象提前晋升到老年代

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      list.add(new byte[_512KB]);
      }
      + +
      [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8565K->512K(9216K), 0.0046378 secs] 8565K->8396K(19456K), 0.0046540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation total 9216K, used 1350K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd1a20, 0x00000000ff400000)
      from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 7884K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb33a0, 0x00000000ffdb3400, 0x0000000100000000)
      Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 360K, capacity 388K, committed 512K, reserved 1048576K
      + +

      在第三次添加时,由于 eden 空间不足,因此又发生了第二次垃圾回收。
      [DefNew: 8565K->512K(9216K), 0.0046378 secs],新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
      在第二次添加完成后,eden [0x00000000fec00000, 0x00000000ff3bd8d0)from [0x00000000ff500000, 0x00000000ff5b45f0) 占用的空间为 8116432 + 738800 = 8855232 约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。
      8565K->8396K(19456K), 0.0046540 secs,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。
      eden 中加入了第三次添加时的对象,大于 512K 不少。
      此时 edenfromtenured 中均有不好确认成分的空间占用,比如 from 中多了 56 字节。

      +

      新生代空间不足,大对象直接在老年代创建

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      }
      + +
      Heap
      def new generation total 9216K, used 2177K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee20730, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
      Metaspace used 3353K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 360K, capacity 388K, committed 512K, reserved 1048576K
      + +

      在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。

      +

      内存不足 OOM

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }
      + +
      waiting...
      [GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
      at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
      at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:750)

      Heap
      def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
      Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
      class space used 522K, capacity 558K, committed 640K, reserved 1048576K
      + +

      当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError

      +

      线程中发生内存不足,不会影响其他线程

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();

      new Thread(() -> {
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }).start();

      System.out.println("waiting...");
      try {
      System.in.read();
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
      }
      + +
      [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
      [Full GC (Allocation Failure) [Tenured: 8912K->8895K(10240K), 0.0011880 secs] 8912K->8895K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0012009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 8895K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffeafce0, 0x00000000ffeafe00, 0x0000000100000000)
      Metaspace used 3380K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 363K, capacity 388K, committed 512K, reserved 1048576K
      Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      at com.moralok.jvm.gc.JvmGcTest.main(JvmGcTest.java:21)
      + +

      Thread-0 发生 OutOfMemoryError 后,main 线程仍然正常运行。

      +

      大对象的划分指标

      当创建的大对象 + 对象头的容量小于等于 eden,如果 GC 后的存活对象可以放入 to,那么还是会先在 eden 中创建大对象。
      在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。

      +
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB - 16]);
      }
      + +
      [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
      Heap
      def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
      Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 354K, capacity 388K, committed 512K, reserved 1048576K
      + +

      尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。

      +

      注意事项

        +
      • 正常情况下,新对象都是在 eden 中创建。
      • +
      • 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。
      • +
      • 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。
      • +
      • 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。
      • +
      • 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。
      • +
      +

      尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

      +]]>
      + + java + jvm + +
      + + 字符串常量池的测试和分析 + /2023/11/03/testing-and-analysis-of-StringTable/ + 如果你准备过 Java 的面试,应该看到过一个问题:“String s1 = new String("abc"); 这个语句创建了几个字符串对象”。这个问题曾经困扰我,当时的我不能理解这个问题想要考察的是什么?
      答案中或许提及了字符串常量池,但是如果细究起来,会发现答案并不完善,有些令人困惑,甚至问题本身就有一定的误导作用。它很容易让初学者以为创建一个字符串对象和创建一个其他类型的对象在过程上是有一些区别的。
      其实关键的地方在于 “abc” 而不是 new String("abc")

      +

      字符串常量池的作用

      字符串字面量

      字面量(literal)是用于表达源代码中的一个固定值的表示法(notion),比如代码中的整数、浮点数、字符串。简而言之,字符串字面量就是双引号包裹的字符串,例如:

      +
      String s1 = "a";
      + +

      在 Java 中,字符串对象就是一个 String 类型的对象,因此在程序运行时,String 类型的变量 s1 指向的一定是一个 String 对象。字面量 “a” 在某一个时刻,没有经过 new 关键字,变成了一个 String 对象

      +

      接下来我们来思考一个问题,程序中每一个字符串字面量都要对应着生成一个单独的 String 对象吗?考虑到 Java 中 String 对象是不可变的,显然相同的字符串字面量完全可以共用一个 String 对象从而避免重复创建对象。JVM 也是这样设计的,这些可以共用的 String 对象组成了一个字符串常量池。

      +
        +
      1. 第一次遇到某一个字符串字面量时,会在字符串常量池中创建一个 String 对象,以后遇到相同的字符串字面量,就复用该对象,不再重复创建。
      2. +
      3. 每一次 new 都会创建一个新的 String 对象。

      ps: 以上的“遇到某一个字符串字面量”就是很纯粹地指代程序的源代码中出现用双引号括起来的字符串字面量。

      进入字符串常量池的两种情况

      因此,如果字符串常量池中没有值为 “abc” 的 String 对象new String("abc") 语句将涉及两个 String 对象的创建,第一个是因为括号里的 “abc” 而在字符串常量池中生成的,第二个才是 new 关键字在堆中创建的;否则只会涉及一个 String 对象的创建。
      为什么上面改用如果字符串常量池中没有值为 “abc” 的 String 对象呢?这是因为,字符串常量池里保留的 String 对象有两种产生来源:

      @@ -1119,201 +1317,148 @@
      - Java 类加载器源码分析 - /2023/07/13/Java-class-loader-source-code-analysis/ - 组织类加载工作:loadClass

      Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

      -
      public Class<?> loadClass(String name) throws ClassNotFoundException {
      return loadClass(name, false);
      }
      - -

      根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

      -
      protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
      {
      // 以二进制名称获取类加载的锁进行同步
      synchronized (getClassLoadingLock(name)) {
      // First, check if the class has already been loaded
      // 首先检查类是否已加载,根据该方法注释可知:
      // 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
      Class<?> c = findLoadedClass(name);
      if (c == null) {
      long t0 = System.nanoTime();
      try {
      // 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
      if (parent != null) {
      // 递归调用
      c = parent.loadClass(name, false);
      } else {
      // 递归调用的终结点
      c = findBootstrapClassOrNull(name);
      }
      } catch (ClassNotFoundException e) {
      // ClassNotFoundException thrown if class not found
      // from the non-null parent class loader
      // 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
      }

      if (c == null) {
      // If still not found, then invoke findClass in order
      // to find the class.
      long t1 = System.nanoTime();
      // 父·类加载器未找到类,当前类加载器自己找。
      c = findClass(name);

      // this is the defining class loader; record the stats
      sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
      sun.misc.PerfCounter.getFindClasses().increment();
      }
      }
      if (resolve) {
      resolveClass(c);
      }
      return c;
      }
      }
      + JVM 内存区域的测试和分析 + /2023/11/04/testing-and-analysis-of-jvm-memory-area/ + 内存区域

      JVM 内存区域划分为:

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

      根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

      -
        -
      1. 调用 findLoadedClass(String) 以检查该类是否已加载。
      2. -
      3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
      4. -
      5. 调用 findClass(String) 方法来查找该类。
      6. +

        程序计数器

        虚拟机栈

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

        +

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

        +

        栈内存溢出

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

        如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

        -
        -

        注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

        -
        -

        怎么并行地加载类 getClassLoadingLock

        需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
        此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
        简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
        这个方案意味着非同名的目标类可以认为在加载时没有冲突?

        -
        protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
        lock = newLock;
        }
        }
        return lock;
        }
        +

        不正确的递归调用

        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();
        }
        }
        -
        什么是 “ClassLoader 对象注册为具有并行能力”呢?

        AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

        -
        static {
        ClassLoader.registerAsParallelCapable();
        }
        -

        java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

        -
          -
        1. 尚未创建调用者的实例(类加载器尚未实例化)
        2. -
        3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
        4. -
        -
        怎么保证这两个条件成立呢?
          -
        1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
        2. -
        3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
        4. -
        -

        你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

        -
        注册工作做了什么?

        简单地说就是保存了类加载器所属 ClassSet

        -
        @CallerSensitive
        protected static boolean registerAsParallelCapable() {
        // 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
        Class<? extends ClassLoader> callerClass =
        Reflection.getCallerClass().asSubclass(ClassLoader.class);
        // 注册调用者的 Class 为具有并行能力
        return ParallelLoaders.register(callerClass);
        }
        +

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

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

        +
        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;
        }
        }
        }
        -

        方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

        -
        // private 修饰,只有其外部类 ClassLoader 才可以使用
        // static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
        private static class ParallelLoaders {
        // private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
        private ParallelLoaders() {}

        // the set of parallel capable loader types
        // 使用 loaderTypes 时通过 synchronized 加同步锁
        private static final Set<Class<? extends ClassLoader>> loaderTypes =
        Collections.newSetFromMap(
        // todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
        new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
        static {
        // 将 ClassLoader 本身注册为具有并行能力
        synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
        }

        /**
        * Registers the given class loader type as parallel capabale.
        * Returns {@code true} is successfully registered; {@code false} if
        * loader's super class is not registered.
        */
        static boolean register(Class<? extends ClassLoader> c) {
        synchronized (loaderTypes) {
        if (loaderTypes.contains(c.getSuperclass())) {
        // register the class loader as parallel capable
        // if and only if all of its super classes are.
        // Note: given current classloading sequence, if
        // the immediate super class is parallel capable,
        // all the super classes higher up must be too.
        // 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
        // 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
        loaderTypes.add(c);
        return true;
        } else {
        return false;
        }
        }
        }

        /**
        * Returns {@code true} if the given class loader type is
        * registered as parallel capable.
        */
        static boolean isRegistered(Class<? extends ClassLoader> c) {
        synchronized (loaderTypes) {
        return loaderTypes.contains(c);
        }
        }
        }
        +

        局部变量的线程安全问题

          +
        1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
        2. +
        3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
        4. +
        +
        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;
        }
        }
        -
        “注册”怎么和锁产生联系?

        但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

        -
        private ClassLoader(Void unused, ClassLoader parent) {
        // 由 private 修饰,不允许子类重写
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
        // 如果类加载器已经注册为具有并行能力,则做一些赋值操作
        parallelLockMap = new ConcurrentHashMap<>();
        // 保存 package->certs 的 map 映射,相关的工作也可以并行进行
        package2certs = new ConcurrentHashMap<>();
        assertionLock = new Object();
        } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        assertionLock = this;
        }
        }

        // 由 protect 修饰,允许子类重写,传递了父·类加载器。
        protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
        }

        // 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
        protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
        }
        +

        线程问题排查

        CPU 占用率居高不下

        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. +
        3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
        4. +
        5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
        6. +
        +
        "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)
        -

        ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。

        -
        为什么注册的代码不能写在构造器方法里?

        使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
        同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。

        -
        -

        todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

        -
        -

        检查目标类是否已加载

        加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
        todo: 不同类加载器同一个类名会如何判定?

        -
        protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
        return null;
        return findLoadedClass0(name);
        }

        private native final Class<?> findLoadedClass0(String name);
        +

        死锁,迟迟未返回结果

        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();
        }
        }
        -

        保证核心类库的安全性:双亲委派模型

        正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
        正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

        -
        -

        在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

        -
        -
        为什么要用这套奇怪的机制

        这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

        -

        通常流程如下:

          -
        1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
        2. -
        3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
        4. -
        5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
        6. -
        7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
        8. -
        9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
        10. -
        11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
        12. +
        13. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
        14. +
        15. 其他工具如 jconsole 也具有检测死锁的功能。
        -

        注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

        -

        查找类资源:findClass

        默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

        -

        过程如下:

        +

        本地方法栈

        堆(Heap)的特点:

          -
        1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
        2. -
        3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
        4. -
        5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
        6. +
        7. 线程共享,需要考虑线程安全问题。
        8. +
        9. 存在垃圾回收机制。
        10. +
        11. 使用 -Xmx8m 设置大小。
        -
        protected Class<?> findClass(final String name)
        throws ClassNotFoundException
        {
        final Class<?> result;
        try {
        // todo:
        result = AccessController.doPrivileged(
        new PrivilegedExceptionAction<Class<?>>() {
        public Class<?> run() throws ClassNotFoundException {
        // 将 name 转换为 path
        String path = name.replace('.', '/').concat(".class");
        // 从 URLClassPath 中查找 Resource
        Resource res = ucp.getResource(path, false);
        if (res != null) {
        try {
        return defineClass(name, res);
        } catch (IOException e) {
        throw new ClassNotFoundException(name, e);
        } catch (ClassFormatError e2) {
        if (res.getDataError() != null) {
        e2.addSuppressed(res.getDataError());
        }
        throw e2;
        }
        } else {
        return null;
        }
        }
        }, acc);
        } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
        throw new ClassNotFoundException(name);
        }
        return result;
        }
        - -

        查找类的目录列表:URLClassPath

        URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
        URLClassPath 的核心构造器方法:

        -
        public URLClassPath(URL[] urls,
        URLStreamHandlerFactory factory,
        AccessControlContext acc) {
        // 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
        for (int i = 0; i < urls.length; i++) {
        path.add(urls[i]);
        }
        // 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
        push(urls);
        if (factory != null) {
        // 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
        jarHandler = factory.createURLStreamHandler("jar");
        }
        if (DISABLE_ACC_CHECKING)
        this.acc = null;
        else
        this.acc = acc;
        }
        - -
        URLClassPath#getResource

        URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
        显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

        -
        public Resource getResource(String name, boolean check) {
        if (DEBUG) {
        System.err.println("URLClassPath.getResource(\"" + name + "\")");
        }

        Loader loader;
        // 获取缓存(默认没有用)
        int[] cache = getLookupCache(name);
        // 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
        for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
        Resource res = loader.getResource(name, check);
        if (res != null) {
        return res;
        }
        }
        return null;
        }
        - -
        URLClassPath#getNextLoader

        获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

        -
        private synchronized Loader getNextLoader(int[] cache, int index) {
        if (closed) {
        return null;
        }
        if (cache != null) {
        if (index < cache.length) {
        Loader loader = loaders.get(cache[index]);
        if (DEBUG_LOOKUP_CACHE) {
        System.out.println("HASCACHE: Loading from : " + cache[index]
        + " = " + loader.getBaseURL());
        }
        return loader;
        } else {
        return null; // finished iterating over cache[]
        }
        } else {
        // 获取 Loader
        return getLoader(index);
        }
        }
        +

        堆内存溢出

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

        +
        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);
        }
        }
        }
        -
        URLClassPath#getLoader(int)
          -
        1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
        2. -
        3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
        4. -
        5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
        6. -
        -
        private synchronized Loader getLoader(int index) {
        if (closed) {
        return null;
        }
        // Expand URL search path until the request can be satisfied
        // or the URL stack is empty.
        while (loaders.size() < index + 1) {
        // Pop the next URL from the URL stack
        // 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
        URL url;
        synchronized (urls) {
        if (urls.empty()) {
        return null;
        } else {
        url = urls.pop();
        }
        }
        // Skip this URL if it already has a Loader. (Loader
        // may be null in the case where URL has not been opened
        // but is referenced by a JAR index.)
        String urlNoFragString = URLUtil.urlNoFragString(url);
        if (lmap.containsKey(urlNoFragString)) {
        continue;
        }
        // Otherwise, create a new Loader for the URL.
        Loader loader;
        try {
        // 根据 URL 创建 Loader
        loader = getLoader(url);
        // If the loader defines a local class path then add the
        // URLs to the list of URLs to be opened.
        URL[] urls = loader.getClassPath();
        if (urls != null) {
        push(urls);
        }
        } catch (IOException e) {
        // Silently ignore for now...
        continue;
        } catch (SecurityException se) {
        // Always silently ignore. The context, if there is one, that
        // this URLClassPath was given during construction will never
        // have permission to access the URL.
        if (DEBUG) {
        System.err.println("Failed to access " + url + ", " + se );
        }
        continue;
        }
        // Finally, add the Loader to the search path.
        validateLookupCache(loaders.size(), urlNoFragString);
        loaders.add(loader);
        lmap.put(urlNoFragString, loader);
        }
        if (DEBUG_LOOKUP_CACHE) {
        System.out.println("NOCACHE: Loading from : " + index );
        }
        return loaders.get(index);
        }
        +
        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
        -
        URLClassPath#getLoader(java.net.URL)

        根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

        -
          -
        1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
        2. -
        3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
        4. -
        5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
        6. +

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

          +

          监测堆内存

            +
          1. 使用 jps 查看 Java 进程列表
          2. +
          3. 使用 jmap -heap pid 查看堆内存信息
          4. +
          5. 还可以使用 jconsole 观察堆内存变化曲线
          6. +
          7. 还可以使用 VisualVM 查看堆信息
          -
          private Loader getLoader(final URL url) throws IOException {
          try {
          return java.security.AccessController.doPrivileged(
          new java.security.PrivilegedExceptionAction<Loader>() {
          public Loader run() throws IOException {
          String file = url.getFile();
          if (file != null && file.endsWith("/")) {
          if ("file".equals(url.getProtocol())) {
          return new FileLoader(url);
          } else {
          return new Loader(url);
          }
          } else {
          return new JarLoader(url, jarHandler, lmap, acc);
          }
          }
          }, acc);
          } catch (java.security.PrivilegedActionException pae) {
          throw (IOException)pae.getException();
          }
          }
          - -

          URLClassPath.FileLoader#getResource

          FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

          -
          Resource getResource(final String name, boolean check) {
          final URL url;
          try {
          URL normalizedBase = new URL(getBaseURL(), ".");
          url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

          if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
          // requested resource had ../..'s in path
          return null;
          }

          if (check)
          URLClassPath.check(url);

          final File file;
          if (name.indexOf("..") != -1) {
          file = (new File(dir, name.replace('/', File.separatorChar)))
          .getCanonicalFile();
          if ( !((file.getPath()).startsWith(dir.getPath())) ) {
          /* outside of base dir */
          return null;
          }
          } else {
          file = new File(dir, name.replace('/', File.separatorChar));
          }

          if (file.exists()) {
          return new Resource() {
          public String getName() { return name; };
          public URL getURL() { return url; };
          public URL getCodeSourceURL() { return getBaseURL(); };
          public InputStream getInputStream() throws IOException
          { return new FileInputStream(file); };
          public int getContentLength() throws IOException
          { return (int)file.length(); };
          };
          }
          } catch (Exception e) {
          return null;
          }
          return null;
          }
          +
          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);
          }
          }
          -

          ClassLoader 的搜索路径

          从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。

          -

          以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。

          -
          C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest
          +

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

          +
          Eden Space:
          capacity = 268435456 (256.0MB)
          used = 32212360 (30.72010040283203MB)

          used = 42698136 (40.720115661621094MB)

          used = 5368728 (5.120018005371094MB)
          -
          bootstrap class loader

          启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

          -
            -
          • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
          • -
          • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
          • -
          • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
          • -
          -

          IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

          -
          Error occurred during initialization of VM
          java/lang/NoClassDefFoundError: java/lang/Object

          进程已结束,退出代码1
          +

          使用 jconsole 查看堆内存信息:

          + -
          ExtClassLoader

          扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

          -
          AppClassLoader

          应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

          -

          CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

          -
            -
          • Windows:.;C:\path\to\classes
          • -
          • Linux:.:/path/to/classes
          • -
          -

          事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

          -

          字节数据转换为 Class 实例:defineClass

          方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
          以下是定义类时最终可能调用的 native 方法。

          -
          private native Class<?> defineClass0(String name, byte[] b, int off, int len,
          ProtectionDomain pd);

          private native Class<?> defineClass1(String name, byte[] b, int off, int len,
          ProtectionDomain pd, String source);

          private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
          int off, int len, ProtectionDomain pd,
          String source);
          +

          堆内存占用居高不下

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

          +
          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 而言:

            -
          • name,目标类的名称。
          • -
          • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
          • -
          • ProtectionDomain,保护域,todo:
          • -
          • sourceCodeSource 的位置。
          • +
          • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
          • +
          • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。
          -

          defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

          -

          URLClassLoader#defineClass

          此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

          -
          private Class<?> defineClass(String name, Resource res) throws IOException {
          long t0 = System.nanoTime();
          // 获取最后一个 . 的位置
          int i = name.lastIndexOf('.');
          // 返回资源的 CodeSourceURL
          URL url = res.getCodeSourceURL();
          if (i != -1) {
          // 截取包名 com.example
          String pkgname = name.substring(0, i);
          // Check if package already loaded.
          Manifest man = res.getManifest();
          definePackageInternal(pkgname, man, url);
          }
          // Now read the class bytes and define the class
          // 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
          java.nio.ByteBuffer bb = res.getByteBuffer();
          if (bb != null) {
          // Use (direct) ByteBuffer:
          // 不常用
          CodeSigner[] signers = res.getCodeSigners();
          CodeSource cs = new CodeSource(url, signers);
          sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
          // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
          return defineClass(name, bb, cs);
          } else {
          // 以字节数组的形式返回资源数据
          byte[] b = res.getBytes();
          // must read certificates AFTER reading bytes.
          // 必须再读取字节数据后读取证书,todo:
          CodeSigner[] signers = res.getCodeSigners();
          // 根据 URL 和签名者创建 CodeSource
          CodeSource cs = new CodeSource(url, signers);
          sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
          // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
          return defineClass(name, b, 0, b.length, cs);
          }
          }
          +

          方法区溢出

          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. +
          3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
          4. +
          5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
          6. +
          7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
          8. +
          +

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

          +

          运行时常量池

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

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

          使用 javap 反编译

          public class MethodAreaTest_2 {  

          public static void main(String[] args) {
          System.out.println("hello world");
          }
          }
          -

          Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

          -
          public byte[] getBytes() throws IOException {
          byte[] b;
          // Get stream before content length so that a FileNotFoundException
          // can propagate upwards without being caught too early
          // 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
          // 获取缓存的 InputStream
          InputStream in = cachedInputStream();

          // This code has been uglified to protect against interrupts.
          // Even if a thread has been interrupted when loading resources,
          // the IO should not abort, so must carefully retry, failing only
          // if the retry leads to some other IO exception.
          // 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
          // 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
          boolean isInterrupted = Thread.interrupted();
          int len;
          for (;;) {
          try {
          // 获取内容长度,顺利的话就跳出循环
          len = getContentLength();
          break;
          } catch (InterruptedIOException iioe) {
          // 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
          Thread.interrupted();
          isInterrupted = true;
          }
          }

          try {
          b = new byte[0];
          if (len == -1) len = Integer.MAX_VALUE;
          int pos = 0;
          while (pos < len) {
          int bytesToRead;
          if (pos >= b.length) { // Only expand when there's no room
          // 如果当前读取位置已经大于等于数组长度
          // 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
          bytesToRead = Math.min(len - pos, b.length + 1024);
          if (b.length < pos + bytesToRead) {
          // 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
          b = Arrays.copyOf(b, pos + bytesToRead);
          }
          } else {
          // 数组还有空间,待读取字节长度 = 数组剩余空间
          bytesToRead = b.length - pos;
          }
          int cc = 0;
          try {
          // 读取数据
          cc = in.read(b, pos, bytesToRead);
          } catch (InterruptedIOException iioe) {
          // 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
          Thread.interrupted();
          isInterrupted = true;
          }
          if (cc < 0) {
          // 如果读取返回值 < 0
          if (len != Integer.MAX_VALUE) {
          // 且长度并未无限,表示提前检测到 EOF,抛出异常
          throw new EOFException("Detect premature EOF");
          } else {
          // 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
          if (b.length != pos) {
          b = Arrays.copyOf(b, pos);
          }
          break;
          }
          }
          pos += cc;
          }
          } finally {
          try {
          in.close();
          } catch (InterruptedIOException iioe) {
          isInterrupted = true;
          } catch (IOException ignore) {}

          if (isInterrupted) {
          // 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
          Thread.currentThread().interrupt();
          }
          }
          return b;
          }
          +
          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"
          -

          getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

          -
          private synchronized InputStream cachedInputStream() throws IOException {
          if (cis == null) {
          cis = getInputStream();
          }
          return cis;
          }
          +
            +
          1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
          2. +
          3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
          4. +
          5. 这个过程其实就是信息从文件进入了内存。
          6. +
          +

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

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

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

          +

          直接内存

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

          NIO 和 IO 的拷贝性能

          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);
          }
          }
          -

          在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

          -
          public InputStream getInputStream() throws IOException
          {
          // 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
          return new FileInputStream(file);
          }

          public int getContentLength() throws IOException
          {
          // 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
          return (int)file.length();
          };
          +
          io 用时 1676.9797
          directBuffer 用时 836.4796
          -

          SecureClassLoader#defineClass

          URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
          由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

          -
          protected final Class<?> defineClass(String name,
          byte[] b, int off, int len,
          CodeSource cs)
          {
          return defineClass(name, b, off, len, getProtectionDomain(cs));
          }

          protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
          CodeSource cs)
          {
          return defineClass(name, b, getProtectionDomain(cs));
          }
          + -

          方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

          -
          private ProtectionDomain getProtectionDomain(CodeSource cs) {
          // 如果 CodeSource 为 null,直接返回 null
          if (cs == null)
          return null;

          ProtectionDomain pd = null;
          synchronized (pdcache) {
          // 先从 Map 缓存中获取 ProtectionDomain
          pd = pdcache.get(cs);
          if (pd == null) {
          // 从 CodeSource 中获取 PermissionCollection
          PermissionCollection perms = getPermissions(cs);
          // 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
          pd = new ProtectionDomain(cs, perms, this, null);
          pdcache.put(cs, pd);
          if (debug != null) {
          debug.println(" getPermissions "+ pd);
          debug.println("");
          }
          }
          }
          return pd;
          }
          + -
          getPermissions

          根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

          -

          AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

          -
          protected PermissionCollection getPermissions(CodeSource codesource)
          {
          // 调用父类 URLClassLoader 的 getPermissions
          PermissionCollection perms = super.getPermissions(codesource);
          // 允许从类路径加载的任何类退出 VM的权限。
          // todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
          perms.add(new RuntimePermission("exitVM"));
          return perms;
          }
          -

          SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

          -
          protected PermissionCollection getPermissions(CodeSource codesource)
          {
          // 调用父类 SecureClassLoader 的 getPermissions
          PermissionCollection perms = super.getPermissions(codesource);

          URL url = codesource.getLocation();

          Permission p;
          URLConnection urlConnection;

          try {
          // FileURLConnection 实例
          urlConnection = url.openConnection();
          // 允许 read 的 FilePermission 实例
          p = urlConnection.getPermission();
          } catch (java.io.IOException ioe) {
          p = null;
          urlConnection = null;
          }

          if (p instanceof FilePermission) {
          // if the permission has a separator char on the end,
          // it means the codebase is a directory, and we need
          // to add an additional permission to read recursively
          // 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
          String path = p.getName();
          if (path.endsWith(File.separator)) {
          path += "-";
          p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
          }
          } else if ((p == null) && (url.getProtocol().equals("file"))) {
          String path = url.getFile().replace('/', File.separatorChar);
          path = ParseUtil.decode(path);
          if (path.endsWith(File.separator))
          path += "-";
          p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
          } else {
          /**
          * Not loading from a 'file:' URL so we want to give the class
          * permission to connect to and accept from the remote host
          * after we've made sure the host is the correct one and is valid.
          */
          URL locUrl = url;
          if (urlConnection instanceof JarURLConnection) {
          locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
          }
          String host = locUrl.getHost();
          if (host != null && (host.length() > 0))
          p = new SocketPermission(host,
          SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
          }

          // make sure the person that created this class loader
          // would have this permission

          if (p != null) {
          final SecurityManager sm = System.getSecurityManager();
          if (sm != null) {
          final Permission fp = p;
          AccessController.doPrivileged(new PrivilegedAction<Void>() {
          public Void run() throws SecurityException {
          sm.checkPermission(fp);
          return null;
          }
          }, acc);
          }
          perms.add(p);
          }
          return perms;
          }
          +

          直接内存溢出

          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);
          }
          }
          -

          SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

          -
          protected PermissionCollection getPermissions(CodeSource codesource)
          {
          // 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
          // 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
          check();
          // ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
          return new Permissions(); // ProtectionDomain defers the binding
          }
          +
          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
          -
          ProtectionDomain

          ProtectionDomain 的相关构造器参数:

          -
            -
          • CodeSource
          • -
          • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
          • -
          • ClassLoader
          • -
          • Principal[]
          • -
          -

          这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

          -

          ClassLoader#defineClass

          抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

          -
          protected final Class<?> defineClass(String name, byte[] b, int off, int len)
          throws ClassFormatError
          {
          return defineClass(name, b, off, len, null);
          }

          protected final Class<?> defineClass(String name, byte[] b, int off, int len,
          ProtectionDomain protectionDomain)
          throws ClassFormatError
          {
          protectionDomain = preDefineClass(name, protectionDomain);
          String source = defineClassSourceLocation(protectionDomain);
          Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
          postDefineClass(c, protectionDomain);
          return c;
          }

          protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
          ProtectionDomain protectionDomain)
          throws ClassFormatError
          {
          int len = b.remaining();

          // Use byte[] if not a direct ByteBufer:
          if (!b.isDirect()) {
          if (b.hasArray()) {
          return defineClass(name, b.array(),
          b.position() + b.arrayOffset(), len,
          protectionDomain);
          } else {
          // no array, or read-only array
          byte[] tb = new byte[len];
          b.get(tb); // get bytes out of byte buffer.
          return defineClass(name, tb, 0, len, protectionDomain);
          }
          }

          protectionDomain = preDefineClass(name, protectionDomain);
          String source = defineClassSourceLocation(protectionDomain);
          Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
          postDefineClass(c, protectionDomain);
          return c;
          }
          +

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

          +

          直接内存释放的原理

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

          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. preDefineClass 前置处理
          2. -
          3. defineClassX
          4. -
          5. postDefineClass 后置处理
          6. -
          -
          preDefineClass

          确定保护域 ProtectionDomain,并检查:

          -
            -
          1. 未定义 java.*
          2. -
          3. 该类的签名者与包(package)中其余类的签名者相匹配
          4. -
          -
          private ProtectionDomain preDefineClass(String name,
          ProtectionDomain pd)
          {
          // 检查 name 为 null 或者有可能是有效的二进制名称
          if (!checkName(name))
          throw new NoClassDefFoundError("IllegalName: " + name);

          // Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
          // relies on the fact that spoofing is impossible if a class has a name
          // of the form "java.*"
          // 如果 name 以 java. 开头,则抛出异常
          if ((name != null) && name.startsWith("java.")) {
          throw new SecurityException
          ("Prohibited package name: " +
          name.substring(0, name.lastIndexOf('.')));
          }
          if (pd == null) {
          // 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
          pd = defaultDomain;
          }

          // 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
          // todo: certs
          if (name != null) checkCerts(name, pd.getCodeSource());

          return pd;
          }
          +

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

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

          +
          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);
          }
          }
          }
          -
          defineClassSourceLocation

          确定 ClassCodeSource 位置。

          -
          private String defineClassSourceLocation(ProtectionDomain pd)
          {
          CodeSource cs = pd.getCodeSource();
          String source = null;
          if (cs != null && cs.getLocation() != null) {
          source = cs.getLocation().toString();
          }
          return source;
          }
          +

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

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

          +

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

          +
          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;
          }
          -
          defineClassX 方法

          这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

          -
          postDefineClass

          在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

          -
          private void postDefineClass(Class<?> c, ProtectionDomain pd)
          {
          if (pd.getCodeSource() != null) {
          // 获取证书
          Certificate certs[] = pd.getCodeSource().getCertificates();
          if (certs != null)
          setSigners(c, certs);
          }
          }
          ]]> +

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

          +
          public void run() {
          if (address == 0) {
          // Paranoia
          return;
          }
          unsafe.freeMemory(address);
          address = 0;
          Bits.unreserveMemory(size, capacity);
          }
          +]]> java - class loader + jvm @@ -1543,151 +1688,6 @@ prometheus - - JVM 内存区域的测试和分析 - /2023/11/04/testing-and-analysis-of-jvm-memory-area/ - 内存区域

          JVM 内存区域划分为:

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

          程序计数器

          虚拟机栈

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

          -

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

          -

          栈内存溢出

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

          不正确的递归调用

          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 解析无限循环

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

          -
          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. -
          3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
          4. -
          -
          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 占用率居高不下

          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. -
          3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
          4. -
          5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
          6. -
          -
          "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)
          - -

          死锁,迟迟未返回结果

          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. -
          3. 其他工具如 jconsole 也具有检测死锁的功能。
          4. -
          -

          本地方法栈

          堆(Heap)的特点:

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

          堆内存溢出

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

          -
          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);
          }
          }
          }
          - -
          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. -
          3. 使用 jmap -heap pid 查看堆内存信息
          4. -
          5. 还可以使用 jconsole 观察堆内存变化曲线
          6. -
          7. 还可以使用 VisualVM 查看堆信息
          8. -
          -
          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 查看堆内存信息:

          -
          Eden Space:
          capacity = 268435456 (256.0MB)
          used = 32212360 (30.72010040283203MB)

          used = 42698136 (40.720115661621094MB)

          used = 5368728 (5.120018005371094MB)
          - -

          使用 jconsole 查看堆内存信息:

          - - -

          堆内存占用居高不下

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

          -
          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 设置大小。
          • -
          -

          方法区溢出

          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. -
          3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
          4. -
          5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
          6. -
          7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
          8. -
          -

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

          -

          运行时常量池

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

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

          使用 javap 反编译

          public class MethodAreaTest_2 {  

          public static void main(String[] args) {
          System.out.println("hello world");
          }
          }
          - -
          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. -
          3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
          4. -
          5. 这个过程其实就是信息从文件进入了内存。
          6. -
          -

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

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

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

          -

          直接内存

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

          NIO 和 IO 的拷贝性能

          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);
          }
          }
          - -
          io 用时 1676.9797
          directBuffer 用时 836.4796
          - - - - - - -

          直接内存溢出

          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);
          }
          }
          - -
          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 影响

          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();
          }
          }
          - -

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

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

          -
          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 对象。

          -
          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 方法。

          -
          public void run() {
          if (address == 0) {
          // Paranoia
          return;
          }
          unsafe.freeMemory(address);
          address = 0;
          Bits.unreserveMemory(size, capacity);
          }
          -]]>
          - - java - jvm - -
          JDK 动态代理和 CGLib /2023/11/19/JDK-dynamic-proxy-and-CGLib/ @@ -2111,4 +2111,145 @@ jvm + + Spring AOP 如何创建代理 beans + /2023/11/19/how-does-Spring-AOP-create-proxy-beans/ + Spring AOP 是基于代理实现的,它既支持 JDK 动态代理也支持 CGLib。

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

          过程简单图解

          + +

          准备工作

            +
          • 引入依赖
            <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>
          • +
          • 目标对象类
            public class MathCalculator {

            public int div(int i, int j) {
            return i / j;
            }
            }
          • +
          • 切面类
            @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;
            }
            }
          • +
          • 配置类
            @Configuration
            @EnableAspectJAutoProxy
            public class AopConfig {

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

            @Bean
            public LogAspects logAspects() {
            return new LogAspects();
            }
            }
          • +
          • 测试类
            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 断点的判断条件(可选)
            beanName.equals("mathCalculator")
          • +
          +

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

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

          +

          常规的入口 getBean

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

          +
          @Override
          public void preInstantiateSingletons() throws BeansException {
          // ...(mathCalculator)
          getBean(beanName);
          // ...
          }
          + +

          应用后置处理器

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

          +

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

          +
          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 的入口方法
          • +
          +
          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

          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. 设置相关信息
          4. +
          5. 通过 ProxyFactory 获取代理
          6. +
          +
          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 的含义与职责从字面上有点不好理解。

          +
          +
          public Object getProxy(ClassLoader classLoader) {
          return createAopProxy().getProxy(classLoader);
          }
          + +

          ProxyFactor视角,创建 AopProxy

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

          +
          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 动态代理,这些结论的依据就在于此。

          +
          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。

          +
          @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 方法中。

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

          CGLib 动态代理

          ObjenesisCglibAopProxy。

          +
          @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。

          +
          @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 会处理它。

          +
          @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 被提前创建。

          +
          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);
          }
          }
          }
          + +
          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. +
          3. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
          4. +
          +
          @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 并缓存。

          +
          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。

          +
          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。

          +
          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

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

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

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

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

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

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

          +

          查找并缓存 advisors

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

          +
          protected List<Advisor> findCandidateAdvisors() {
          List<Advisor> advisors = super.findCandidateAdvisors();
          advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
          return advisors;
          }
          + +

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

          +
          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 的查找情况。

          +]]>
          + + java + spring + aop + +
          diff --git a/tags/aop/index.html b/tags/aop/index.html new file mode 100644 index 00000000..e4bae95c --- /dev/null +++ b/tags/aop/index.html @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +标签: aop | Moralok + + + + + + + + + + + +
          + +
          +
          +
          + + + + + +
          + + + + + + + +
          + +
          + +
          + + + + + + + + +
          + + +
          + + 0% +
          + + + + +
          + + + + + +
          +
          +
          +

          aop + 标签 +

          +
          + + +
          + 2023 +
          + + + +
          +
          + + + + +
          +
          + +
          + +
          + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/bytecode/index.html b/tags/bytecode/index.html index 9dd8eda8..8963028a 100644 --- a/tags/bytecode/index.html +++ b/tags/bytecode/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          bytecode - 42k + 45k
          Hexo & NexT.Muse 强力驱动 diff --git a/tags/cglib/index.html b/tags/cglib/index.html index 10e0887e..ea430dce 100644 --- a/tags/cglib/index.html +++ b/tags/cglib/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          cglib - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/clash/index.html b/tags/clash/index.html index f30c9228..9e1e6e87 100644 --- a/tags/clash/index.html +++ b/tags/clash/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          clash - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/class-loader/index.html b/tags/class-loader/index.html index fc6dc810..de7536ce 100644 --- a/tags/class-loader/index.html +++ b/tags/class-loader/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          class loader - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/distributed-lock/index.html b/tags/distributed-lock/index.html index a7971bbf..56d955f7 100644 --- a/tags/distributed-lock/index.html +++ b/tags/distributed-lock/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          distributed lock - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/docker/index.html b/tags/docker/index.html index 986a475c..62159032 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -152,13 +152,13 @@ @@ -274,13 +274,13 @@

          docker - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/grafana/index.html b/tags/grafana/index.html index ed515c8f..42fc82d5 100644 --- a/tags/grafana/index.html +++ b/tags/grafana/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          grafana - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/index.html b/tags/index.html index 3e6a66f8..0b732595 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - + @@ -155,13 +155,13 @@ @@ -209,10 +209,10 @@

          tags
          @@ -246,13 +246,13 @@

          tags - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/java/index.html b/tags/java/index.html index d30a278e..50385e2b 100644 --- a/tags/java/index.html +++ b/tags/java/index.html @@ -152,13 +152,13 @@ @@ -205,6 +205,26 @@

          java 2023

          + +
          Hexo & NexT.Muse 强力驱动 diff --git a/tags/jdk-proxy/index.html b/tags/jdk-proxy/index.html index 062137d2..4c04408b 100644 --- a/tags/jdk-proxy/index.html +++ b/tags/jdk-proxy/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          jdk proxy - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/jvm/index.html b/tags/jvm/index.html index d8d1b794..88265c1f 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -152,13 +152,13 @@ @@ -311,13 +311,13 @@

          jvm - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/minikube/index.html b/tags/minikube/index.html index 0cd81084..5a72db8d 100644 --- a/tags/minikube/index.html +++ b/tags/minikube/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          minikube - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/openvpn/index.html b/tags/openvpn/index.html index 10b14335..eb04a9c3 100644 --- a/tags/openvpn/index.html +++ b/tags/openvpn/index.html @@ -152,13 +152,13 @@ @@ -291,13 +291,13 @@

          openvpn - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/prometheus/index.html b/tags/prometheus/index.html index 899b4179..2de5d5bb 100644 --- a/tags/prometheus/index.html +++ b/tags/prometheus/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          prometheus - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/proxy/index.html b/tags/proxy/index.html index 6064cbd0..d53e65e3 100644 --- a/tags/proxy/index.html +++ b/tags/proxy/index.html @@ -152,13 +152,13 @@ @@ -271,13 +271,13 @@

          proxy - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/redis/index.html b/tags/redis/index.html index bc925cd0..bce0a5bf 100644 --- a/tags/redis/index.html +++ b/tags/redis/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          redis - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/spring/index.html b/tags/spring/index.html index 030ab9de..1a263629 100644 --- a/tags/spring/index.html +++ b/tags/spring/index.html @@ -152,13 +152,13 @@ @@ -205,6 +205,26 @@

          spring 2023

          + +
          Hexo & NexT.Muse 强力驱动 diff --git a/tags/ssh/index.html b/tags/ssh/index.html index 06e60a08..9505d73b 100644 --- a/tags/ssh/index.html +++ b/tags/ssh/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          ssh - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/tmux/index.html b/tags/tmux/index.html index 6da9b12b..6824f67c 100644 --- a/tags/tmux/index.html +++ b/tags/tmux/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          tmux - 42k + 45k

          Hexo & NexT.Muse 强力驱动 diff --git a/tags/ubuntu/index.html b/tags/ubuntu/index.html index 49a8b0cf..694d5a29 100644 --- a/tags/ubuntu/index.html +++ b/tags/ubuntu/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

          ubuntu - 42k + 45k

          Hexo & NexT.Muse 强力驱动