zoukankan      html  css  js  c++  java
  • 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()建议分开操作
  • 相关阅读:
    手把手教会你如何通过C#创建Windows Service
    推荐几款软件界面模型设计工具
    visual studio 2010小技巧
    C# 枚举在属性中运用
    C# Stream 和 byte[] 之间的转换
    推荐一款DataGridView的打印解决方案
    VB提高专辑VB编写自定义类(下)
    vb 怎么把长整型转字符串
    Android NAND: nand_dev_load_disk_state, restore failed: size required (3546398242485400641) exceeds device limit (6920
    VB中各种类型的转换
  • 原文地址:https://www.cnblogs.com/yiyangyu/p/jvm0003.html
Copyright © 2011-2022 走看看