目录

池化技术

像线程资源、数据库连接资源或者 TCP 连接等,这类对象的初始化通常要花费比较长的时间,如果频繁地申请和销毁,就会耗费大量的系统资源,造成不必要的性能损失。

并且这些对象都有一个显著的特征,就是通过轻量级的重置工作,可以循环、重复地使用。

这个时候,我们就可以使用一个虚拟的池子,将这些资源保存起来,当使用的时候,我们就从池子里快速获取一个即可。

在 Java 中,池化技术应用非常广泛,常见的就有数据库连接池、线程池等

Apache Commons Pool

开源软件库 Apache Commons Pool 提供了一个对象池 API 与含多个对象池的实现。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

对象池的核心类

泛型接口 PooledObjectFactory<T> 管理池对象的生命周期。一般通过继承 BasePooledObjectFactory 来定制。

GenericObjectPool 提供多种配置选项,包括限制空闲或活动实例的数量、在池中空闲时驱逐实例等。从版本 2 开始,GenericObjectPool 还提供废弃实例跟踪和删除功能。

1
2
3
GenericObjectPool(PooledObjectFactory<T> factory)
GenericObjectPool(PooledObjectFactory<T> factory, GenericObjectPoolConfig<T> config)
GenericObjectPool(PooledObjectFactory<T> factory, GenericObjectPoolConfig<T> config, AbandonedConfig abandonedConfig)

GenericKeyedObjectPool 为键控池提供相同的行为。

SoftReferenceObjectPool 可以根据需要增长,但允许垃圾收集器根据需要从池中逐出空闲实例。

 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 class ReaderUtil {
    
    private ObjectPool<StringBuffer> pool;
    
    public ReaderUtil(ObjectPool<StringBuffer> pool) {
        this.pool = pool;
    }
    public void dosomething(){
    // 池的使用
    StringBuffer buf = pool.borrowObject();
    }
}
private ObjectPool<StringBuffer> pool;
public void dosomething(){
  // 池的使用
  StringBuffer buf = pool.borrowObject();
}

public class StringBufferFactory
    extends BasePooledObjectFactory<StringBuffer> {

    @Override
    public StringBuffer create() {
        // 主要的耗时操作
        return new StringBuffer();
    }

    /**
     * Use the default PooledObject implementation.
     */
    @Override
    public PooledObject<StringBuffer> wrap(StringBuffer buffer) {
        return new DefaultPooledObject<StringBuffer>(buffer);
    }

    /**
     * When an object is returned to the pool, clear the buffer.
     */
    @Override
    public void passivateObject(PooledObject<StringBuffer> pooledObject) {
        pooledObject.getObject().setLength(0);
    }

    // for all other methods, the no-op implementation
    // in BasePooledObjectFactory will suffice
}

ReaderUtil readerUtil = new ReaderUtil(new GenericObjectPool<StringBuffer>(new StringBufferFactory()));

Jedis 中 JedisPool、ConnectionPool 都是使用的 Apache Commons Pool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Fork(2) 
@State(Scope.Benchmark) 
@Warmup(iterations = 5, time = 1) 
@Measurement(iterations = 5, time = 1) 
@BenchmarkMode(Mode.Throughput) 
public class JedisPoolVSJedisBenchmark { 
   JedisPool pool = new JedisPool("localhost", 6379); 

   @Benchmark 
   public void testPool() { 
       Jedis jedis = pool.getResource(); 
       jedis.set("a", UUID.randomUUID().toString()); 
       jedis.close(); 
   } 

   @Benchmark 
   public void testJedis() { 
       Jedis jedis = new Jedis("localhost", 6379); 
       jedis.set("a", UUID.randomUUID().toString()); 
       jedis.close(); 
   } 
   //此处省略若干行
}

数据库连接池 HikariCP

快速、简单、可靠。HikariCP 是一个“零开销”的生产就绪 JDBC 连接池。它是 SpringBoot 中默认的数据库连接池。

  • 它使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作
  • 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令
  • 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争

业务类型通常有两种:一种需要快速的响应时间,把数据尽快返回给用户;另外一种是可以在后台慢慢执行,耗时比较长,对时效性要求不高。

如果这两种业务类型,共用一个数据库连接池,就容易发生资源争抢,进而影响接口响应速度。

虽然微服务能够解决这种情况,但大多数服务是没有这种条件的,这时就可以对连接池进行拆分。

线程池

ThreadPoolExecutor 线程池通过队列对任务进行了二层缓冲,提供了多样的拒绝策略。

结果缓存池

到了这里你可能会发现池(Pool)与缓存(Cache)有许多相似之处。

它们之间的一个共同点,就是将对象加工后,存储在相对高速的区域。我习惯性将缓存看作是数据对象,而把池中的对象看作是执行对象。缓存中的数据有一个命中率问题,而池中的对象一般都是对等的。

考虑下面一个场景,jsp 提供了网页的动态功能,它可以在执行后,编译成 class 文件,加快执行速度;再或者,一些媒体平台,会将热门文章,定时转化成静态的 html 页面,仅靠 nginx 的负载均衡即可应对高并发请求(动静分离)。

这些时候,你很难说清楚,这是针对缓存的优化,还是针对对象进行了池化,它们在本质上只是保存了某个执行步骤的结果,使得下次访问时不需要从头再来。

我通常把这种技术叫作结果缓存池(Result Cache Pool),属于多种优化手段的综合。

常量池

常量池分为两个类型,一是.class文件中静态的常量池,二是.class文件中的静态常量池被加载到JVM中而形成的运行时常量池。

  • 类文件中常量池(The Constant Pool)
  • 运行时常量池(The Run-Time Constant Pool)
  • String 常量池

静态常量池

.class文件中的常量池可以看作一个数组,数组中存储了一些常量,当需要在字节码指令中用到这个常量的时候,就通过数组的索引来访问它。

1
2
String m = "hello";
String n = "world";

字节码形式

1
2
3
4
5
6
7
 // 常量池:
 #1 hello
 #2 world
 //...
 
 String m = #1;
 String n = #2;

运行时常量池

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

简单来说,运行时常量池就是.class文件中的静态常量池在JVM中的运行时表示,每一个.class文件的静态常量池都会生成一个对应的运行时常量池。等到JVM在解释String m = #1这条指令时,它可以去这个类的运行时常量池中查找#1的定义。

存在于内存的元空间中

字符串常量池

存在于堆中

字符串池里的内容是在类加载完成,经过验证、准备阶段之后存放在字符串常量池中。

在程序中使用双引号来表示一个字符串时,这个字符串就会进入到 String Pool 中。当然,这里说的是已被加载到 JVM 中的类。

另外,就是 String#intern() 方法,这个方法的作用就是:

  • 如果字符串未在 Pool 中,那么就往 Pool 中增加一条记录,然后返回 Pool 中的引用。
  • 如果已经在 Pool 中,直接返回 Pool 中的引用。

只要 String Pool 中的 String 对象对于 GC Roots 来说不可达,那么它们就是可以被回收的。

数值类缓冲池

Integer 缓存池是有限制的,只能缓存-128~127之间的数字 ,如果定义的两个相同的数字在这个范围之间,可以使用“==”比较值是否相等。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

总结

总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:

  • 对象的创建或者销毁,需要耗费较多的系统资源
  • 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待
  • 对象创建后,通过一些状态重置,可被反复使用