栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

性能优化篇

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

性能优化篇

性能优化 性能优化相关概念 如何理解JDK、JRE和JVM?
  • JDK(Java Develop Kit):Java开发工具。编译成对应特定机器码。

  • JRE(Java Resource Environment):Java运行环境。只能运行.class文件,不能编译.class文件。

    JRE + 各种Java工具(javac/java/jdb等) + Java基础的类库(即Java API 包括rt.jar) 等效于 JDK。
    其中,rt.jar是基础类库。
    
  • JVM(Java Virtual Machine):Java虚拟机。编译成.class文件。

    JVM + 类库lib 等价于 JRE。
    
  • 简单来说就是:JDK包含JRE,JRE包含JVM的关系。

如何理解Java语言的一次编译,到处运行?
  • 机器码(machine code/native code):电脑CPU能读懂的语言。底层,执行快,晦涩难懂。

  • 每个操作系统的指令是不同的,即:不同操作系统机器码不同。(不同的语言体系哦!)

  • JDK(Java Develop Kit)是区分了操作系统的,即:不同操作系统,针对其特定的机器码,有不同的JDK。

  • JVM(Java Virtual Machine):运行在操作系统之上。结合字节码文件,在软件层面,屏蔽了不同操作系统在底层硬件与指令上的区别。

  • Java字节码文件(.class文件):中间状态(中间码)的二进制代码(文件),源码经过Java编译器转为字节码,字节码经过虚拟机内嵌的解释器将字节码转为机器码。运行在JVM之上,且都统一标准,遵守JVM规范。

  • 所以,操作系统<->机器码<->JDK,是一一对应的关系。即:字节码可以理解为统一了不同操作系统机器指令的上层抽象标准(JVM规范),JVM是字节码的翻译官,不同JDK是其对应操作系统指令的编译官。

  • 操作系统的机器指令执行从自说自话变为都说普通话。代价是中转了一层字节码(性能减分),好处是一次编译,到处运行(便捷加分)。亦是一种取舍,一种智慧。

Java是编译执行还是解释执行?
  • 编译执行:将高级语言的源码编译成机器语言的目标程序,以此作为编译和执行的粒度,进行执行。编译执行有点批量预处理的感觉,先把执行前的准备工作一次性搞定,后面执行无需兼顾其他,有了前期的铺垫,所以执行阶段快。在程序运行时,随着时间的推移,编译器逐渐发挥作用根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
  • 解释执行:将高级语言的源码一句一句的翻译,计算机一句一句的执行,无目标程序产生。解释执行好处是一次编译(整体编译为字节码文件,再由字节码一句一句编译为机器码),到处运行(便捷加分),且在启动阶段省去了编译的时间,立即执行。缺点除了中转了一层字节码(性能减分),在安全层面,字节码文件更容易被反编译破解,因为和存粹的机器指令相比,字节码更易懂。当程序需要迅速启动的时候,解释器可以首先发挥作用,省去了编译的时间,立即执行。解释执行占用更小的内存空间。同时,当编译器进行的激进优化失败的时候,还可以进行逆优化来恢复到解释执行的状态。
  • Java即有编译执行,也有解释执行。Why?往下看。
