JVM优化
3.jvm 优化
3.1、什么是垃圾回收?
程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了
3.1.1、C/C++语言的垃圾回收
在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete关键字释放内存资源。
如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,最终可能会导致内存溢出
3.1.2、Java语言的垃圾回收
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成
3.2、垃圾回收的常见算法
自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对象,哪些是无效的对象,对于无效的对象就要进行回收处理。
常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法等。
3.2.1 引用计数法
-
原理
假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。 -
优缺点
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember错误。
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。无法解决循环引用问题。(最大的缺点)
- 什么是循环引用
Aa=newA();
Bb=newB();
a.b=b;
b.a=a;
a=null;
b=null;
- 虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收
3.2.2、标记清除法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 标记:从根节点开始标记引用的对象。
- 清除:未被标记引用的对象就是垃圾对象,可以被清理
1.原理
按照根搜索算法,所有从root对象可达的对象就被标记为了存活的对象,没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。
2.优缺点
- 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
- 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
3.2.3、标记压缩算法
1.原理
- 按照根搜索算法,所有从root对象可达的对象就被标记为了存活的对象,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题
2.优缺点
- 优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响
3.2.4、复制算法
1.原理
将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收
- 如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合
2.优缺点
- 优点:在垃圾对象多的情况下,效率较高清理后,内存无碎片
- 缺点:在垃圾对象少的情况下,不适用,如:老年代内存 。分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
3.2.4、复制算法
分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法
3.3.垃圾收集器以及内存分配
在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器
3.3.1 串行垃圾收集器
- 串行垃圾收集器,是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)
对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。一般在Javaweb应用中是不会采用该收集器的。
设置垃圾回收为串行收集器
#-XX:+UseSerialGC 指定年轻代和老年代都使用串行垃圾收集器
#-XX:+PrintGCDetails打印垃圾回收的详细信息
#为了测试GC,将堆的初始和最大内存都设置为16M
[root@fgfg]>java ‐XX:+UseSerialGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m
GC日志信息解读:
年轻代的内存GC前后的大小:
- DefNew 表示使用的是串行垃圾收集器。
- 4416K->512K(4928K) 表示年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K
- 0.0046102secs 表示GC所用的时间,单位为毫秒。
- 4416K->1973K(15872K) 表示GC前,堆内存占有4416K,GC后,占有1973K,总大小为15872K
- FullGC 表示内存空间全部进行GC
3.3.1 串行垃圾收集器
并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)当然了,并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是一样的,只是并行执行,速度更快些,暂停的时间更短一些。
- ParNew垃圾收集器
ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改为了并行。
通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。
测试
#-XX:+UseParNewGC参数设置年轻代使用ParNew回收器
#-XX:+PrintGCDetails打印垃圾回收的详细信息
#为了测试GC,将堆的初始和最大内存都设置为16M
[root@fgfg]>java ‐XX:+UseParNewGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m
- ParallelGC垃圾收集器
ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。
测试
#-XX:+UseParNewGC参数设置年轻代使用ParNew回收器
#-XX:+PrintGCDetails打印垃圾回收的详细信息
#为了测试GC,将堆的初始和最大内存都设置为16M
[root@fgfg]>java ‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐XX:MaxGC
PauseMillis=100‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m
相关参数如下:
- -XX:+UseParallelGC 年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器
- -XX:+UseParallelOldGC 年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
- -XX:MaxGCPauseMillis
- 设置最大的垃圾收集时的停顿时间,单位为毫秒
- 需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。
- 该参数使用需谨慎。
- -XX:GCTimeRatio
- 设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。
- 它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%
- -XX:UseAdaptiveSizePolicy
- 自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。
- 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。
- CMS垃圾收集器
CMS全称ConcurrentMarkSweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,通过参数-XX:+UseConcMarkSweepGC进行设置。
垃圾回收过程:
- 初始化标记(CMS-initial-mark),标记root,会导致stw;
- 并发标记(CMS-concurrent-mark),与用户线程同时运行;
- 预清理(CMS-concurrent-preclean),与用户线程同时运行;
- 重新标记(CMS-remark),会导致stw;
- 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
- 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
- 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
测试
#-XX:+UseParNewGC参数设置年轻代使用ParNew回收器
#-XX:+PrintGCDetails打印垃圾回收的详细信息
#为了测试GC,将堆的初始和最大内存都设置为16M
[root@fgfg]>java ‐XX:+UseConcMarkSweepGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m
3.4 G1垃圾收集器(重点)
G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
- G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
- 第一步,开启G1垃圾收集器
- 第二步,设置堆的最大内存
- 第三步,设置最大的停顿时间
- G1中提供了三种模式垃圾回收模式,YoungGC、MixedGC和FullGC,在不同的条件下被触发。
3.4.1 原理
G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。
这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
在G1划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。
这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,有一种特殊的区域,叫Humongous区域。
- 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。
- 这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
- 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动FullGC。
3.4.2. YoungGC
YoungGC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。
- Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间 * Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。
- 最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是xxRegion的xxCard。
3.4.3. MixedGC
-
当越来越多的对象晋升到老年代oldregion时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即MixedGC,该算法并不是一个OldGC,除了回收整个YoungRegion,还会回收一部分的OldRegion,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些oldregion进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是MixedGC并不是FullGC。
MixedGC什么时候触发?由参数-XX:InitiatingHeapOccupancyPercent=n决定。默认:45%,该参数的意思是:当老年代大小占整个堆大小百分比达到该阀值时触发。
它的GC步骤分2步:
1.全局并发标记(globalconcurrentmarking)
2.拷贝存活对象(evacuation)
1.全局并发标记
全局并发标记,执行过程分为五个步骤:
- 初始标记(initialmark,STW)
* 标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿。 - 根区域扫描(rootregionscan)
* G1GC在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。
* 该阶段与应用程序(非STW)同时运行,并且只有完成该阶段后,才能开始下一次STW年轻代垃圾回收。 - 并发标记(ConcurrentMarking): G1GC在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW年轻代垃圾回收中断。
- 重新标记(Remark,STW): 该阶段是STW回收,因为程序在运行,针对上一次的标记进行修正。
- 清除垃圾(Cleanup,STW): 清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收。
2.拷贝存活对象(evacuation)
Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。
测试
[root@vfvg]# java ‐XX:+UseG1GC‐XX:MaxGCPauseMillis=100‐XX:+PrintGCDetails‐Xmx256m
#扫描根节点
[ExtRootScanning(ms):Min:0.2,Avg:0.3,Max:0.3,Diff:0.1,Sum:0.8]
#更新RS区域所消耗的时间
[UpdateRS(ms):Min:1.8,Avg:1.9,Max:1.9,Diff:0.2,Sum:5.6][ProcessedBuffers:Min:1,Avg:1.7,Max:3,Diff:2,Sum:5][ScanRS(ms):Min:0.0,Avg:0.0,Max:0.0,Diff:0.0,Sum:0.0][CodeRootScanning(ms):Min:0.0,Avg:0.0,Max:0.0,Diff:0.0,Sum:0.0]
#对象拷贝
[ObjectCopy(ms):Min:1.1,Avg:1.2,Max:1.3,Diff:0.2,Sum:3.6]
....
[ClearCT:0.0ms]#清空CardTable
[Other:0.7ms][ChooseCSet:0.0ms]#选取CSet
[RefProc:0.5ms]#弱引用、软引用的处理耗时
[RefEnq:0.0ms]#弱引用、软引用的入队耗时
[RedirtyCards:0.0ms][HumongousRegister:0.0ms]#大对象区域注册耗时
[HumongousReclaim:0.0ms]#大对象区域回收耗时
....
参数说明:
- -XX:+UseG1GC: 使用G1垃圾收集器
- -XX:MaxGCPauseMillis: 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒。
- -XX:G1HeapRegionSize=n 设置的G1区域的大小。值是2的幂,范围是1MB到32MB之间。目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
- -XX:ParallelGCThreads=n 设置STW工作线程数的值。将n的值设置为逻辑处理器的数量。n的值与逻辑处理器的数量相同,最多为8。
- -XX:ConcGCThreads=n 设置并行标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
- -XX:InitiatingHeapOccupancyPercent=n 设置触发标记周期的Java堆占用率阈值。默认占用率是整个Java堆的45%。
对于G1垃圾收集器优化建议
- 年轻代大小
* 避免使用-Xmn选项或-XX:NewRatio等其他相关选项显式设置年轻代大小。
* 固定年轻代的大小会覆盖暂停时间目标。 - 暂停时间目标不要太过严苛
* G1GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间。
* 评估G1GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量
GC日志输出参数
‐XX:+PrintGC 输出GC日志
‐XX:+PrintGCDetails 输出GC的详细日志
‐XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
‐XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013‐05‐04T21:53:59.234+0800)
‐XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
‐Xloggc:../logs/gc.log 日志文件的输出路径
[root@vfvg]# java ‐XX:+UseG1GC‐XX:MaxGCPauseMillis=100‐Xmx256m‐XX:+PrintGCDetails‐XX:+PrintGCTimeStamps‐XX:+PrintGCDateStamps‐XX:+PrintHeapAtGC‐Xloggc:F://test//gc.log
4. JVM优化02
知识要点:
- 4.1 Tomcat8的优化
- 4.2 看懂Java底层
- 4.3 字节码编码的优化建议
4.1 Tomcat8的优化
tomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了。对于tomcat的优化,主要是从2个方面入手,一是,tomcat自身的配置,另一个是tomcat所运行的jvm虚拟机的调优。
4.1.1 搭建tomcat环境
tar‐xvf apache‐tomcat‐8.5.34.tar.gz
cd apache‐tomcat‐8.5.34/conf #修改配置文件,配置tomcat的管理用户
vim tomcat‐users.xml #写入如下内容:
<rolerolename="manager"/> <rolerolename="manager‐gui"/>
<rolerolename="admin"/> <rolerolename="admin‐gui"/>
<userusername="tomcat" password="tomcat" roles="admin‐gui,admin,manager‐gui,manager"/>
#保存退出#如果是tomcat7,配置了tomcat用户就可以登录系统了,但是tomcat8中不行,还需要修改另一个配置文件,否则访问不了,提示403vimwebapps/manager/META‐INF/context.xml#将<Valve的内容注释掉
<Context antiResourceLocking="false" privileged="true">
<!‐‐<ValveclassName="org.apache.catalina.valves.RemoteAddrValve"allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1"/>‐‐>
<ManagersessionAttributeValue ClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>
#保存退出即可#启动tomcatcd/tmp/apache‐tomcat‐8.5.34/bin/./startup.sh&&tail‐f../logs/catalina.out
#打开浏览器进行测试访问http://192.168.40.133:8080/
4.1.2.禁用AJP连接
- 什么是AJP呢?
AJP(ApacheJServerProtocol)AJPv13协议是面向包的。WEB服务器和Servlet容器通过TCP连接来交互;为了节省SOCKET创建的昂贵代价,WEB服务器会尝试维护一个永久TCP连接到servlet容器,并且在多个请求和响应周期过程会重用连接。
我们一般是使用Nginx+tomcat的架构,所以用不着AJP协议,所以把AJP连接器禁用。修改conf下的server.xml文件,将AJP服务禁用掉即可。
<!‐‐ <Connectorport="8009"protocol="AJP/1.3"redirectPort="8443"/> -->
4.1.3 执行器(线程池)
在tomcat中每一个用户请求都是一个线程,所以可以使用线程池提高性能。修改server.xml文件:
<!‐‐将注释打开‐‐>
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="100"/>
<!‐‐
参数说明:
maxThreads:最大并发数,默认设置200,一般建议在500~1000,根据硬件设施和业务来判断
minSpareThreads:Tomcat初始化时创建的线程数,默认设置25
prestartminSpareThreads:在Tomcat初始化的时候就初始化minSpareThreads的参数值,如果不等于true,minSpareThreads的值就没啥效果了maxQueueSize,最大的等待队列数,超过则拒绝请求
‐‐>
<!‐‐在Connector中设置executor属性指向上面的执行器‐‐>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>
- 保存退出,重启tomcat,查看效果
在页面中显示最大线程数为-1,这个是正常的,仅仅是显示的问题,实际使用的指定的值。也有人遇到这样的问题:https://blog.csdn.net/weixin_38278878/article/details/80144397
4.1.4 3种运行模式
tomcat的运行模式有3种:
- bio
- 默认的模式,性能非常低下,没有经过任何优化处理和支持.
- nio
- nio(newI/O),是JavaSE1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Javanio是一个基于缓冲区、并能提供非阻塞I/O操作的JavaAPI,因此nio也被看成是non-blockingI/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。
- pr
- 安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能.
推荐使用nio,不过,在tomcat8中有最新的nio2,速度更快,建议使用nio2.设置nio2:
<Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000"redirectPort="8443"/>
使用ApacheJMeter进行测试
ApacheJmeter是开源的压力测试工具,我们借助于此工具进行测试,将测试出tomcat的吞吐量等信息。下载安装下载地址:http://jmeter.apache.org/download_jmeter.cgi
4.1.5 调整tomcat参数进行优化
通过上面测试可以看出,tomcat在不做任何调整时,吞吐量为73次/秒。
主要有几个方面优化:
-
- 禁用AJP服务.
-
- 设置nio2的运行模式
<!‐‐设置nio2‐->
<Connector executor="tomcatThreadPool"port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"connectionTimeout="20000" redirectPort="8443"/>
-
- 设置线程池参数
通过设置线程池,调整线程池相关的参数进行测试tomcat的性能。
- 设置线程池参数
<!‐‐最大线程数为500,初始为50 吞吐量为128次/秒,性能有所提升‐‐>
<Executorname="tomcatThreadPool" namePrefix="catalina‐exec‐"maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true"/>
<!‐‐最大等待数为100‐‐>
<Executorname="tomcatThreadPool"namePrefix="catalina‐exec‐"maxThreads="500"minSpareThreads="100"prestartminSpareThreads="true"maxQueueSize="100"/>
测试结果:
- 平均响应时间:3.1秒响应时间明显缩短
- 错误率:49.88%错误率提升到一半,也可以理解,最大线程为500,测试的并发为1000
- 吞吐量:238次/秒吞吐量明显
提升结论:响应时间、吞吐量这2个指标需要找到平衡才能达到更好的性能。
- 4.调整JVM参数进行优化
1.设置并行垃圾回收器
#年轻代、老年代均使用并行收集器,初始堆内存64M,最大堆内存512M
JAVA_OPTS="‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms64m ‐Xmx512m ‐XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log"
测试结果与默认的JVM参数结果接近.
1:年轻代的gc有74次,次数稍有多,说明年轻代设置的大小不合适需要调整
2:FullGC有8次,说明堆内存的大小不合适,需要调整
3:吞吐量表现不错,但是gc时,线程的暂停时间稍有点长。
4:在报告中显示,在5次GC时,系统所消耗的时间大于用户时间,这反应出的服务器的性能存在瓶颈,调度CPU等资源所消耗的时间要长一些
5:从GC原因的可以看出,年轻代大小设置不合理,导致了多次GC
2.调整年轻代大小
JAVA_OPTS=" ‐XX:+UseParallelGC ‐XX:+UseParallelOldGC ‐Xms128m ‐Xmx1024m ‐XX:NewSize=64m ‐XX:MaxNewSize=256m ‐XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log"
3:设置G1垃圾回收器
#设置了最大停顿时间100毫秒,初始堆内存128m,最大堆内存1024m
JAVA_OPTS="‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=100 ‐Xms128m ‐Xmx1024m ‐XX:+PrintGCDetails ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCDateStamps ‐XX:+PrintHeapAtGC ‐Xloggc:../logs/gc.log"
4.2 JVM字节码
前面我们通过tomcat本身的参数以及jvm的参数对tomcat做了优化,其实要想将应用程序跑的更快、效率更高,除了对tomcat容器以及jvm优化外,应用程序代码本身如果写的效率不高的,那么也是不行的,所以,对于程序本身的优化也就很重要了。
对于程序本身的优化,可以借鉴很多前辈们的经验,但是有些时候,在从源码角度方面分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还是使用StringBuilder效率高?
这个时候,就需要通过查看编译好的class文件中字节码,就可以找到答案。
我们都知道,java编写应用,需要先通过javac命令编译成class文件,再通过jvm执行,jvm执行时是需要将class文件中的字节码载入到jvm进行运行的
4.2.1 查看字节码
通过javap命令查看class文件的字节码内容
javap‐v Test1.class>Test1.txt #查看并追加到test1.txt文件中
javap 用法:javapjavap -help
内容大致分为4个部分:
第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。
第二部分:显示了该类中所涉及到常量池,共35个常量。
第三部分:显示该类的构造器,编译器自动插入的。
第四部分:显示了main方的信息。(这个是需要我们重点关注的)
1:常量池
# https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140
ConstantType Value 说明
CONSTANT_Class 7 类或接口的符号引用
CONSTANT_Fieldref 9 字段的符号引用
CONSTANT_Methodref 10 类中方法的符号引用
CONSTANT_InterfaceMethodref 11 接口中方法的符号引用
CONSTANT_String 8 字符串类型常量
CONSTANT_Integer 3 整形常量
CONSTANT_Float 4 浮点型常量
CONSTANT_Long 5 长整型常量
CONSTANT_Double 6 双精度浮点型常量
CONSTANT_NameAndType 12 字段或方法的符号引用
CONSTANT_Utf81UTF-8 编码的字符串
CONSTANT_MethodHandle 15 表示方法句柄
CONSTANT_MethodType 16 标志方法类型
CONSTANT_InvokeDynamic 18 表示一个动态方法调用点
2: 字段描述符
# https://docs.or传智播客acle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
B byte signedbyte
C char UnicodecharactercodepointintheBasicMultilingualPlane,encodedwithUTF-16
D double double-precisionfloating-pointvalue
F float single-precisionfloating
I int integer
J long longinteger
LClassName reference aninstanceofclassClassName
S short signedshort
Z boolean trueorfalse
[ reference
2: 字段描述符
Objectm(inti,doubled,Threadt){...}
解析为:(IDLjava/lang/Thread;)Ljava/lang/Object;
3:解读字节码
public static void main(java.lang.String[]);
descriptor:([Ljava/lang/String;)V//方法描述,V表示该方法的放回值为void
flags:ACC_PUBLIC,ACC_STATIC//方法修饰符,public、static的Code://stack=2,操作栈的大小为2、locals=4,本地变量表大小,args_size=1,参数的个数stack=2,locals=4,args_size=10:iconst_2//将数字2值压入操作栈,位于栈的最上面
1:istore_1//从操作栈中弹出一个元素(数字2),放入到本地变量表中,位于下标为1的位置(下标为0的是this)
2:iconst_5//将数字5值压入操作栈,位于栈的最上面
3:istore_2//从操作栈中弹出一个元素(5),放入到本地变量表中,位于第下标为2个位置
4:iload_2//将本地变量表中下标为2的位置元素压入操作栈(5)
5:iload_1//将本地变量表中下标为1的位置元素压入操作栈(2)
6:isub//操作栈中的2个数字相减
7:istore_3//将相减的结果压入到本地本地变量表中,位于下标为3的位置
//通过#2号找到对应的常量,即可找到对应的引用8:getstatic#2
//Fieldjava/lang/System.out:Ljava/io/PrintStream;
11:iload_3//将本地变量表中下标为3的位置元素压入操作栈(3)
//通过#3号找到对应的常量,即可找到对应的引用,进行方法调用
12:invokevirtual #3 //Methodjava/io/PrintStream.println:(I)V
15:return//返回
LineNumberTable://行号的列表
line 6: 0
line 7: 2
line 8: 4
line 9: 8
line 10: 15
LocalVariableTable: //本地变量表
Start Length Slot NameSignature
0 16 0 args [Ljava/lang/String;
...
}
SourceFile:"Test1.java"
-
研究i++与++i的不同
区别:
i++- 只是在本地变量中对数字做了相加,并没有将数据压入到操作栈
- 将前面拿到的数字1,再次从操作栈中拿到,压入到本地变量中
++i - 将本地变量中的数字做了相加,并且将数据压入到操作栈
- 将操作栈中的数据,再次压入到本地变量中小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。
-
字符串拼接
字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种:
* +号拼接:str+"456"
* StringBuilder拼接
* StringBuffer拼接
StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。
那么,问题来了,StringBuilder和“+”号拼接,哪个效率高呢?接下来我们通过字节码的方式进行探究。
- 从解字节码中可以看出,m1()方法源码中是使用+号拼接,但是在字节码中也被编译成了StringBuilder方式。
以得出结论,字符串拼接,+号和StringBuilder是相等的,效率一样. m1()方法中的循环体内,每一次循环都会创建StringBuilder对象,效率低于m2()方法。做循环拼接或大量字符串拼接时,就会创建大量StringBuilder,从而降低性能。
4.2.2 代码优化
- 尽可能使用局部变量
- 尽量减少对变量的重复计算
for(inti=0;i<list.size();i++){...}
替换为
intlength=list.size();
for(inti=0,i<length;i++){...}
- 尽量采用懒加载的策略,即在需要的时候才创建
- 异常不应该用来控制程序流程
- 不要将数组声明为public static final 被外部类所改变
- 不要创建一些不使用的对象,不要导入一些不使用的类
- 程序运行过程中避免使用反射
- 使用数据库连接池和线程池
- 容器初始化时尽可能指定长度
- ArrayList随机遍历快,LinkedList添加删除快
- 使用Entry遍历Map
- 不要手动调用System.gc();
- String尽量少用正则表达式(replace()不支持正则replaceAll()支持正则)
- 日志的输出要注意级别
- 对资源的close()建议分开操作