池化技术
像线程资源、数据库连接资源或者 TCP 连接等,这类对象的初始化通常要花费比较长的时间,如果频繁地申请和销毁,就会耗费大量的系统资源,造成不必要的性能损失。
并且这些对象都有一个显著的特征,就是通过轻量级的重置工作,可以循环、重复地使用。
这个时候,我们就可以使用一个虚拟的池子,将这些资源保存起来,当使用的时候,我们就从池子里快速获取一个即可。
在 Java 中,池化技术应用非常广泛,常见的就有数据库连接池、线程池等
Apache Commons Pool
开源软件库 Apache Commons Pool 提供了一个对象池 API 与含多个对象池的实现。
|
|
对象池的核心类
泛型接口 PooledObjectFactory<T>
管理池对象的生命周期。一般通过继承 BasePooledObjectFactory
来定制。
GenericObjectPool 提供多种配置选项,包括限制空闲或活动实例的数量、在池中空闲时驱逐实例等。从版本 2 开始,GenericObjectPool 还提供废弃实例跟踪和删除功能。
|
|
GenericKeyedObjectPool 为键控池提供相同的行为。
SoftReferenceObjectPool 可以根据需要增长,但允许垃圾收集器根据需要从池中逐出空闲实例。
|
|
Jedis 中 JedisPool、ConnectionPool 都是使用的 Apache Commons Pool
|
|
数据库连接池 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文件中的常量池可以看作一个数组,数组中存储了一些常量,当需要在字节码指令中用到这个常量的时候,就通过数组的索引来访问它。
|
|
字节码形式
|
|
运行时常量池
运行时常量池是在类加载完成之后,将每个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 初始化的时候就会读取该系统属性来决定上界。
总结
总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:
- 对象的创建或者销毁,需要耗费较多的系统资源
- 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待
- 对象创建后,通过一些状态重置,可被反复使用