java程序员,不懂JIT,你写的代码慢死了

努力做程序员中的标题党,这个文章的副标题

面向JIT编程-方法内联

JIT的全称是Just in time compilation,中文称之为即时编译。JIT是JVM最强大的武器之一,在运行时可以让java从屌丝到高富帅的飞跃,强大到在google上搜索为什么java比c++快居然会有200W的结果

通常JIT的有以下几种手段来优化JVM的性能

  1. 针对特定CPU型号的编译优化,JVM会利用不同CPU支持的SIMD指令集来编译热点代码,提升性能。像intel支持的SSE2指令集在特定情况下可以提升近40倍的性能。
  2. 减少查表次数。比如调用Object.equals()方法,如果运行时发现一直是String对象的equals,编译后的代码可以直接调用String.equals方法,跳过查找该调用哪个方法的步骤。
  3. 逃逸分析。JAVA变量默认是分配在主存的堆上,但是如果方法中的变量未逃出使用的生命周期,不会被外部方法或者线程引用,可以考虑在栈上分配内存,减少GC压力。另外逃逸分析可以实现锁优化等提升性能方法。
  4. 寄存器分配,部分变量可以分配在寄存器中,相对于主存读取,更大的提升读取性能。
  5. 针对热点代码编译好的机器码进行缓存。代码缓存具有固定的大小,并且一旦它被填满,JVM 则不能再编译更多的代码。
  6. 方法内联,也是JIT实现的非常有用的优化能力,同时是开发者能够简单参与JIT性能调优的地方。

方法内联是什么。为什么它能够提升性能

要搞清楚为什么方法内联有用,首先要知道当一个函数被调用的时候发生了什么

  1. 首先会有个栈,存储目前所有活跃的方法,以及它们的本地变量和参数
  2. 当一个新的方法被调用了,一个新的栈帧会被加到栈顶,分配的本地变量和参数会存储在这个栈帧
  3. 跳到目标方法代码执行
  4. 方法返回的时候,本地方法和参数会被销毁,栈顶被移除
  5. 返回原来地址执行

因此,函数调用需要有一定的时间开销和空间开销,当一个方法体不大,但又频繁被调用时,这个时间和空间开销会相对变得很大,变得非常不划算,同时降低了程序的性能。

方法内联就是把被调用方函数代码"复制"到调用方函数中,来减少因函数调用开销的技术。

被内联前的代码

private int add4(int x1, int x2, int x3, int x4) { 
    return add2(x1, x2) + add2(x3, x4); 
}

private int add2(int x1, int x2) {  
    return x1 + x2;  
}  

运行一段时间后,代码被内联翻译成

private int add4(int x1, int x2, int x3, int x4) { 
    return x1 + x2 + x3 + x4; 
}

JVM会自动的识别热点方法,并对它们使用方法内联优化。那么一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置:

1、使用client编译器时,默认为1500;
2、使用server编译器时,默认为10000;

但是一个方法就算被JVM标注成为热点方法,JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了,分为两种情况。

  • 如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过** -XX:MaxFreqInlineSize=N**来设置这个大小)
  • 如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过** -XX:MaxInlineSize=N **来设置这个大小)

我们可以通过增加这个大小,以便更多的方法可以进行内联;但是除非能够显著提升性能,否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多,更少的热点方法会被缓存,最终的效果不一定好。

如果想要知道方法被内联的情况,可以使用下面的JVM参数来配置

-XX:+PrintCompilation: Prints out when JIT compilation happens
-XX:+UnlockDiagnosticVMOptions: Is needed to use flags like -XX:+PrintInlining
-XX:+PrintInlining: Prints what methods were inlined

Netty库上使用内联优化的例子

ISSUE地址:https://github.com/netty/netty/issues/1812

NioMessageUnsafe是Netty常用的读取Socket channel,处理readbuf方法。

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());
                }
            }
        }
        ...
    }

使用netty的官方HTTP sever hello work例子,启动的时候加入jit分析参数

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 

