问题:把房间打扫一下
解法一(not good):用扫帚把地上的垃圾扫到门外面去
解法二(good):用扫帚把地上的垃圾扫干净,垃圾扔到楼下的垃圾桶中,并把地上的污渍拖干净
做正确的事;正确的事,做
问题:把房间打扫一下
解法一(not good):用扫帚把地上的垃圾扫到门外面去
解法二(good):用扫帚把地上的垃圾扫干净,垃圾扔到楼下的垃圾桶中,并把地上的污渍拖干净
spring配置
1 2 3 4 5 6 7 |
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager"> <ref bean="transactionManager" /> </property> <property name="propagationBehaviorName" value="PROPAGATION_REQUIRES_NEW"/> </bean> |
java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 |
transactionTemplate.execute(new TransactionCallback<Object>() { @Override public Exception doInTransaction(TransactionStatus status) { try { // do something return xxx; } catch (Exception e) { status.setRollbackOnly(); // do exception return yyy; } } }); |
一般情况我们使用默认的传播行为就行了,但是有些相对特殊的情况,需要使用其他的用法,举个例子:
某个也事务中执行一个业务逻辑,该业务逻辑中写数据一条数据A,并在事务结束前发出去一条消息给其他的应用,其他的应用受到消息会先来查询数据A,然后你很可能会发现其他应用来查询A时发现A并不存在。原因就是整个事务相关的还未commit,其他不在事务中的查询DB请求都读取不到未commit的数据(一般都是读已提交的事务隔离级别ISOLATION_READ_COMMITTED)
主要是这篇:https://access.redhat.com/documentation/en-US/JBoss_Enterprise_Web_Platform/5/html/Administration_And_Configuration_Guide/ch12s02.html
https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/6.4/html/administration_and_configuration_guide/sect-datasource_configuration#Datasource_Parameters1
https://docs.jboss.org/jbossas/docs/Server_Configuration_Guide/4/html/Connectors_on_JBoss-Configuring_JDBC_DataSources.html
当执行mvn install的时候,如果ctrl+C突然中断mvn install的执行,就可能碰到“Error installing artifact’s metadata: Error installing metadata: Error updating group repository metadata input contained no data”错误,导致mvn install失败。
搜集了半天才找到答案,这里记录一下,解决办法是:
在~/.m2/repository/目录下找到文件内容为空的maven-metadata-local.xml文件,并删除掉这个空文件,重新mvn install即可。或者直接用下面的命令也可以
1 |
sudo find ~/.m2/repository/ -name maven-metadata-local.xml -empty -print |
网站地址支持https可以增加安全性也可以增加seo排名,以前没有免费的证书服务,最近发现可在阿里云购买免费的Symantec品牌的单域名证书(每个子域需要单独购买免费的证书),购买地址:https://common-buy.aliyun.com/?commodityCode=cas#/buy
由于一次只能购买一年,所以一年后还需要再重新购买和安装。
购买后按照流程需要一段时间审批下来,审批下来后在SSL证书控制台查看并下载证书zip包,比如我的下来下来是:3021562_rongmayisheng.com_nginx.zip,解压后能看到两个文件:3021562_rongmayisheng.com.key、3021562_rongmayisheng.com.pem。
接下来是把这两个证书文件复制到对应ECS的nginx的conf/cert(我机器上具体目录是/usr/local/nginx/conf/cert)目录下,然后参考阿里云的官方文档来配置nginx.conf(我机器上具体目录是/usr/local/nginx/conf/nginx.conf):
期间我碰到两个问题,通过这两篇文章得到解决:
最后一步,需要在ECS上增加https 443端口的安全组入口流量,操作路径:进入ECS“实例列表”–>实例后面的“更多”–>“网络和安全组”–>“安全组设置”–>“安全组列表”tab–>找到之前配置了http 80端口的安全组并点击后面的“配置规则”–>“添加安全组规则”按钮。配置如下:
至此访问自己的域名应该能用https://开头了
这里附上我的完整配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
worker_processes 1; error_log logs/error.log; error_log logs/error.log notice; error_log logs/error.log info; pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; client_max_body_size 2m; access_log logs/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; gzip on; server { listen 80; server_name localhost; rewrite ^(.*)$ https://$host$1 permanent; access_log logs/host.access.log main; location / { root html; index index.php index.html index.htm; } error_page 404 /404.html; error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } location ~* \.php$ { fastcgi_index index.php; fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; } } server { listen 443 ssl; server_name rongmayisheng.com; ssl_certificate cert/3021562_rongmayisheng.com.pem; ssl_certificate_key cert/3021562_rongmayisheng.com.key; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { root html; index index.php index.html index.htm; } location ~* \.php$ { fastcgi_index index.php; fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; } } } |
mark下。
源码安装太麻烦,依赖包,依赖库等原因很容易出问题。用rpm就简单很多。
如下几个命令搞定:
1 2 3 4 5 6 7 8 |
# 下载memcahed和依赖库libevent wget http://mirror.centos.org/centos/6/os/x86_64/Packages/libevent-1.4.13-4.el6.x86_64.rpm wget http://mirror.centos.org/centos/6/os/x86_64/Packages/memcached-1.4.4-5.el6.x86_64.rpm # 安装 sudo rpm -ivh libevent-1.4.13-4.el6.x86_64.rpm sudo rpm -ivh memcached-1.4.4-5.el6.x86_64.rpm # 启动 memcached -d -m 50 -p 11211 -u root |
通常的NullPointerException异常其实是很好判断,一定是有某个对象是null导致的。但是java中数字都有自动装箱和拆箱功能,它简化了我们的代码,隐藏了其背后的执行步骤,导致程序抛出异常时我们会有意识的忽略了数字类型也可能会抛NullPointerException。 其实自动装箱和拆箱在字面上很容易使人对其理解的不准确或者不完整,大家都知道装箱就是从基础类型自动转换成对应的对象类型,拆箱就是从对象类型自动转换成基础类型;字面上很好理解,但是背后到底是如何实现的,其实有一种道不清说不明的感觉。自动装箱是不会抛异常的,但是自动拆箱则有可能会,所以本文从三种代码场景,以字节码的角度,还原自动拆箱背后的逻辑。
1 2 3 4 5 6 |
public class Test { public static void main(String[] args){ Long a = null; long b = a; } } |
上面的代码在把变量a赋值给b时就会抛出NullPointerException,使用javap Test.class命令查看其对应的字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: invokevirtual #2 // Method java/lang/Long.longValue:()J 6: lstore_2 7: return } |
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是把变量a赋值给b,显然由于a是null,所以调用longValue()方法自然会抛空指针了。
1 2 3 4 5 6 7 8 9 10 |
public class Test { public static void main(String[] args){ Long a = null; increment(a); } public static long increment(long n){ return n++; } } |
上面把变量a传参给方法increment时就会抛NullPointerException,还是用javap Test.class查看其字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: invokevirtual #2 // Method java/lang/Long.longValue:()J 6: invokestatic #3 // Method increment:(J)J 9: pop2 10: return public static long increment(long); Code: 0: lload_0 1: dup2 2: lconst_1 3: ladd 4: lstore_0 5: lreturn } |
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是真正的执行increment,显然由于a是null,所以调用longValue()方法也会抛空指针了。
1 2 3 4 5 6 7 8 |
public class Test { public static void main(String[] args){ Long n1 = null; long n2 = 1L; if(n1 == n2){ } } } |
同样用javap Test.class查看字节码内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: lconst_1 3: lstore_2 4: aload_1 5: invokevirtual #2 // Method java/lang/Long.longValue:()J 8: lload_2 9: lcmp 10: ifne 13 13: return } |
同样的在行号8的字节码通过Long.longValue()方法把变量n1从Long转换成了long,然后行号8为加载变量n2,行号9才是比较两个long(注意不是比较Long)。
上面提到的字节码中的行号其实是字节码指令占用空间的偏移量,为了便于理解,暂以行号来理解
拆箱的过程其实是调用了对应的XXX.xxxValue()方法,比如:Integer.intValue(), Long.longValue(), Short.shortValue(), Byte.byteValue(), Float.floatValue(), Double.doubleValue(),如果这个对象是null,那么就会抛出空指针异常。
JVM JIT编译器优化技术有近100中,其中最最重要的方式就是内联(inlining)。方法内联可以省掉方法栈帧的创建,同时增加了CPU指令cache的命中率,方法内联还使让JIT编译器更多更深入的优化变成可能。本人在fastxml(速度比XPP3(基于xmlpull)还快的xml解析器)开源项目中针对方法内联进行了很多学习和实践,这里总结一下,介绍一下怎么让你的代码更好的被JVM JIT Inlining。
上一篇博客《Java JIT性能调优》中介绍了inlining相关的几个参数,这里copy下: jvm可以通过两个启动参数来控制字节码大小为多少的方法可以被内联:
可见,要想inlining就要让你的方法的字节码变得尽可能的小,默认情况下,你的方法要么小于35B(针对普通方法),要么小于325B(针对调用频率很高的方法)。
同样的上一篇博客《Java JIT性能调优》中也介绍了非常牛x的JIT优化工具JITWatch,该工具的详细文档请看这里:https://github.com/AdoptOpenJDK/jitwatch
通过JITWatch中提示或者在启动命令中添加-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining来输出提示信息,我们可以很快定位哪些方法需要优化,从而针对性的调优那些执行频率很高但没有inlining的方法,或者那些很小但不足以小到35Byte的方法。减少方法字节码的方法有很多,下面介绍我在fastxml项目中实践过的9种方法。
我们的代码里面常常会有分支语句,比如:if-else、switch。但是我仔细想想,是不是所有的分支都有均等的执行机会,是不是很多时候,某些分支能进入的概率很小或者几乎没有。如果某个分支执行的概率很小,而且这个分支中的代码量不算小,那把这个分支中的代码抽取成一个方法就可以大大缩减当前这个方法的字节码大小。下面举一个著名NIO框架Netty中的一个例子,看优化前的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
private final class NioMessageUnsafe extends AbstractNioUnsafe { ... @Override public void read() { assert eventLoop().inEventLoop(); final SelectionKey key = selectionKey(); if (!config().isAutoRead()) { int interestOps = key.interestOps(); if ((interestOps & readInterestOp) != 0) { // only remove readInterestOp if needed key.interestOps(interestOps & ~readInterestOp); } } final ChannelConfig config = config(); final int maxMessagesPerRead = config.getMaxMessagesPerRead(); final boolean autoRead = config.isAutoRead(); final ChannelPipeline pipeline = pipeline(); boolean closed = false; Throwable exception = null; try { for (;;) { int localRead = doReadMessages(readBuf); if (localRead == 0) { break; } if (localRead < 0) { closed = true; break; } if (readBuf.size() >= maxMessagesPerRead | !autoRead) { break; } } } catch (Throwable t) { exception = t; } int size = readBuf.size(); for (int i = 0; i < size; i ++) { pipeline.fireChannelRead(readBuf.get(i)); } readBuf.clear(); pipeline.fireChannelReadComplete(); if (exception != null) { if (exception instanceof IOException) { // ServerChannel should not be closed even on IOException because it can often continue // accepting incoming connections. (e.g. too many open files) closed = !(AbstractNioMessageChannel.this instanceof ServerChannel); } pipeline.fireExceptionCaught(exception); } if (closed) { if (isOpen()) { close(voidPromise()); } } } ... } |
代码中的config().isAutoRead()默认情况是false,也就是说,在没有专门配置的情况下,这个分支不会进去,而且一般人不会把这个配置为true,所以可以把这个if分支中的代码块抽取为方法,这样方法就变小了一些,而代码的功能没有减少。修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private final class NioMessageUnsafe extends AbstractNioUnsafe { ... private void removeReadOp() { SelectionKey key = selectionKey(); int interestOps = key.interestOps(); if ((interestOps & readInterestOp) != 0) { // only remove readInterestOp if needed key.interestOps(interestOps & ~readInterestOp); } } @Override public void read() { assert eventLoop().inEventLoop(); if (!config().isAutoRead()) { removeReadOp(); } final ChannelConfig config = config(); ... } ... } |
这样就很容易的把方法的字节码大小减少了一些,这样read()方法就可以inlining了。详见这篇文章:http://normanmaurer.me/blog_in_progress/2013/11/07/Inline-all-the-Things/
现在IDE(idea、eclipse等)功能很强大,往往能在开发阶段就能提示我们哪些代码是冗余无用的代码,请及时的删除那些无用代码,因为它们会占用方法的字节大小。此处我要说的不是这种IDE能提示我们的场景,而是隐含在我们编写的代码中的无用代码,比如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public static String parseTrimedString(byte[] bytes, int begin, int length, Charset charset, boolean decodeEntityReference) throws ParseException { if (length == 0) { return null; } int last = begin + length; for (; begin < last; begin++) { // forward to find a valid char if (!ByteUtils.isWhiteSpaceOrNewLine(bytes[begin])) { break; } } for (last = last - 1; last > begin; last--) { // backward to find a valid char if (!ByteUtils.isWhiteSpaceOrNewLine(bytes[last])) { break; } } return parseString(bytes, begin, last - begin + 1, charset, decodeEntityReference); } public static String parseString(byte[] bytes, int begin, int length, Charset charset, boolean decodeEntityReference) throws ParseException { if (length == 0) { return null; } // ..... } |
在方法parseTrimedString的开头处对length进行判断,乍一看,似乎没有什么不合理的,如果要处理的数组长度为0就直接返回null。逻辑上没有问题,IDE也不会提示这一段代码是无用的,但是仔细想想,要是length==0时,要是没有这个判断会怎么样呢? 第一个for循环中有begin<last的判断,避免了循环体中的bytes[begin]数组越界;同样的,第二个for循环里数组下标也被last>begin保护起来了,防止越界;再看看parseString()方法中正好有对length的判断,也就是说方法parseTrimString内部不再需要对length等于0的验证了。这一段代码可以移除。 类似这种非0、非null的判断在我们日常的代码中也是很常见的,其实可以通过一定的约定来减少这种判断,比如:按照约定保证每次方法的返回都不为null,若返回类型为String数组,则返回new String[0];若返回类型为Map<String, String>,则返回new HashMap<String, String>()。
很多时候,程序员因为懒惰不愿修改别人的代码,怕承担风险,而是有时候偏向于复制代码,这可能会导致一个方法体内部出现重复的表达式或者代码段。这时候应该把相同的幂等的表达式抽取为临时变量,把相同的代码段抽取为成员方法(不仅可以减少当前方法的字节码,还可能增加方法的复用率,为JIT优化提供了更多可能)。 通过抽取重复的代码来减少字节码,这种方式的效果是显而易见的。这里重点讲解一下,相同的幂等的表达式抽取为临时变量前后的字节码大小差异,看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 |
if (ByteUtils.isValidTokenChar(docBytes[cursor])) {// next attributeName return ATTRIBUTE_NAME; } else if (docBytes[cursor] == '>') { // the start tag moveCursor(1); return processAfterStartTag(); } else if (docBytes[cursor] == '/') {// found end tag moveCursor(1); return END_TAG_WITHOUT_TEXT; } else { throw ParseException.formatError("should be space or '>' or '/>' or another attribute here", this); } |
这段代码中重复出现了docBytes[cursor],通常大家可能会认为这种数组取下标的操作没有几行字节码,应该不耗时,只有这种objA.getField1().getSubField2().getSubSubField3()的字节码多。其实不然,先看看仅docBytes[cursor]一句话的字节码有多少:
1 2 3 4 5 |
aload_0 //取this getfield #3 //取cursor aload_0 //取this getfield #4 //取docBytes属性 bload // 取数组下标对应的元素值 |
这里看出docBytes[cursor]对应9个字节的5行字节码,而这个表达式在上面的语句块中出现了3次,所以一共占用了9*3=27个字节。如果抽取成临时变量那么第二次和第三次使用临时变量即可,而使用临时变量仅占用1个字节,这样又可以减少近18字节。当一个方法里面重复出现的变量越多,优化效果就越明显。
先看代码:
1 |
i=i+2; |
上面的代码很简单,其中i=i+2这种写法也很常见,那么看看这行代码对应的字节码是什么样的:
1 2 3 4 |
iload_2 // 加载栈帧地址为2的变量的值,此处为变量i iconst_2 // 加载数值2 iadd // 相加 istore_2 // 把相加的结果赋值给i |
这行代码已经很简单了,只占用4个字节,还有优化空间吗?当然有,我们还有一种常见的写法:
1 |
i+=2; |
再看看这种写法的字节码:
1 |
iinc 2,2 // 第一个参数为变量在栈帧的位置,第二个参数为数值2</span> |
这一行字节码占用3个字节的空间,而且只有一行字节码。这样一个微小的改动就可以节省1个字节,缩减3条字节码的指令条数。
现在一些高级IDE也支持无用初始化的提示,比如:idea(eclipse还不支持这个功能)。如果IDE提示,请尽量移除这些无用的初始化,比如下面的例子:
1 2 3 4 5 6 7 |
int x = 1; int sum = 0; if(x > 0){ sum = 1; }else{ sum = 0; } |
显然对临时变量sum的初始化是没有作用的,上面的java代码产生的字节码有这些:
1 2 3 4 5 6 7 8 9 10 11 |
iconst_1 // 数值1 istore_1 // 把1存到偏移量为1的临时变量中,即变量x中 iconst_0 // 数值0 istore_2 // 把0存到偏移量为2的临时变量中,即变量sum中 iload_1 // 取x的值 ifle 15 // 把x与0比较,如果成功跳转到15行字节码 iconst_1 // 数值1 istore_2 // 把数值1存到偏移量为2的临时变量中,即变量sum中 goto 17 // 跳转到17行字节码 iconst_0 // 数值0 istore_2 // 把数值0存到变量sum |
如果把对sum的初始化移除,java代码如下:
1 2 3 4 5 6 7 |
int x = 1; int sum; if(x > 0){ sum = 1; }else{ sum = 0; } |
代码变化很小,改后的字节码如下:
1 2 3 4 5 6 7 8 |
iconst_1 // 数值1 istore_1 // 把1存到偏移量为1的临时变量中,即变量x中 iload_1 // 取x的值 ifle 15 // 把x与0比较,如果成功跳转到15行字节码 iconst_1 // 数值1 istore_2 // 把数值1存到偏移量为2的临时变量中,即变量sum中 goto 17 // 跳转到17行字节码 iconst_0 // 数值0 |
通过对比可以看出移除对sum的初始化后, 字节码少了两行(2个字节)。有时候为了减少字节码就是需要一个字节一个字节的扣,没有办法。
临时变量的初始化是会占用字节码的,减少不必要的临时变量无形之中也减少了临时变量的初始化。看下面的例子:
1 2 3 4 5 6 7 8 |
public final static String toString(final byte[] bytes, int begin, int length) { StringBuilder sb = new StringBuilder(length); int last = begin + length; for(int i = begin; i < last; i++){ sb.append((char)bytes[i]); } return sb.toString(); } |
这个例子中,习惯性的使用临时变量i来作为for循环数组的下标,但是这个临时变量i真的有必要吗? 变量begin虽然是方法的参数,但是它也是这个方法的临时变量,而且它是java中的原始类型,改变begin的值不回对调用toString方法的调用方有任何影响,而且变量begin在方法中没有其他作用,很自然可以使用begin来代替i的下标作用。修改后的代码如下:
1 2 3 4 5 6 7 8 |
public final static String toString(final byte[] bytes, int begin, int length) { int last = begin + length; StringBuilder sb = new StringBuilder(length); for (; begin < last; begin++) { sb.append(bytes[begin]); } return sb.toString(); } |
可以看到代码中少了一句:int i=begin; 这将减少2字节的2行字节码。 当你很需要这个方法被inlining时,每个字节的减少都来之不易。
有时我们写代码时,为了把方法的输入表达得更准确,会给出一个精确的参数列表,其中可能有多个参数来自相同的对象。比如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private int processAttributeName() throws ParseException { moveCursor(1); // the first char has been checked in previous event, so here just skip it for (; cursor < docBytesLength; moveCursor(1)) {// read tag bytes if (!ByteUtils.isValidTokenChar(docBytes[cursor])) {// this attribute name end currentBytesLength = cursor - currentIndex; skipUselessChar(); // skip ' ' and '\t' between attribute name and '=' // read "=\"", '\'' should be ok if (docBytes[cursor] == '=') { moveCursor(1); skipUselessChar(); // skip ' ' and '\t' between '=' and attribute value if (docBytes[cursor] == '\"' || docBytes[cursor] == '\'') { // found the quotation at the beginning of attribute value moveCursor(1); // move to the first byte in quotes return ATTRIBUTE_VALUE; // found attribute value } else { throw ParseException.formatError("need '\"' or '\'' here", this.getRow(), this.getColumn()); } } else { throw ParseException.formatError("need '=' here", this.getRow(), this.getColumn()); } } } throw ParseException.documentEndUnexpected(this.getRow(), this.getColumn()); } |
这代代码中,可以看到有两处抛出异常,每次创建异常都需要传三个参数:message、row、column。而上面的这个例子,很显然row和column都来自当前对象this,我在优化fastxml时,发现这个地方也可以优化一下,毕竟this.getRow()和this.getColumn()这两个表达式分别占用了4个字节,如果把formatError方法的参数减少为message、parser(即this),就可以减少7个字节的字节码,因为传this仅占用1个字节。
举个实际的案例,fastxml中为了解析xml中的标签名中的字符是否符合xml规范,我需要做如下的判断:
1 2 3 4 |
public static boolean isValidTokenChar(byte b) { return b > 0 && ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == ':' || b == '-' || b == '_' || b == '.'); } |
这一长串的条件判断其对应的字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
0: iload_0 1: ifle 68 4: iload_0 5: bipush 97 7: if_icmplt 16 10: iload_0 11: bipush 122 13: if_icmple 64 16: iload_0 17: bipush 65 19: if_icmplt 28 22: iload_0 23: bipush 90 25: if_icmple 64 28: iload_0 29: bipush 48 31: if_icmplt 40 34: iload_0 35: bipush 57 37: if_icmple 64 40: iload_0 41: bipush 58 43: if_icmpeq 64 46: iload_0 47: bipush 45 49: if_icmpeq 64 52: iload_0 53: bipush 95 55: if_icmpeq 64 58: iload_0 59: bipush 46 61: if_icmpne 68 64: iconst_1 65: goto 69 68: iconst_0 69: ireturn |
上面的代码中冒号前的数组为当前行字节码的第一个字节为整个方法体字节码中的位置。可以看到这里面的有11个的if判断,而每次判断都需要加载if判断的两个操作数,第一个操作数为变量b,第二个操作数为常量(比如:“:”),由于代码太长,此处以b == ‘.’为例,其字节码对应58~61处,共7个字节,三行字节码。 由于这个方法相比较的字符串都在ASCII码0~128范围内,所以我这里可以把比较转换为数组下标的方式,数组下标为byte的数值,数组的元素值为当前下标是否是符合xml规范的字符,修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public final static byte[] byteType = { 0, // 0 0, 0, 0, 0, 0, 0, 0, 0, // 1~8 2, // 9: '\t' 2, // 10: '\n' 0, 0, // 11~12 2, // 13: '\r' 0, 0, 0, 0, 0, 0, 0, // 14~ 20 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21~30 0, // 31 2, // 32: ' ' 0, 0, 0, 0, 0, 0, 0, 0, // 33~40 0, 0, 0, 0, // 41~44 1, // 45: '-' 1, // 46: '.' 0, // 47 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48~57: '0'~'9' 1, // 58: ':' 0, 0, 0, 0, 0, 0, // 59~64 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 65~90: 'A'~'Z' 0, 0, 0, 0, // 91~94 1, // 95: '_' 0, // 96 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 97~122: 'a'~'z' }; public static boolean isValidTokenChar(byte b) { // to check validChars return b >= 0 && b <= 122 && (byteType[b] & 1) > 0; } |
可以看到方法变短来很多,字节码也减少了很多,执行速度也快很多。当然Java中原始类型比较适合用数组的方式解决这种众多if判断的问题,如果是对象的话,也可以用Map的方式。
一般而言JDK中的API都是久经考验的、非常完备的、通用的、众多高手智慧的结晶,但是也正是因为它的完备性和通用性,就导致其必然的损失了针对性。当优化到了一定程度后,就需要针对数据场景或者业务场景来进行针对性的优化来,举个例子:给一个基本排好序的数组进行排序时,冒泡比快速排序要快得多,因为这种数组最适合冒泡排序(O(n)时间复杂度),而快速排序时是O(nlog(n))。 看一个实际案例,下面的代码在fastxml的性能测试输出的编译和内联的log中看到StringBuilder的append方法太大了,导致无法内联到下面的方法中。
1 2 3 4 5 6 7 8 |
public final static String toString(final byte[] bytes, int begin, int length) { int last = begin + length; StringBuilder sb = new StringBuilder(length); for (; begin < last; begin++) { sb.append(bytes[begin]); } return sb.toString(); } |
看看StringBuilder的append方法内容是什么样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public StringBuilder append(char c) { super.append(c); return this; } public AbstractStringBuilder append(char c) { ensureCapacityInternal(count + 1); value[count++] = c; return this; } private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); } void expandCapacity(int minimumCapacity) { int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); } |
可以看到append方法一层层调用了几层方法,每个方法不算太大,用JITWatch可以看到优化建议:
The call at bytecode 2 to
Class: java.lang.AbstractStringBuilder
Member: public AbstractStringBuilder append(String)
was not inlined for reason: ‘callee is too large’
The callee method is greater than the max inlining size at the C1 compiler level.
Invocations: 1031
Size of callee bytecode: 50
可以看到public AbstractStringBuilder append(String)这个方法稍大了一点,导致没有内联。这会导致多出一次方法栈,而这个方法可能在程序运行中调用频率很高,如果这个方法不能inlining,势必降低了性能。jdk的源码我们改不了,那我们可以选择不使用jdk的类,而是根据自己的实际场景写一个更简单的类,比如下面这个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public final class FastStringBuilder { private char[] chars; // char array holder private int last = 0; // last index to append a byte or a char public FastStringBuilder(int length) { this.chars = new char[length]; } public void append(byte b) { chars[last] = (char) b; last++; } public void append(char c) { chars[last] = c; last++; } public int length() { return last; } public String toString() { return new String(chars, 0, last); } } |
这个代码把安全检查都移除了,也把冗长繁杂的校验和数组扩容都直接移除了,代码变得简洁高效。这么做不会出错么?当然不会,因为在fastxml中,调用方已经保证了其不会出现数组越界,而且保证了初始长度length就是最大长度,所以多一层的安全检查是没有必要的,也没有必要考虑去扩容和复制数组了。在fastxml中,使用这个简化后的代码,使性能提升了10%。
上面这9种方法都是在使用了JITWatch后,根据其提示绞尽脑汁想到的一些办法,通过这些优化,fastxml的性能提升了近1倍,现在fastxml的性能已经是XPP3的1倍左右。优化方法还有很多,针对不同的场景,会有很多不可思议的优化方法,这需要不断的挖掘。欢迎指正。 最后引用大牛Donald Knuth的一句话:
过早的优化是万恶之本
JVM自动监控这所有方法的执行,如果某个方法是热点方法,JVM就计划把该方法的字节码代码编译成本地机器代码,同时还会在后续的执行过程中进行可能的更深层次的优化,编译成机器代码的过程是在独立线程中执行的,不会影响程序的执行;除次以外,JVM还对热点方法和很小的方法内联到调用方的方法中,减少方法栈的创建。这些就是JIT(just in time)。
其中以下三种方式效果非常明显:
这些优化方法通常是层层依赖的,所以当JIT优化后的代码被JVM应用,就会开始尝试进行更上一层次的优化。因此我们写代码的时候,应该尽量往这些优化方式上面靠。
在JVM启动参数中添加三个启动参数,比如下面的命令,把编译信息输出到inline.log文件中,便于后续使用grep命令分析:
1 |
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining SimpleInliningTest > inline.log |
inline.log中内容类似这样:
1 |
31 23 s! sun.misc.URLClassPath::getLoader (136 bytes) inline (hot) |
inline.log文件内容中的方法还以tab缩进的方式来体现方法调用链的层次结构,非常易懂。
通过添加参数-XX:+PrintCompilation,可以看到的信息其实并不具体,比如:那些方法进行了内联,内联后的二进制代码是怎么样的都没有。而要输出JIT编译的细节信息,就需要在JVM启动参数中添加这个参数:
1 2 3 4 |
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+TraceClassLoading -XX:+PrintAssembly |
输出的编译信息,默认情况是在启动JVM的目录下一个名为:hotspot_pid<PID>.log的文件
如果想指定文件路径和文件名的话,可以再添加一个启动参数:
1 |
-XX:LogFile=<path to file> |
输出的是一个很大的xml文件,可能有几十上百兆,下面摘出部分内容如下(文件中的汇编代码太长,就不贴了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<nmethod compile_id='78' compiler='C2' level='4' entry='0x00000001052bc060' size='856' address='0x00000001052bbf10' relocation_offset='296' insts_offset='336' stub_offset='496' scopes_data_offset='544' scopes_pcs_offset='624' dependencies_offset='848' oops_offset='520' method='com/github/fastxml/util/ByteUtils isValidTokenChar (B)Z' bytes='26' count='7722' iicount='7722' stamp='0.344'/> |
这些内容很难读懂,建议使用JITWatch(https://github.com/AdoptOpenJDK/jitwatch/)的可视化界面来查看JIT编译的细节信息。同时JITWatch还可以给出很多优化建议,给我们有效的优化代码提供参考,详见下文。
上面的输出的细节编译信息inline.log文件中,有个字段上“compiler=C2”,这里的C2就是JIT的编译模式,C2表示这个方法进行了深度优化。下面介绍下JIT的编译模式
C1: 通常用于那种快速启动的GUI应用,对应启动参数:-client
C2: 通常用于长时间允许的服务端应用,对应启动参数:-server
分层编译模式(tiered compilation):这是自从Java SE 7以后的新特性,可通过添加启动参数来开启:
1 |
-XX:+TieredCompilation |
这个特性在应用启动阶段使用C1模式以达到快速启动的效果,一旦应用程序运行起来以后,C2模式将取代C1模式,以进行更深度的优化。在Java SE 8中,这个特性是默认的。
前面也提到了,JITWatch可以通过可视化界面来帮助我们分析JVM输出的JIT编译输出日志,还可以帮助我们静态分析jar中的代码是否符合JIT编译优化的条件,还可以以曲线图形的方式展示JIT编译的整个过程中的一些指标,还给我们的代码提意见和建议,非常好用的工具。
JITWatch需要在github上把代码clone下来,然后用maven来运行,地址为:https://github.com/AdoptOpenJDK/jitwatch/
如果在jvm的启动参数中添加了下面的启动参数:
1 2 3 4 |
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+TraceClassLoading -XX:+PrintAssembly |
但是你发现启动你的java程序后,有如下的报错信息:
1 |
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output |
或者启动啦JITWATCH后,打开了某个编译信息log文件,但是看不到每个方法编译后的汇编信息,且那么你就需要安装hsdis。hsdis可以帮助我们查看编译后的本地代码,具体可以参考JITWatch提供的文档,根据自己的系统类型来选择安装:https://github.com/AdoptOpenJDK/jitwatch/wiki/Building-hsdis,如果你是mac,可以参考这篇文章:http://nitschinger.at/Printing-JVM-generated-Assembler-on-Mac-OS-X/。
如果安装了hsdis库后,仍然在JITWatch中看不到汇编信息,那你检查下环境变量配置是否正确,实在不行可以尝试下重启电脑。
在代码根目录下执行launchUI.sh(
Linux/Mac)或则launchUI.bat(windows)
如果你使用maven,也可以在代码根目录下这样运行(其他运行方式,请参考JITWatch的github首页)
1 |
mvn clean compile exec:java |
如果你使用的是mac,而且idk版本是jdk7,且运行mvn clean compile exec:java时出现下面的错误和异常时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Caused by: java.lang.NullPointerException at com.sun.t2k.MacFontFinder.initPSFontNameToPathMap(MacFontFinder.java:339) at com.sun.t2k.MacFontFinder.getFontNamesOfFontFamily(MacFontFinder.java:390) at com.sun.t2k.T2KFontFactory.getFontResource(T2KFontFactory.java:233) at com.sun.t2k.LogicalFont.getSlot0Resource(LogicalFont.java:184) at com.sun.t2k.LogicalFont.getSlotResource(LogicalFont.java:228) at com.sun.t2k.CompositeStrike.getStrikeSlot(CompositeStrike.java:86) at com.sun.t2k.CompositeStrike.getMetrics(CompositeStrike.java:132) at com.sun.javafx.font.PrismFontUtils.getFontMetrics(PrismFontUtils.java:31) at com.sun.javafx.font.PrismFontLoader.getFontMetrics(PrismFontLoader.java:466) at javafx.scene.text.Text.<init>(Text.java:153) at com.sun.javafx.scene.control.skin.Utils.<clinit>(Utils.java:52) ... 13 more [ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.5.0:java (default-cli) on project jitwatch-ui: An exception occured while executing the Java class. null: InvocationTargetException: Exception in Application start method: ExceptionInInitializerError: NullPointerException -> [Help 1] |
请在org.adoptopenjdk.jitwatch.launch.LaunchUI类的main函数开头处添加下面的代码(或者直接使用我fork修改好的JITWatch):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
final Class<?> macFontFinderClass = Class.forName("com.sun.t2k.MacFontFinder"); final java.lang.reflect.Field psNameToPathMap = macFontFinderClass.getDeclaredField("psNameToPathMap"); psNameToPathMap.setAccessible(true); if (psNameToPathMap.get(null) == null) { psNameToPathMap.set( null, new java.util.HashMap<String, String>()); } final java.lang.reflect.Field allAvailableFontFamilies = macFontFinderClass.getDeclaredField("allAvailableFontFamilies"); allAvailableFontFamilies.setAccessible(true); if (allAvailableFontFamilies.get(null) == null) { allAvailableFontFamilies.set( null, new String[] {}); } |
然后重新运行即可看到JITWatch的界面。
首先点击“Open Log”按钮,选择前面提到过的hotspot_pid<PID>.log文件,然后点击“Start”分析该文件。随后就会在左边生成程序运行过程中加载的类及其目录结构。选择某个类后,右侧会展示该类对应的方法。这些方法中可能部分方法前面有个绿颜色的勾,这说明这个方法被编译成本地代码,选中这个方法后,可以在下方看到该方法具体信息,比如方法调用次数,方法大小等。如下图所示:
这个界面中,顶部的工具栏都可以自己尝试一下,个人觉得“TopList”和“Suggest”比较直接,我们根据这两个就可以快速的定位需有优化哪些代码了,大体是什么原因导致未编译或者未内联。
选中方法后,点击“TriView”即可查看该方法和字节码和编译后的汇编代码,如下图:
如果你左边的java代码看不到,那你就需要在上一个界面中点击“Config”来添加源码路径或者源码文件以告诉JITWatch从哪里找源码;如果你右边的汇编代码看不到,说明你上面的hsdis未安装好,请重新安装。
此时,点击上面的“Chain”按钮,即可看到该方法调用了哪些方法,以及这些方法是否被编译了,是否被内联了。如下图所示:
JIT的功能能显著提升java程序的性能,尤其是编译为本地代码和内联功能。内联需要方法比较小,也就是说写代码时就尽量把方法写得更小,让方法的复用度更高,复用的越多,就越可能被编译为本地代码。高性能的框架和类库针对JVM的JIT功能进行优化是非常有必要的,JVM提供的调试输出参数和JITWatch这样友好的工具能大大帮助我们快速的发现和定位需要优化的代码,大大提升了效率。
尽管我们可以手动调整JIT相关的一些参数,来让我们的更多的方法被编译和被内联,但一般不建议这么做(大牛都这么说)。
JIT编译成本地代码的过程也是需要消耗时间的,而且编译后本地代码不一定会使用(made not entrant,如果JVM根据一段时间的执行后进行了某项优化,但是在后来的某次执行时验证之前的优化是不完备的,那么JVM会取消这个优化,继续用解释执行的方式来执行字节码),所以并不是把所有或者大部分代码都编译一定会性能最优,那有可能也是灾难。
我所了解的JVM JIT性能调优的大致原理和方法就是这些,如有错误请指出。
性能优化永远是最后一步,不要提前过早开始性能优化。
如果你有耐心,就看看下面的文章吧,因为它们比我写的更详细
http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html
https://www.chrisnewland.com/images/jitwatch/HotSpot_Profiling_Using_JITWatch.pdf
http://www.docklandsljc.uk/presentations/2015/ChrisNewland-JITWatch.pdf
http://blog.csdn.net/hengyunabc/article/details/26898657
https://advancedweb.hu/2016/05/27/jvm_jit_optimization_techniques/
JVM启动参数:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
JITWatch使用文档:https://github.com/AdoptOpenJDK/jitwatch/wiki (右侧的页面目录分类很清晰)
https://advancedweb.hu/2016/05/27/jvm_jit_optimization_techniques/
https://advancedweb.hu/2016/06/28/jvm_jit_optimization_techniques_part_2/
新买的mac各种不好用不会用,拼音输入法居然不能和英文通过快捷键自由切换。网上各种搜也没有找到我想要的问题。因为大家都说按command+space就可以切换,但是我的却不行。
后来在System Preference > Keyboard中发现,拼音输入法的切换按钮上ctrl+space,但是实际上ctrl+space是打开的spotlight,mac系统初始化做得真够烂的,应该是快捷键冲突了。
于是我重置了快捷键,发现spotlight变成command+space了,输入法变成了ctrl+space,试了试,spotlight可以用,但是输入法不能切换。晕,mac默认的快捷键都不能正常工作。
于是我尝试把spotlight和输入法的快捷键换一下,勾选了输入法切换快捷键,修改快捷键为command+space,然后勾选spotlight快捷键修改成ctrl+space,经测试,都ok了。
总结下:
spotlight快捷键这样设置:ctrl+space
输入法快捷键这样设置:command+space
我的系统版本:10.11.5 (15F34)