什么是JIT?
  • 如果把.class文件的生成看作是编译(只不过不是直接编译成机器指令形成目标文件而已),那么,将该过程理解为编译执行也未尝不可。但是,其中的.class文件执行过程是解释执行的,也就是一句一句的翻译。
  • 如果.class文件一行语句解释执行一次,好像影响不大。
  • 但是,也会有需要执行多次的代码,比如,for循环、热点代码块等。每次都重复字节码解释成机器指令的过程是一种浪费。怎么解?缓存。
  • 既然你总被调用,那就将你直接编译成机器码缓存起来,随用随取。
  • 简单理解,上面描述的就是即时编译的思想。Just In Time,简称JIT。
  • 那么,问题来了,如何判断是否需要缓存?掌握好JIT的度呢?(缓存也是一种资源哦!)
  • 我们无法预先知道哪些代码是热点代码,所以只能在执行过程中去统计分析判别。
  • 比如,一个for循环,当循环累计达到某一阈值(计数器统计),就可以认定其为热点代码,就可以编译为机器码缓存起来,大于阈值后的循环执行就可以直接使用缓存的机器码,而无需每次解释执行。该例子描述的情景,就是栈上替换的过程。而通过计数器统计的方式,针对for或者while循环的情况,称为回边计数器(Back Edge Counter)。如果是统计某个方法被调用的次数,则称为方法计数器(Invocation Counter),默认阈值在Client模式下是1500次,在Server模式下是10000次,可人为修改。而基于计数器探测热点的方法,称为基于计数器的热点探测(Counter based Hot Spot Detection)。HotSpot虚拟机中采用的就是这种热点探测方法。
  • 无独有偶,另一种JVM的热点探测(Hot Spot Detection)方法,周期性地检测各个线程的栈顶,如果发现某个方法经常出现在栈顶,换句话说就是某个方法频繁被调用,导致频繁入栈和出栈,那么就可以认为这个方法是热点代码。它的缺点是无法精确确认一个方法的热度,容易受线程阻塞或别的原因干扰探测的准确性。我们称之为基于采样的热点探测(Sample based Hot Spot Detection)。
  • 其实,HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器。HotSpot编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体选用的编译器,取决于虚拟机运行的模式。用户也可以使用 -client和 -server参数强制指定虚拟机运行在Client模式或者Server模式,此种配合使用的方式称为“混合模式”(Mixed Mode)。同时,用户可以使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。另外,使用 -Xcomp 强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程。通过虚拟机 -version 命令可以查看当前默认的运行模式。
Java为什么需要编译执行?
提前编译(Ahead Of Time,AOT)
即时编译(Just In Time,JIT)
解释器(Interpreter)
编译器(Compiler)
客户端编译器(Client Compiler,C1)
服务端编译器(Server Compiler,C2,也叫Opto编译器)
Graal编译器(JDK 10 出现用于替代 C2)
混合模式(Mixed Mode)默认
解释模式(Interpreted Mode)-Xint
编译模式(Compiled Mode)-Xcomp

解释执行:启动速度快。
C1编译执行:预热较快,运行时,执行快,相对更高的编译速度。(一般编译优化)
C2编译执行:需要较慢,运行时,执行快,相对更好的编译质量。(彻底编译优化)

Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或者代码块的执行特别频繁,就会把这些代码认定为 “热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。

静态编译:一次性编译。在编译的时候把你所有的模块都编译进去。
动态编译:按需编译。程序在运行的时候,用到那个模块就编译哪个模块。
  • 分层编译:在分层编译的工作模式出现前,HotSpot虚拟机通常是采用解释器与其中一个编译器直接搭配的方式工作。为了在程序启动相应速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译功能。(预编译优化的度的掌握)

    level 0:interpreter 解释执行。
    level 1:C1 编译,无 profiling(性能监控)。
    level 2:C1 编译,仅方法及循环 back-edge 执行次数的 profiling。
    level 3:C1 编译,除 level 2 中的 profiling 外还包括 branch(针对分支跳转字节码)及 receiver type(针对成员方法调用或类检测,如 checkcast,instnaceof,aastore 字节码)的 profiling。
    level 4:C2 编译。
    
什么是逃逸分析?
  • 什么是逃逸分析?简单理解就是对指针的作用域和生命周期的分析。

    当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸。

    逃逸分析确定某个指针可以存储的所有地方,以及确定能否保证指针的生命周期只在当前进程或在其它线程中。

  • 什么时候进行逃逸分析?

    简单来说编译期进行逃逸分析是可以的,但是Java的分离编译和动态加载使得前期的静态编译的逃逸分析比较困难或收益较少,所以目前Java的逃逸分析只发在JIT的即时编译中,因为收集到足够的运行数据JVM可以更好的判断对象是否发生了逃逸。

  • 逃逸分析的目的是什么?当判断出对象不发生逃逸时,编译器可以使用逃逸分析的结果作一些代码优化。

    优化一:将堆分配转化为栈分配。
    解析:方法栈上的对象在方法执行完之后,栈桢弹出,对象就会自动回收。这样的话就不需要等内存满时再触发内存回收。这样的好处是程序内存回收效率高,并且GC频率也会减少,程序的性能就提高了。前提是要使指向该对象的指针永远不会逃逸。
    
    优化二:同步锁消除,即同步省略(锁消除)。
    解析:如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
    
    优化三:分离对象或标量替换。
    解析:这个简单来说就是把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有:
    		1. 减少内存使用,因为不用生成对象头。 
    		2. 程序内存回收效率高,并且GC频率也会减少,总的来说和上面优点的效果差不多。
    		3. 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
    
    备注:说明一下Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别。
    
  • 堆栈的区别:

    栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
    
    栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
    但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
    
    栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和引用对象。
    栈里存的却是堆的首地址名,就像引用变量。
    
    Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在栈中分配的内存只是一个指向这个堆对象的指针(引用变量)而已。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
    
    引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。
    
