写点什么

【Java 应用服务体系】「序章入门」全方位盘点和总结调优技术专题指南

作者:洛神灬殇
  • 2023-01-21
    江苏
  • 本文字数:4725 字

    阅读完需:约 16 分钟

【Java应用服务体系】「序章入门」全方位盘点和总结调优技术专题指南

专题⽬标

本系列专题的目标是希望可以帮助读者们系统和全访问掌握应⽤系统调优的思路与方案以及相关的调优工具的使用,虽然未必会覆盖目前的所有的问题场景,但是还是提供了较为丰富的案例和调优理论,会帮助大家打开思维去⽀撑系统服务体系优化能力。

适合人员

Java 相关的开发人员、系统架构师、数据库 DB 人员以及运维人员等

什么是调优

调优手段就是让计算机的硬件或软件在正常地⼯作基础上,非常出色的发挥其应有的性能,并且将所承担的负担降低到最低的技术手段。在 Java 应用服务体系中有大致可以分为 5 个维度的调优方向。

调优技术的五个维度

  • 应⽤⾃身的调优

  • 运⾏环境的调优(JVM 的调优)

  • 存储上的调优(数据库的调优)

  • 操作系统的调优

  • 架构上的调优


如下图所示。



一般从上到下系统优化的层面成本越来越高,而从下到上系统优化层面的成本越来月底,而且难度也适当下降,建议自下而上的去进行调优规划。

调优技术的四条准则

借助监控预防问题、发现问题,监控 + 告警

采用监控和预防的手段去实现提前发现问题:zabbix、promethus 等等

借助⼯具定位问题

问题排查工具使用机制

定期复盘,防⽌同类问题再现

定期进行排查和复盘相关的代码问题,加深我们对问题的印象以及防止问题再次发生

定好规范,⼀定程度上规避问题

制定标准规范,约束问题的发生。

调优的原则

有问题,解决问题。not broken, don't fix.

应⽤调优

应⽤调优-⼯具篇

  • ⼯具旨在帮助我们快速找到应⽤的性能瓶颈。

  • ⽇志分析⼯具⽐较与分析

  • ELK、GrayLog、SLSLog...

  • ELK 搭建与使⽤

  • 现场演示

  • 调⽤链跟踪⼯具与对⽐

  • Skywalking、Sleuth + Zipkin、Jaeger...

  • Skywalking 快速发现性能瓶颈

应⽤调优常⽤技巧-池化技术-对象池

通过复⽤对象,减少对象创建、垃圾回收的开销

适⽤场景

维护⼀些很⼤、创建很慢的对象,提升性能缺点:有学习成本、增加了代码的复杂度

对象池框架

Apache Commons-Pool2
  • 官⽹:https://commons.apache.org/proper/commons-pool

  • GitHub:https://github.com/apache/commons-pool

Commons-Pool2 详解

两⼤类对象池:ObjectPool & KeyedObjectPool

ObjectPool

实现类如下,其中,最重要、功能最强、使⽤最⼴泛的 GenericObjectPool,这个对象池⾮常的强⼤,它⽐较的通⽤,⽽且封装得也⾮常完备。


  • BaseObjectPool:抽象类,⽤来扩展⾃⼰的对象池

  • ErodingObjectPool:“腐蚀”对象池,代理⼀个对象池,并基于 factor 参数,为其添加“腐蚀”⾏为。归还的对象被腐蚀后,将会丢弃,⽽不是添加到空闲容量中。

  • GenericObjectPool:⼀个可配置的通⽤对象池实现。

  • ProxiedObjectPool:代理⼀个其他的对象池,并基于动态代理(⽀持 JDK 代理和 CGLib 代理),返回⼀个代理后的对象。该对象池主要⽤来增强对池化对象的控制,⽐如防⽌在归还该对象后,还继续使⽤该对象等。

  • SoftReferenceObjectPool:基于软引⽤的对象池

  • SynchronizedObjectPool:代理⼀个其他对象池,并为其提供线程安全的能⼒。


核⼼API 如下


  • borrowObject() 从对象池中借对象

  • returnObject() 将对象归还到对象池

  • invalidateObject() 失效⼀个对象

  • addObject() 增加⼀个空闲对象,该⽅法适⽤于使⽤空闲对象预加载对象池

  • clear() 清空空闲的所有对象,并释放相关资源

  • close() 关闭对象池,并释放相关资源

  • getNumIdle() 获得空闲的对象数量

  • getNumActive() 获得被借出对象数量

KeyedObjectPool

这种对象池和 ObjectPool 的区别在于,它是通过 key 找对象的,从设计上来看和 ObjectPool 没什么区别。实现类如下,使⽤最⼴的是 GenericKeyedObjectPool。


  • ErodingKeyedObjectPool 类似 ErodingObjectPool

  • GenericKeyedObjectPool 类似 GenericObjectPool

  • ProxiedKeyedObjectPool 类似 ProxiedObjectPool

  • SynchronizedKeyedObjectPool 类似 SynchronizedObjectPool