并对其做了一段时间压测后

 66527  370             io.netty.channel.nio.NioEventLoop::processSelectedKeysOptimized (80 bytes)
                        @ 14   java.nio.channels.SelectionKey::attachment (5 bytes)   inline (hot)
           !            @ 33   io.netty.channel.nio.NioEventLoop::processSelectedKey (120 bytes)   inline (hot)
                          @ 1   io.netty.channel.nio.AbstractNioChannel::unsafe (8 bytes)   inline (hot)
                            @ 1   io.netty.channel.AbstractChannel::unsafe (5 bytes)   inline (hot)
                          @ 6   java.nio.channels.spi.AbstractSelectionKey::isValid (5 bytes)   inline (hot)
                          @ 26   sun.nio.ch.SelectionKeyImpl::readyOps (9 bytes)   inline (hot)
                            @ 1   sun.nio.ch.SelectionKeyImpl::ensureValid (16 bytes)   inline (hot)
                              @ 1   java.nio.channels.spi.AbstractSelectionKey::isValid (5 bytes)   inline (hot)
           !              @ 42   io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe::read (191 bytes)   inline (hot)
           !              @ 42   io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe::read (327 bytes)   hot method too big
                            @ 4   io.netty.channel.socket.nio.NioSocketChannel::config (5 bytes)   inline (hot)
                              @ 1   io.netty.channel.socket.nio.NioSocketChannel::config (5 bytes)   inline (hot)
                            @ 12   io.netty.channel.AbstractChannel::pipeline (5 bytes)   inline (hot)
                            @ 17   io.netty.channel.DefaultChannelConfig::getAllocator (5 bytes)   inline (hot)
                            @ 24   io.netty.channel.DefaultChannelConfig::getMaxMessagesPerRead (5 bytes)   inline (hot)
                            @ 44   io.netty.channel.DefaultChannelConfig::getRecvByteBufAllocator (5 bytes)   inline (hot)
                            @ 49   io.netty.channel.AdaptiveRecvByteBufAllocator::newHandle (20 bytes)   executed < MinInlining Threshold times

NioMessageUnsafe.read方法体因为过大无法被inline优化。当对read方法进行简单拆分,减少每个方法体大小之后, 简化代码如下

 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();
            ...
        }
        ...
    }
   

再次压测后的日志,NioMessageUnsafe.read方法被顺利加入内联优化。

               !              @ 42   io.netty.channel.nio.AbstractNioMessageChannel$NioMessageUnsafe::read (288 bytes)   inline (hot)

方法内联的条件

虽然JIT号称可以针对代码全局的运行情况而优化,但是JIT对一个方法内联之后,还是可能因为方法被继承,导致需要类型检查而没有达到性能的效果

想要对热点的方法使用上内联的优化方法,最好尽量使用final、private、static这些修饰符修饰方法,避免方法因为继承,导致需要额外的类型检查,而出现效果不好情况。

结论

热点方法的内联优化建议

  1. 更小的方法体
  2. 尽量使用final、private、static修饰符
  3. 使用+PrintInlining参数校验效果

你的支持是我最大的动力,点下方的赞赏支持吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,528评论 3 83
  • 该文章为转载,原文章请点击 1. 背景 1.1. Netty 3.X系列版本现状 根据对Netty社区部分用户的调...
    Pramyness阅读 1,965评论 1 14
  • 经常想什么是生活,我想要的是什么生活?都说父母是孩子最好的老师,确实这样,我父母的目标就是过上高质量的生活,...
    十二月的阳光阅读 198评论 0 0
  • 初级 基本是对手速的考验 难度程度:★★☆☆☆ http://colorrun.pl/ ​ 测试比较简单,就是从两...
    原设计阅读 33,488评论 1 2
  • 这本书,给了我们一个新的概念,那就是: 以输掉比赛为第一目标。 我们人人都害怕失败,但失败能够成为成功之母的头等要...
    程晓晓阅读 197评论 0 0