什么是方法内联?
  • 指JVM在运行时将调用次数达到一定阈值的方法调用替换为方法体本身,从而消除调用成本,并为接下来进一步的代码性能优化提供基础。

  • 方法内联是由JIT编译器在运行时完成的。既然涉及到编译,方法内联也是有一定的开销的,包括cpu时间和内存。

  • 内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。由于在编译时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销。以目标代码的增加为代价来换取时间的节省。

什么是CMS和G1?
  • CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现。

    步骤:
    1. 初始标记:独占CPU,仅标记GCroots能直接关联的对象。(先由根节点step一步,初始扩散)
    2. 并发标记:可以和用户线程并行执行,标记所有可达对象。(整体扩散)
    3. 重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正。(扩散期间的变化回补)
    4. 并发清理:可以和用户线程并行执行,清理垃圾。(清理)
    
    优点:
    1. 并发。
    2. 低停顿。
    
    缺点:
    1. 对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢。(侵用CPU)
    2. 无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾。
    3. CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了。(标记-清理 导致碎片 影响老年代无连续空间分配存储,单线程整理导致耗时停顿明显)
    
    CMS出现FullGC的原因:
    1. 年轻代晋升到老年带没有足够的连续空间,很有可能是内存碎片导致的。
    2. 在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC。
    
  • G1:是一款面向服务端应用的垃圾收集器。

    特点:
    1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
    2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
    3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
    4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    
    与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了。它们都是一部分Region(不需要连续)的集合。同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。
    
    如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
    1、初始标记(Initial Making)
    2、并发标记(Concurrent Marking)
    3、最终标记(Final Marking)
    4、筛选回收(Live Data Counting and Evacuation)
    
    看上去跟CMS收集器的运作过程有几分相似,事实的确也这样。
    1. 初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。
    2. 并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。
    3. 而最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。
    4. 最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。
    
什么是ASM(Assembly)?
  • JDK动态代理:简单易用,只能对接口进行代理。

  • CGLIB:借助ASM实现动态代理。

  • JIT:即时编译,直接缓存热点代码的机器码,防止字节码文件反复解析造成的性能浪费。

  • 如果把JIT比作是跳过字节码编译的小技巧,那么,ASM则是对字节码的全权掌控。

  • 通过ASM可以修改现有的class文件或动态生成class文件,是一种通用的Java字节码操作和分析框架。

    ASM是一个JAVA字节码分析、创建和修改的开源应用框架。它可以动态生成二进制格式的stub类或其他代理类,或者在类被JAVA虚拟机装入内存之前,动态修改类。
    
    源码分析:访问者模式
    

备注:

  1. 在 IDEA 里安装字节码的插件ASM Bytecode Outline,查看类文件右键选择 Show bytecode Outline 即可右侧工具栏查看生成的字节码。
  2. javac -g Test.java 编译为class文件, javap -verbose Test.class 命令查看class文件格式。
JVM是基于栈的指令集,与基于寄存器的指令集相比,有什么异同?
1. 基于栈的指令集主要的优点就是可移植,缺点是执行速度慢,相同操作指令数要多很多。

2. 寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。