使⽤


new GenericObjectPool(PooledObjectFactory<T> factory)new GenericObjectPool(PooledObjectFactory<T> factory, GenericObjectPoolConfig<T> config)new GenericObjectPool(PooledObjectFactory<T> factory, GenericObjectPoolConfig<T> config, AbandonedConfig abandonedConfig)
复制代码


最重要的参数是 PooledObjectFactory,⼀般来说,⼯⼚是需要我们⾃⼰根据业务需求去实现的。它是⽤来创建对象的,这其实就是设计模式⾥⾯的⼯⼚模式。


⽬前 PooledObjectFactory 有两个实现类。


  • BasePooledObjectFactory:抽象类,⽤于扩展⾃⼰的 PooledObjectFactory

  • PoolUtils.SynchronizedPooledObjectFactory:内部类,代理⼀个其他的 PooledObjectFactory,实现线程同步,⽤ PoolUtils.synchronizedPooledFactory() 创建


Factory 核⼼⽅法:


  • makeObject 创建⼀个对象实例,并将其包装成⼀个 PooledObject

  • destroyObject 销毁对象

  • validateObject 校验对象,确保对象池返回的对象是 OK 的

  • activateObject 重新初始化对象

  • passivateObject 取消初始化对象。GenericObjectPool 的 addIdleObject、returnObject、evict 调⽤该⽅法。

Commons-Pool2 总体分析

  • ObjectPool:对象池,最核⼼:GenericObjectPool、 GenericKeyedObjectPool。

  • Factory:创建 &管理 PooledObject,⼀般要⾃⼰扩展

  • PooledObject:包装原有的对象,从⽽让对象池管理,⼀般⽤DefaultPooledObject 即可

Factory 示例
class MyPooledObjectFactory implements PooledObjectFactory<Model> {    public static final Logger LOGGER = LoggerFactory.getLogger(MyPooledObjectFactory.class);    @Override   public PooledObject<Model> makeObject() throws Exception {      DefaultPooledObject<Model> object = new DefaultPooledObject<>(new Model(1, "S"));       LOGGER.info("makeObject..state = {}", object.getState());      return object;    }  @Override  public void destroyObject(PooledObject p) throws Exception{      LOGGER.info("destroyObject..state = {}", object.getState());   }  @Override  public boolean validateObject(PooledObject p) {      LOGGER.info("validateObject..state = {}", object.getState());      return true;   }  @Override  public void activateObject(PooledObject p) throws Exception{    LOGGER.info("activateObject..state = {}", p.getState());   }@Override  public void passivateObject(PooledObject p) {      LOGGER.info("passivateObject..state = {}", object.getState());      return true;   }
复制代码


所有操作面向的都是 PooledObject 这个参数,makeObject 返回的是 PooledObject,其他 API 为什么操作的也是 PooledObject,⽽不是直接操作我们创建的对象呢?


这其实也是 commons-pool 设计巧妙之处。Pooledobject 可以对原始对象进⾏包装,从⽽被对象池管理。⽬前 pooledobject 有两个实现类:


  • DefaultPooledObject:包装原始对象,实现监控(例如创建时间、使⽤时间等)、状态跟踪等

  • PooledSoftReference:封装了 DefaultPooledObject,⽤来和 SoftReferenceObjectPool 配合使⽤。

DefaultPooledObject 定义了对象的若⼲种状态
  • IDLE 对象在队列中,并空闲。

  • ALLOCATED 使⽤中(即出借中)

  • EVICTION 对象当在队列中,正在进⾏驱逐测试

  • EVICTION_RETURN_TO_HEAD 对象驱逐测试通过后,放回到队列头部

  • VALIDATION 对象当前在队列中,空闲校验中

  • VALIDATION_PREALLOCATED 对象当前不在队列中,出借前校验中 VALIDATION_RETURN_TO_HEAD 对象当前不在队列中,校验通过后放回头部 INVALID 对象失效,驱逐测试失败、校验失败、对象销毁,都会将对象置为 INVALID。

  • ABANDONED 放逐中,如果对象上次使⽤时间超过 removeAbandonedTimeout 的配置,则将其标记为 ABANDONED。标记为 ABANDONED 的对象即将变成 INVALID。

  • RETURNING 对象归还池中。

JVM 调优

本系列专题将针对于 Oracle Java HotSpot 虚拟机为为开发者们提供不同的 Java Heap 内存空间的较为深入的分析介绍。对于任何接触的开发者都是非常重要的理论依据。频繁遇到的内存问题,提供生产环境的优化调整。那么适当的实战层级的 Java 虚拟机的内存空间分析能力是至关重要的。

前提概述

  • Java 虚拟机是你的 Java 程序运行的基础,它为你提供动态的分配内存服务、垃圾收集、线程调度和切换、IO 处理和本机操作等

