Java性能权威指南 Dear 丶 2022-03-06 12:48 341阅读 0赞 JVM主要接受两类标志: 布尔标志—— -XX:+FlagName表示开启,-XX:-FlagName表示关闭 附带参数的标志—— -XX:FlagName=something,表示将标志flagname的值设置为something 性能测试的方法 1. 微基准测试,用来测量微小代码单元的性能,包括调用同步方法的用时与非同步方法用时的比较,创建线程的代价与使用线程池的代价,执行某种算法的耗时与其替代实现的耗时 2. 宏基准测试,整个项目 3. 介基准测试,做一些实际工作,但不是完整应用的基准测试 吞吐量测试,基于一段时间内所能完成的工作量:每秒事务数(TPS)、每秒请求数(RPS)、每秒操作数(OPS) 相应时间测试,从客户端发送请求至收到响应之间流逝的时间 用统计的方法应对性能的变化,求得一个平均值 CPU使用率分为两类:用户态时间(CPU执行应用代码所占时间的百分比)和系统态时间(CPU执行内核代码所占时间的百分比) 性能调优在CPU方面的目的是在尽可能短的时间内让CPU使用率尽可能高,注意是提升而不是降低 磁盘使用率,写入磁盘的应用遇到瓶颈,是因为写入数据的效率不高(吞吐量太低),或者是因为写入太多数据(吞吐量太高) 网络使用率 Java监控工具:jcmd、jconsole、jhat、jmap、jinfo、jstack、jstat、jvisualvm、 做的事情:基本的vm信息、线程信息、类信息、实时GC分析、堆转储的事后处理、JVM的性能分析 性能分析有两种模式:数据采样或数据探查 垃圾收集由两步构成:查找不再使用的对象,并且释放这些对象所管理的内存。 分代垃圾收集器,根据情况将堆划分成不同的代(Generation),有老年代(Old Generation或Tenured Generation)和新生代(Young Generation)。 新生代又被进一步划分为不同的区段,又Eden空间(新创建的对象)和Survivor空间(新生代Minor GC未被回收的对象) Minor GC当新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间,没有被其他对象引用的对象会被回收,仍然使用的对象会被放在其他地方。 Full GC,直接停掉所有应用线程,找到不再使用的对象,对其进行回收,接着对堆空间进行整理。可能会导致应用程序线程长时间的停顿。新生代与老生代会一起被回收。 CMS和G1收集器被称为Concurrent垃圾收集器,不停止应用线程的情况下找到不再使用的对象,同时会使用不同的方法对老年代空间进行压缩。减少停顿时间的同时会增加CPU的消耗 JVM垃圾回收算法:Serial垃圾收集器、Throughput垃圾收集器、CMS收集器、G1垃圾收集器 调整堆得大小,堆过小会造成频繁GC,但堆变大,GC停顿造成的时间会更多 调整堆大小的首要原则是永远不要将堆得容量设置得比机器的物理内存大,如果有多个JVM,所有JVM堆内存的总和不能比机器的物理内存大。 设置堆:初始值(-Xms) 和最大值(-Xmx) 默认堆大小 <table> <tbody> <tr> <td style="border-color:#a3a3a3;vertical-align:top;width:1.3868in;"> <p style="margin-left:0in;">设备</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:3.3486in;"> <p style="margin-left:0in;">初始堆大小</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:2.8423in;"> <p style="margin-left:0in;">最大堆大小</p> </td> </tr> <tr> <td style="border-color:#a3a3a3;vertical-align:top;width:1.4062in;"> <p style="margin-left:0in;">Linux32位客户端</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:3.3486in;"> <p style="margin-left:0in;">16M</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:2.8229in;"> <p style="margin-left:0in;">256M</p> </td> </tr> <tr> <td style="border-color:#a3a3a3;vertical-align:top;width:1.4062in;"> <p style="margin-left:0in;">Linux32位服务器</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:3.3486in;"> <p style="margin-left:0in;">64M</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:2.8951in;"> <p style="margin-left:0in;">取1G和物理内存大小1/4二者中的最小值</p> </td> </tr> <tr> <td style="border-color:#a3a3a3;vertical-align:top;width:1.4062in;"> <p style="margin-left:0in;">Linux64位服务器</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:3.368in;"> <p style="margin-left:0in;">取512MB和物理内存大小1/64二者中的最小值</p> </td> <td style="border-color:#a3a3a3;vertical-align:top;width:2.9743in;"> <p style="margin-left:0in;">取32G和物理内存大小1/4二者中的最小值</p> </td> </tr> </tbody> </table> 将堆的初始值和最大值设置成为一样的意义在于jvm不再需要估算堆是否需要调整大小。 代空间的调整 如果新生代分配得比较大、垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少,老年代也会相对较小,比较容易被填满,更频繁地触发Full GC。因此需要一个平衡。 \-XX:NewRatio=N // 设置新生代与老年代的空间占用比例 \-XX:NewSize=N // 设置新生代空间的初始大小 \-XX:MaxNewSize=N // 设置新生代空间的最大大小 \-XmnN // 将NewSize和MaxNewSize设定为同一值 最初新生代空间的大小是初始堆大小的33%,新生代以外的堆内存被分配给老年代 Initial Young Gen Size = Initial Head Size/(1+NewRatio) 使用NewSize确定的新生代的大小,其优先级要高于通过NewRatio计算出来的大小 在整个堆得范围内,不同代的大小的划分是由新生代所占用的空间控制的。 新生代的大小会随着整个堆的大小的增长而增长,但也会随着整个堆的空间比率波动变化的(根据新生代的初始值和最大值) 永久代(Permgen或Permanent Generation)与元空间(Metaspace) 永久代与元空间保存着类的元数据,它以分离的堆得形式存在 JVM在堆的内部如何调整新生代及老生代的百分比是自适应调整机制控制的。通常情况下,我们应该开启自适应调整,但对于稳定的已经经过精细调优的堆,关闭自适应调整能获得一定的性能提升。 GC日志是分析GC相关问题的重要线索,我们应该开启GC日志标志,即使在生产服务器上 使用PrintGCDetails标志可以获得更详尽的GC日志 使用jstat能动态地观察运行程序的垃圾回收操作 MaxGCPauseMillis标志用于设定应用可承受的最大停顿时间。 GCTimeRatio用于设置希望应用程序在垃圾回收上面花费时间的百分比,默认是99 避免发生并发模式失效是提升CMS收集容器处理能力、获得高性能的关键。避免发生并发模式失效最简单的方法是增大堆的容量。否则我们能进行的下一个步骤就是通过调整CMSInitiatingOccupancy-Fraction参数,今早启动并发后台线程的运行。 在JVM调优时主要要考虑的几个问题: 1. 你的应用能够忍受Full GC停顿吗?如果可以忍受,选择Throughput收集器可以获得最高性能,同时使用的CPU和堆得大小也比其他的垃圾收集器少。否则需要根据堆大小进行选择,如果堆较小,可以选择并发收集器,如CMS收集器或者G1收集器,如果堆较大,则推荐使用G1收集器。 2. 使用默认设置能够达到你的期望的性能目标吗?自动调优绝大多数情况下取得的效果是最小的,如果使用默认设置没能达到性能目标,分析垃圾收集器是否是性能瓶颈,根据垃圾收集日志了解垃圾收集的频率和占用的时间,如果JVM花在垃圾收集器上的时间不超过3%,即使进行垃圾调优也不会得到太大的性能提升。 3. 应用的停顿时间与你的预期目标接近吗?可以调整最大停顿时间为你的预期目标时间,如果停顿时间过长,但是应用的吞吐量正常,可以尝试减小新生代的大小,如果瓶颈是Full GC的停顿,就减少老年代的大小,调整后停顿频率会增加,停顿时间会减小。 4. 应用吞吐量上不去?此时考虑增加堆得大小,至少增加新生代的大小。 5. 如果CPU资源充足,可以考虑使用并发收集器 减少对象大小,节约系统对内存的使用我,往往可以改进GC的效率。 对象的重用有两种实现方式:对象池和线程局部变量。 应用总的内存占用量=原生内存+堆内存 当一个线程离开某个同步块时,必须将任何修改过的值刷新回主内存。 线程同步有两个性能方面的开销,限制了应用的可伸缩性,另一个是获取锁的开销 ## 数据库性能 ## Oracle的瘦驱动程序(让数据库处理更多的事情,比如大量使用函数和存储过程)与胖驱动程序(让应用程序处理更多的事情,目前更主流) 理解:数据库处理事情相对方便,但是随着业务的更加庞大,用过多的函数、存储过程会提升系统维护的难度,程序员必须纯熟每个表,表与表之间的关系,视图的意义,函数和存储过程的定义实现,才能很好完成工作,在团队作战,人员更新迭代更快的时候,知识存在断档,学习成本应当越低越好。因此现在会将更多的业务集中在应用层处理。同时应用层定制化的能力更强,优化也更容易,随着分布式微服务技术的成熟,从横向拓展角度对性能的提升也更加强大。 预处理语句和语句池—— PreparedStatement,尽量避免直接使用Statement 预处理语句让数据库可以重用已经执行过的语句,比方仅仅替换查询参数,节省预处理环节的时间 预处理语句基于每个连接进行工作,每个连接拥有自己的连接池 预处理语句会消耗大量的堆空间,但要注意由此引发的gc问题。 对于连接池而言,首要原则是应用程序的每个线程都持有一个连接,因此一般业务场景,将连接池数量与线程池数量设为一样。 数据库连接对象初始化的代价是非常昂贵的,因此要引入数据库连接池。 理解:连接池可以节省连接初始化的时间,可以缓存预处理语句,连接池占有空间更多的是预处理对象,此处要权衡连接池的数量,可能占用的空间(避免频繁GC)。同时连接池数量还需要由数据库自身的性能决定,不能超过数据库能承受的负载。 数据库事务影响性能的原因在于两点,首先,数据库事务的设置和提交都会耗费时间,包括IO操作和检查数据库事务的一致性;其次是进行事务期间,事务要对部分数据加锁,两个事务在同一数据库行锁上发生竞争,应用的拓展性会受到影响。 思考:数据库锁的粒度,能否细化到某个字段或者某几个字段,当业务只对部分行内某几个数据加锁时,对行加锁就会产生不必要的性能消耗,同理,代码执行事务阶段,尽量不要让不相干代码进入事务中,降低阻塞的影响。 事务提交相对消耗性能,此处在批处理的时候有个权衡,带事务的批处理可以一次提交,可以获得最佳性能,但如果失败,会造成大量重复工作,影响用户体验;但是如果一个一个提交,会使事务提交占用的时间更多。 基本的事务隔离模式(按性能开销最大到最小排列) TRANSACTION\_SERIALIZABLE:要求在事务执行期间,事务涉及的所有数据都会被锁定,不能读也不能写。 TRANSACTION\_REPEATABLE\_READ: 事务进行期间,所访问的数据都会被锁定。不过其他事务可以随时向表里插入新纪录,也可以读。这种模式会发生还“幻读”。 TRANSACTION\_READ\_COMMITTED: 事务运行期间,只有正在写入的行会被锁定。这种模式会发生“不可重复读”问题,一个时间点读到的数据,另一个时间点再次读取时就完全不同了。 TRANSACTION\_READ\_UNCOMMITTED: 事务运行期间不会加任何锁。一个事务可以同时读取另一个事务写入的数据(尚未提交),会造成“脏读”。 Mysql默认是TRANSACTION\_REPEATABLE\_READ模式;Oracle默认是TRANSACTION\_READ\_COMMITTED。 JDBC中为了细粒度地控制事务,可以默认使用TRANSACTION\_READ\_UNCOMMITTED隔离级别,然后显式的按需锁定数据。 结果集处理 需要查询大量数据的应用程序应该考虑增大数据提取缓存区的大小。 一个取舍,应用程序载入大量数据,增加应用服务器内存的开销;频繁的进行数据库调用,每次获取数据集的一部分。 对于大文件字段应剥离处理,避免大文件严重影响查询性能。
还没有评论,来说两句吧...