3. 虽然栈架构指令集的代码非常紧凑,但是完成相同功能所需的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。
  • 基于栈式架构的特点:

    1. 设计和实现更简单,适用于资源受限的系统。
    2. 避开了寄存器的分配难题:使用零地址指令方式分配。
    3. 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
    4. 不需要硬件支持,可移植性更好,更好实现跨平台。
    
  • 基于寄存器架构的特点:

    1. 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
    2. 指令集架构则完全依赖硬件,可移植性差。
    3. 性能优秀和执行更高效。
    4. 花费更少的指令去完成一项操作
    5. 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。
    6. 栈式架构是8位的,而寄存器架构是16位的。所以栈式架构指令集更小,但是寄存器架构的指令更少。
    
    备注:零地址指令只有操作码,没有操作数。
    尽管基于寄存器架构的虚拟机所使用的零地址指令更加紧凑,但是完成一项操作的时候必然需要花费更多的入栈和出栈指令,这同时也意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于访问内存是执行速度的一个重要瓶颈,二地址指令或三地址指令虽然每条指令占的空间较多,但总体来说可以用更少的指令去完成一项操作,指令分派与内存读/写次数相对来说也都更少。
    
其他

JVM内存区域图解

JVM性能调优方法

JVM垃圾回收机制

性能优化具体案例

Java内存使用明细

  1. CMS Old Gen
  2. Par Eden Space(伊甸园区)8
  3. metaSpace
  4. Compressed Class Space
  5. Code Cache
  6. Par Survivor Space(幸存者区)1
性能优化相关工具
  • java dump工具,ZProfile
Reference
  • https://zhuanlan.zhihu.com/p/81941373(深入理解Java即时编译器(JIT)-上篇)
  • https://blog.csdn.net/Jbinbin/article/details/87783455(jvm的解释器和编译器是如何进行协作执行代码的)
  • https://zhuanlan.zhihu.com/p/36822336Java(一次编译到处运行跨平台的底层原理)
  • https://zhuanlan.zhihu.com/p/48285067(JDK、JRE和JVM的区别与相互之间的联系)
  • https://www.zhihu.com/question/366524107(java字节码是如何执行的?)
  • https://zhuanlan.zhihu.com/p/94498015?utm_source=wechat_timeline(史上最通俗易懂的ASM教程)
  • https://www.cnblogs.com/zt007/p/6377789.html(Java中ASM框架详解)
  • https://segmentfault.com/a/1190000040440196?utm_source=sf-similar-article(硬核万字长文,深入理解 Java字节码指令)
  • https://asm.ow2.io/asm4-guide.pdf(ASM 4.0 A Java bytecode engineering library)
  • https://www.bilibili.com/read/cv9803401/(Java ASM详解:ASM库使用)
  • https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html(基本功 | Java即时编译器原理解析及实践)
  • http://www.ruanyifeng.com/blog/2017/09/flame-graph.html(如何读懂火焰图?)
  • https://blog.csdn.net/hyman_c/article/details/103008165(JVM内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen))
  • https://xie.infoq.cn/article/a05e8c191dcf06ee6d4b67117(JVM 分析与调优技巧分析(原理篇))
  • https://blog.csdn.net/qq_22796957/article/details/108049133(cpu使用率低负载高,原因分析)
  • https://www.cnblogs.com/rainy0426/articles/12620127.html(如何正确理解 CPU 使用率和平均负载的关系?看完你就知道了)
  • https://blog.csdn.net/srs1995/article/details/109203174(公司线上虚拟机大量GC导致STW和CPU飙升–抽丝剥茧定位的过程)
  • https://blog.csdn.net/u013490280/article/details/108522427(HotSpot虚拟机的分层编译(Tiered Compilation))
  • https://www.cnblogs.com/rgever/p/9534857.html(CMS和G1的区别)
  • https://asm.ow2.io/asm4-guide.pdf(ASM 4.0 A Java bytecode engineering library)
  • https://arthas.aliyun.com/doc/profiler.html(Arthas)
  • https://blog.csdn.net/HappySundlut/article/details/116705829(JVM的架构模型&基于栈式的指令集架构与基于寄存器式的指令集架构的区别)
  • https://www.iteye.com/blog/rednaxelafx-492667(虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩)
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/311044.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号