  • Java 堆空间是运行时 Java 程序的内存“容器”,它提供给您的 Java 应用程序所需的适当内存空间(Java 堆、本机堆),并由 JVM 本身去管理。

JVM HotSpot 内存被划分 2 类和 5 空间:

  • Heap 堆内存空间:属于线程共享区域,也是我们 JVM 的内存管理范畴的最大的一部分运行时内存区域。

  • 方法区(永久代/元空间):属于线程共享区域,往往我们会忽略了这个区域的内存回收能力。

  • 本地堆 (C-Heap):本地方法的调用栈。

  • 虚拟机栈:Java 方法的调用栈。


Heap 堆内存空间

JVM 的堆空间的变化在<18 的版本之内,主要有一个分水岭,主要集中在 8 之前和 8 之后。

JDK8 之前的对空间

JDK8 之前的 Heap 空间如下图所示:



JDK8 之后的 Heap 空间如下图所示:



主要时针对于方法区的实现机制:永久代 -> 元空间结构模型,接下来我们看看元数据空间在方法区中的分布结构模型。

后续版本中的-元空间和方法去的内存才能出分配关系


可以看到 JDK8 之后,方法去的实现有元空间和一部分堆内存组成。之前主要只有单纯的永久代去实现的。

常量池

常量池主要有静态常量池和运行时常量池组成。


  • 类信息

  • 类的版本

  • 字段描述信息

  • 方法描述信息

  • 接口和父类等描述信息

  • class 文件常量池(静态常量池)


静态常量池,也叫 class⽂件常量池,主要存放:


  • 字⾯量:例如⽂本字符串、 final 修饰的常量。

  • 符号引⽤:例如类和接⼝的全限定名、字段的名称和描述符、⽅法的名称和描述符。


运⾏时常量池


当类加载到内存中后,JVM 就会将静态常量池中的内容存放到运⾏时的常量池中;运⾏时常量池⾥⾯存储的主要是编译期间⽣成的字⾯量、符号引⽤等等。如下图对应的字符串常量在字符串常量池中的存储模式。



字符串常量池


字符串常量池,也可以理解成运⾏时常量池分出来的⼀部分,类加载到内存的时候,字符串,会存到字符串常量池⾥⾯。


对象和类在内存分布

针对于代码的执行和存储在 JVM 的分布,主要集中在栈空间和堆空间、方法区。它们各个的职能不同,对应的能力也是不同的。我们针对于一段代码块进行分析和介绍

虚拟机栈的基本结构模型


代码在堆栈中的存储结构信息


内存泄漏怎么排查[java 内存溢出排查]

top 等查看系统内存概况

top:显示所有进程运行情况,按 M 键按照内存大小排序。

使用格式

top [-] [d] [p] [q] [c] [C] [S] [s] [n]
复制代码

参数说明

  • d:指定每两次屏幕信息刷新之间的时间间隔,当然用户可以使用 s 交互命令来改变之

  • p:通过指定监控进程 ID 来仅仅监控某个进程的状态。

  • q:该选项将使 top 没有任何延迟的进行刷新。如果调用程序有超级用户权限,那么 top 将以尽可能高的优先级运行。

  • S:指定累计模式。

  • s:使 top 命令在安全模式中运行。这将去除交互命令所带来的潜在危险。

  • i:使 top 不显示任何闲置或者僵死进程。

  • c:显示整个命令行而不只是显示命令名。

命令说明

  • jmx 快速发现 jvm 中的内存异常项

【实战阶段】JVM 排查问题优化参数

jps [-q] [-mlvV] [<hostid>]
复制代码

参数如下:

  • -q 只显示进程号

  • -m 显示传递给 main⽅法的参数

  • -l 显示应⽤main class 的完整包名应⽤的 jar⽂件完整路径名

  • -v 显示传递给 JVM 的参数

  • -V 禁⽌输出类名、JAR⽂件名和传递给 main⽅法的参数,仅显示本地 JVM 标识符的列表

hostid 的参数格式

  • hostid:想要查看的主机的标识符,格式为: [protocol:][[//]hostname][:port][/servername] ,其中:

  • protocol:通信协议,默认 rmi

  • hostname:⽬标主机的主机名或 IP 地址

  • port:通信端⼝,对于默认 rmi 协议,该参数⽤来指定 rmiregistry 远程主机上的端⼝号。如省略该参数,并且该

  • protocol 指示 rmi,则使⽤默认使⽤1099 端⼝。

  • servicename:服务名称,取值取决于实现⽅式,对于 rmi 协议,此参数代表远程主机上 RMI 远程对象的名称


今天就写到这里,未完待续,等待下一部分的内容。

发布于: 刚刚阅读数: 3
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

【个人简介】酷爱计算机科学、醉心编程技术、喜爱健身运动、热衷悬疑推理的“极客达人” 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、微服务/分布式体系和算法设计等

评论

发布
暂无评论
【Java应用服务体系】「序章入门」全方位盘点和总结调优技术专题指南_Java_洛神灬殇_InfoQ写作社区