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 引用计数法


  1. 原理
    假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。

  2. 优缺点
    优点:

  • 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
  • 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember错误。
  • 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

  • 每次对象被引用时,都需要去更新计数器,有一点时间开销。浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。无法解决循环引用问题。(最大的缺点)
  1. 什么是循环引用
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 串行垃圾收集器

并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)当然了,并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是一样的,只是并行执行,速度更快些,暂停的时间更短一些。

  1. ParNew垃圾收集器
    ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改为了并行。
    通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。

测试

#-XX:+UseParNewGC参数设置年轻代使用ParNew回收器
#-XX:+PrintGCDetails打印垃圾回收的详细信息
#为了测试GC,将堆的初始和最大内存都设置为16M
[root@fgfg]>java ‐XX:+UseParNewGC ‐XX:+PrintGCDetails ‐Xms16m ‐Xmx16m
  1. 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模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。
    • 一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。
  1. 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性能调优,开发人员只需要简单的三步即可完成调优:
  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间
  • 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

  1. 当越来越多的对象晋升到老年代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"/>

4.1.4 3种运行模式


tomcat的运行模式有3种:

  1. bio
  • 默认的模式,性能非常低下,没有经过任何优化处理和支持.
  1. nio
  • nio(newI/O),是JavaSE1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Javanio是一个基于缓冲区、并能提供非阻塞I/O操作的JavaAPI,因此nio也被看成是non-blockingI/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。
  1. 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次/秒。
主要有几个方面优化:

    1. 禁用AJP服务.
    1. 设置nio2的运行模式
<!‐‐设置nio2‐->
<Connector executor="tomcatThreadPool"port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"connectionTimeout="20000" redirectPort="8443"/>
    1. 设置线程池参数
      通过设置线程池,调整线程池相关的参数进行测试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 用法:javap javap -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 代码优化

  1. 尽可能使用局部变量
  2. 尽量减少对变量的重复计算
for(inti=0;i<list.size();i++){...}
替换为
intlength=list.size();
for(inti=0,i<length;i++){...}
  1. 尽量采用懒加载的策略,即在需要的时候才创建
  2. 异常不应该用来控制程序流程
  3. 不要将数组声明为public static final 被外部类所改变
  4. 不要创建一些不使用的对象,不要导入一些不使用的类
  5. 程序运行过程中避免使用反射
  6. 使用数据库连接池和线程池
  7. 容器初始化时尽可能指定长度
  8. ArrayList随机遍历快,LinkedList添加删除快
  9. 使用Entry遍历Map
  10. 不要手动调用System.gc();
  11. String尽量少用正则表达式(replace()不支持正则replaceAll()支持正则)
  12. 日志的输出要注意级别
  13. 对资源的close()建议分开操作
posted @ 2021-06-23 13:12  易阳羽之灵异  阅读(96)  评论(0编辑  收藏  举报