zoukankan      html  css  js  c++  java
  • Java性能权威指南读书笔记--之一

    JIT(即时编译)

    解释型代码:程序可移植,相同的代码在任何有适当解释器的机器上,都能运行,但是速度慢。
    编译型代码:速度快,电视不同CPU平台的代码无法兼容。
    java则是使用java的编译器先将其编译为class文件,也就是字节码;然后将字节码交由jvm(java虚拟机)解释执行。由于这个编译是在程序执行时进行的,因此被称为“即使编译”。

    热点编译

    对于程序来说,通常只有一部分代码被经常执行,而应用的性能就取决于这些代码执行得有多快。这些关键代码段被称为应用的热点,代码执行得越多就被认为是越热。
    因此JVM执行代码时,并不会立即编译代码:

    1. 如果代码只执行一次,那编译完全就是浪费精力。对于只执行一次的代码,解释执行Java字节码比先编译然后执行的速度快。
    2. JVM执行特定方法或者循环的次数越多,它就会越了解这段代码。这使得JVM可以在编译代码时进行大量优化。

    分层编译

    Client编译器和server编译器主要的区别在于编译代码的时机不同。client编译器开启编译比server编译器要早。这意味着在代码执行的开始阶段,client编译器比server编译器要快,因为它的编译代码相比server编译器而言要多。
    分层编译是综合了client和server的优点。在开启分层编译(-XX:+TieredCompilation)后代码先由client编译器编译,随着代码变热,由server编译器重新编译。

    调优代码缓存

    JVM编译代码时,会在代码缓存中保留编译之后的汇编语言指令集。代码缓存的大小固定,所以一旦填满,JVM就不能编译更多代码了。
    也就是说,如果代码缓存过小,那么就会有一些热点代码被编译了,而其他没有,最终导致应用的大部分代码都是解释运行(非常慢)。这个问题在使用client编译器或进行分层编译时很常见。
    当代码缓存填满时,JVM通常会发出以下警告:

    Java HotSopt(TM) 64-Bit Server VM warning:CodeCache is full.Compiler has bean disabled.
    Java HotSopt(TM) 64-Bit Server VM warning:Try increasing the code cache size using -XX:ReservedCodeCacheSize=
    

    各平台代码缓存的默认大小:

    jvm jdk版本 大小
    32位client Java8 32MB
    32位client 分层编译,Java8 240MB
    64位client 分层编译,Java8 240MB
    32位client Java7 32MB
    32位server Java7 32MB
    64位server Java7 48MB
    64位server 分层编译,Java7 48MB

    如果代码缓存设为1GB,JVM就会保留1GB的本地内存空间。如果是32位JVM,那么进程占用的总内存不能超过4GB(包括Java堆、JVM自身所有代码占用空间、分配给应用的本地内存、代码缓存)。
    通过jconsole Memory(内存)面板的Memory Pool Code Cache图表,可以监控代码缓存。

    编译阈值

    一旦代码执行到一定次数,且达到了编译阈值,编译器就可以获得足够的信息编译代码了。
    编译是基于两种JVM计数器的:方法调用计数器和方法中的循环回边计数器。回边实际上可以看作是循环完成执行的次数。
    栈上替换:JVM可以在方法循环运行时进行编译,并在循环代码编译结束之后,JVM替换还在栈上的代码,循环的下一次迭代就会执行快的多的代码。
    标准编译由-XX:CompileThreshold=N标志触发。使用client编译器时,N的默认值是1500,使用server编译器时为10000。
    计数器会随着时间而减少,所以计数器只是方法或循环最新热度的度量。由此带来一个副作用是,执行不太频繁的代码可能永远不会编译。

    检测编译过程

    -XX:+PrintCompilation
    如果开启PrintCompilation,每次编译一个方法(或循环)时,JVM就会打印一行被编译的内容信息。
    绝大多数编译日志的行具有以下格式:
    timestamp compilation_id attributes (tiered_level) method_name size deopt
    timestamp表示编译完成的时间
    compilation_id内部的任务ID
    attributes是一组5个字符长的串,表示代码编译的状态。如果给定的编译被赋予了特定属性,就会打印下面列表中所显示的字符,否则该属性就打印一个空格。
    * % :编译为OSR
    * s :方法是同步的
    * !:方法有异常处理器
    * b :阻塞模式时发生的编译
    * n:为封装本地方法所发生的编译
    tiered_level 如果程序没有使用分成编译的方式运行则为空,否则为数字,表明所完成编译的级别
    method_name格式为:ClassName::method
    然后是编译后代码大小(单位是字节)
    最后,在某些情况下,编译日志的结尾会有一条信息,表明发生了某种逆优化,通常是“made not entrant”或”made zombie”

        135    1     n 0       java.lang.Thread::currentThread (native)   (static)
        136    2       3       java.util.Arrays::copyOf (19 bytes)
        136    7       3       sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
        137    8       2       java.lang.String::hashCode (55 bytes)
    

    使用jstat -compiler 进程ID 也可以看有多少方法被编译
    使用jstat -printcompilation 5003 1000 表示进程ID为5003的程序每1秒输出一次最近被编译的方法

    编译器线程

    当方法(或循环)适合编译时,就会进入到编译队列。队列则由一个或多个后台线程处理。编译队列是一种优先队列,即调用计数次数多的方法有更高的优先级。
    当开启分层编译时,JVM默认开启多个client和server线程。

    cpu数量 C1的线程数(client) C2的线程数(server)
    1 1 1
    2 1 1
    4 1 2
    8 1 2
    16 2 6
    32 3 7
    64 4 8
    128 4 10

    编译器的线程数可通过-XX:CICompilerCount=N标志来设置。对于分层编译来说,设置的值中三分之一将用来处理client编译器队列,其余的线程(至少一个)用来处理server编译器队列。
    使用分层编译时,线程数很容易超过系统限制,特别是有多个JVM同时运行的时候。在这种情况下,减少线程数有助于提高整体的吞吐量(尽管代价可能是热身期会持续得更长)。

    方法内联

    public class Point{
    	private int x,y;
    	public int getX(){ return x; }
    	public void setX(int i){ x = i;}
    }
    

    如果你写下面的代码

    Point p = getPoint();
    p.setX(p.getX()*2);
    

    编译后的代码本质上执行的是:

    Point p = getPoint();
    p.x = p.x *2;
    

    方法是否内联取决于它有多热以及它的大小。
    -XX:MaxInlineSize=N默认是35字节,即只有方法小于35字节时第一次调用方法时就会被内联。
    -XX:MaxFreqInlineSize=N默认是325字节,即只有当一个方法频繁被调用并且小于325字节时会被内联。

    逃逸分析

    -XX:+DoEscapeAnalysis默认为true。逃逸分析可以让JVM对一个对象根据代码来进行优化。

    1. 栈上分配
      我们都知道Java中的对象都是在堆上分配的,而垃圾回收机制会回收堆中不再使用的对象,但是筛选可回收对象,回收对象还有整理内存都需要消耗时间。如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
    2. 同步消除
      如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
    3. 标量替换
      Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。

    小结

    1. 不用担心小方法,特别是getter和setter,因为它们容易内联。
    2. 需要编译的代码在编译队列中,队列中代码越多,程序打到最佳性能的时间越久。
    3. 虽然代码缓存的大小可以调整,但它仍然是有限的资源
    4. 代码越简单,优化越多。
  • 相关阅读:
    使用 yo 命令行向导给 SAP UI5 应用添加一个新的视图
    SAP Fiori Elements 应用的 manifest.json 文件运行时如何被解析的
    SAP UI5 标准应用的多语言支持
    微软 Excel 365 里如何设置下拉菜单和自动高亮成指定颜色
    SAP Fiori Elements 应用里的 Title 显示的内容是从哪里来的
    本地开发好的 SAP Fiori Elements 应用,如何部署到 ABAP 服务器上?
    如何在 Cypress 测试代码中屏蔽(Suppress)来自应用代码报出的错误消息
    教你一招:让集群慢节点无处可藏
    应用架构步入“无服务器”时代 Serverless技术迎来新发展
    MySQL数据库事务隔离性的实现
  • 原文地址:https://www.cnblogs.com/luozhiyun/p/10597842.html
Copyright © 2011-2022 走看看