指令优化
在谈到优化之前我们先看一个简单例子,非常简单的例子,查看编译后的文件的的指令是什么样子的,一个非常简单的java程序,Hello.java
1
2
3
4
5
6
7
8
|
public class Hello {
public String getName() {
return "a" ;
}
public static void main(String []args) {
new Hello().getName();
}
} |
我们看看这段代码编译后指令会形成什么样子:
C:\>javac Hello.java
C:\>javap -verbose -private Hello
Compiled from “Hello.java”
public class Hello extends java.lang.Object
SourceFile: “Hello.java”
minor version: 0
major version: 50
Constant pool:
const #1 = Method #6.#17; // java/lang/Object.”<init>”:()V
const #2 = String #18; // a
const #3 = class #19; // Hello
const #4 = Method #3.#17; // Hello.”<init>”:()V
const #5 = Method #3.#20; // Hello.getName:()Ljava/lang/Stri
const #6 = class #21; // java/lang/Object
const #7 = Asciz <init>;
const #8 = Asciz ()V;
const #9 = Asciz Code;
const #10 = Asciz LineNumberTable;
const #11 = Asciz getName;
const #12 = Asciz ()Ljava/lang/String;;
const #13 = Asciz main;
const #14 = Asciz ([Ljava/lang/String;)V;
const #15 = Asciz SourceFile;
const #16 = Asciz Hello.java;
const #17 = NameAndType #7:#8;// "<init>":()V
const #18 = Asciz a;
const #19 = Asciz Hello;
const #20 = NameAndType #11:#12;// getName:()Ljava/lang/String;
const #21 = Asciz java/lang/Object;
{
public Hello();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
public java.lang.String getName();
Code:
Stack=1, Locals=1, Args_size=1
0: ldc #2; //String a
2: areturn
LineNumberTable:
line 14: 0
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=1, Args_size=1
0: new #3; //class Hello
3: dup
4: invokespecial #4; //Method “<init>”:()V
7: invokevirtual #5; //Method getName:()Ljava/lang/String;
10: pop
11: return
LineNumberTable:
line 26: 0
line 30: 11
}
看起来乱七八糟,不要着急,这是一个最简单的java程序,我们按照正常的程序思路从main方法开始看,首先第一行是告诉你new #3;//class Hello,这个地方相当于执行了new Hello()这个命令,而#3是什么意思呢,在前面编译的指令列表中,找到对应的#3的位置,这就是我们所谓的入口位置,如果指令还要去寻找下一个指令就跟着#找到就可以了,就想刚才#3又找到#19,其实是要找到Hello的定义,也就是要引用到Class的定义的位置。
继续看下一步(关于内部入栈出栈的指令我们这里不多说明),invokespecial #4; //Method “<init>”:()V,这个貌似看不太懂,不过可以看到后面是一个init方法,它到底初始化了什么,我们这里因为只有一行代码,我们姑且相信它初始化了Hello,不过invokespecial不是对super进行调用的时候才用到的吗?所以这里需要补充一下的就是当对象的初始化的时候,也会调用它,这里的初始化方法就是构造方法了,在指令的时候统一命名为init的说法;
那么调用它的构造方法,如果没有构造方法,肯定会进入Hello的默认构造方法,我们看看上面的public Hello(),发现它内部就执行了一条指令就是调用又调用一个invokespecial指令,这个指令其实就是初始化Object父对象的。
再继续看下一条指令:invokevirtual #5; //Method getName:()Ljava/lang/String;你会发现是调用了getName的方法,采用的就是我们原先说的invokevirtual的指令,那么根据到getName方法部分去:
会发现直接做了一个ldc #2; //String a操作就返回了,获取到对应的数据的地址后就直接返回了,执行的指令在位置#2,也就是在常量池中的一个2。
好了一个简单的程序指令就分析到这里了,更多的指令大家可以自己去分析,你就可以看明白java在指令上是如何处理的了,甚至于可以看出java在继承、内部类、静态内部类的包含关系是如何实现的了,它并不是没用,当你想成为一个更为专业和优秀的程序员,你应该知道这些,才能让你对这门驾驭得更加自如。
几个简单的测试下来,会发现一些常见的东西,比如
==>你继承一个类,那个类里面有一个public方法,在编译后,你会发现这个父亲类的方法的指令部分会被拷贝到子类中的最后面来
==>而当使用String做 “+” 的时候,那怕是多个 “+” ,JVM会自动编译指令时编译为StringBuilder的append的操作(JDK 1.5以前是StringBuffer),大家都知道append的操作将比 + 操作快非常的倍数,既然JVM做了这个指令转换,那么为什么还这么慢呢,当你发现java代码中的每一行做完这种+操作的时候,StringBuilder将会做一个toString()操作,如果下一次再运行就要申请一个新的StringBuilder,它的空间浪费在于toString和反复的空间申请;并且我们在前面探讨过,在默认情况下这个空间数组的大小是10,当超过这个大小时,将会申请一个双倍的空间来存放,并进行一次数组内容的拷贝,此时又存在一个内部空间转换的问题,就导致更多的问题,所以在单个String的加法操作中而且字符串不是太长的情况下,使用+是没有问题的,性能上也无所谓;当你采用很多循环、或者多条语句中字符串进行加法操作时,你就要注意了,比如读取文件这类;比如采用String a = “dd” + “bb” + “aa”;它在运行时的效率将会等价于StringBuilder buf = new StringBuilder().append(“dd”).append(“bb”).append(“aa”);
但是当发生以下情况的时候就不等价了(也就是不要在所有情况下寄希望于JVM为你优化所有的代码,因为代码具有很多不确定因素,JVM只是去优化一些常见的情况):
1、字符串总和长度超过默认10个字符长度(一般不是太长也看不出区别,因为本身也不慢)。
2、多次调用如上面的语句修改为String a = “dd”;a += “bb”; a += “aa”;与上面的那条语句的执行效率和空间开销都是完全不一样的,尤其是很多的时候。
3、循环,其实循环的基础就是来源于第二点的多次调用加法,当循环时肯定是多次调用这条语句;因为Java不知道你下一条语句要做什么,所以加法操作,它不得不将它toString返回给你。
==>继续测试你会发现内部类、静态内部类的一些特征,其实是将他编辑成为一个外部的class文件,用了一些$标志符号来分隔,并且你会发现内部类编译后的指令会将外包类的内容包含进来,只是他们通过一些标志符号来标志出它是内部类,它是那个类的内部类,而它是静态的还是静态的特征,用以在运行时如何来完成调用。
==>另外通过一些测试你还会发现java在编译时就优化的一个动作,当你的常量在编译时可能会在一些判定语句中直接被解析完成,比如一个boolean类型常量IS_PROD_SYS(表示是否为生产环境),如果这个常量如果是false,在一段代码中如果出现了代码片段:
1
2
3
|
if (IS_PROD_SYS) {
.....
} |
此时JVM编译器在运行时将会直接放弃这段代码,认为这段代码是没有意义的;反之,当你的值为true的时候,编译器会认为这个判定语句是无效的,编译后的代码,将会直接抛弃掉if语句,而直接运行内部的代码;这个大家在编译后的class文件通过反编译工具也可以看得出来的;其实java在运行时还做了很多的动作,下面再说说一些简单的优化,不过很多细节还是需要在工作中去发现,或者参考一些JVM规范的说明来完善知识。
上面虽然说明了很多测试结果所表明的JVM所为程序所做的优化,但是实际的情况却远远不止如此,本文也无法完全诠释JVM的真谛,而只是一个开头,其余的希望各位自己可以做相应的测试操作;
说完了一些常见的指令如何查看,以及通过查看指令得到一些结论,我们现在来看下指令在调用时候一般的优化方法一般有哪些(这里主要是在跨方法调用上,大家都知道,java方法建议很小,而且来回层次调用非常多,但是java依然推荐这样写,由上面的分析不得不说明的是,这样写了后,java来回调用会经过非常的class寻址以及在class对对内部的方法名称进行符号查表操作,虽然Hash算法可以让我们的查表提速非常的倍数,但是毕竟还是需要查表的,这些不变化的东西,我们不愿意让他反复的去做,因为作为底层程序,这样的开销是伤不起的,JVM也不会那么傻,我们来看看它到底做了什么):
==>在上面都看到,要去调用一个方法的call site,是非常麻烦的事情,虽然说static的是可以直接定位的,但是我们很多方法都不是,都是需要找到class的入口(虽然说Class的转换只需要一次,但是内部的方法调用并不是),然后查表定位,如果每个请求都是这样,就太麻烦了,我们想让内部的放入入口地址也只有一次,怎么弄呢?
==>在前面我们说了,JVM在加载后,一般不使用特殊的API,是不会造成Class的变化的,那么它在计算偏移量的时候,就可以在指令执行的过程中,将目标指令记忆,也就是在当前方法第一次翻译为指令时,在查找到目标方法的调用点后,我们希望在指令的后面记录下调用点的位置,下次多个请求调用到这个位置时,就不用再去寻找一次代码段了,而直接可以调用到目标地址的指令。
==>通过上面的优化我们貌似已经满足了自己的想法,不过很多时候我们愿意将性能进行到底,也就是在C++中有一种传说中的内联,inline,所以JVM在运行时优化中,如果发现目标方法的方法指令非常小的情况下,它会将目标方法的指令直接拷贝到自己的指令后面,而不需要再通过一次寻址时间,而是直接向下运行,所以JVM很多时候我们推荐使用小方法,这样对代码很清晰,对性能也不错,大的方法JVM是拒绝内联的(在C++中,这种内联需要自己去指定,而并非由系统完成,正常C++的指令也是按照入口+偏移量来找到的)
==>而对于继承关系的优化,通过层次模型的分析,我们在第四章中就已经说明,也就是利用一般情况下多态中的单个链中对应的对象的重写方法数组肯定不会太长,所以在Class的定义时我们就知道自下向上有多少个重写方法,而不是运行时才知道的,这个也叫做编译时的层次分析。
相关推荐
《淘宝JVM优化实践》是一份深入探讨阿里巴巴集团核心系统研发部在JVM优化方面的实践经验的文档。这份文档主要涵盖了以下几个关键知识点: 1. **淘宝JVM优化背景**:随着淘宝、天猫等业务的快速发展,其Java应用规模...
大厂架构师-日均百万订单量的JVM优化与高级GC调优策略实战(5.8G) 〖课程介绍〗: 来自顶尖大厂的架构师级JVM优化与GC调优策略实战课程,是具备有尖端技术的优化课程。在课程内容上几乎不用过多的介绍,单是查阅目录就...
### JVM优化及面试热点分析 #### 一、Java技术概览与重要性 Java作为一门历史悠久且广泛应用的编程语言,其重要性和影响力不言而喻。根据TIOBE编程语言排行榜,Java一直稳居首位,这不仅是因为它拥有庞大的开发者...
在JVM优化中,内存配置是关键的一环。例如,`-Xms2048m -Xmx2048m`设置了JVM的最小和最大堆内存为2GB,`-Xmn512m`指定了新生代的大小为512MB,`-XX:MaxPermSize=256m`设定了永久代的最大值为256MB,而`-Xss128k`则...
JVM优化方法
- 了解下我们为什么要学习JVM优化 - 掌握jvm的运行参数以及参数的设置 - 掌握jvm的内存模型(堆内存) - 掌握jamp命令的使用以及通过MAT工具进行分析 - 掌握定位分析内存溢出的方法 - 掌握jstack命令的使用 - 掌握...
为了深入理解JVM优化的相关知识点,我们可以从多个角度来探讨。首先,需要理解JVM(Java虚拟机)的基本概念,以及在性能调优中的关键作用。JVM是运行Java程序的核心组件,负责解释Java字节码,并将字节码转换成特定...
"jvm优化参数配置"是确保Tomcat稳定运行的关键环节,能够提高应用的响应速度,减少内存泄露,提升系统整体性能。以下是对JVM参数优化的详细解释: 1. **内存配置**: - **堆内存(Heap Memory)**:分为新生代...
【标题】"JVM优化经验总结Java开发Java经验技巧共15页.p" 提供的信息表明,这是一份关于Java开发中的JVM优化经验的详细总结,共有15页的内容。在Java开发过程中,理解并掌握JVM(Java虚拟机)的优化技巧是至关重要的...
本篇文件内容主要介绍了JVM优化的第三部分,重点围绕Tomcat参数调优、JVM参数调优、JVM字节码优化以及代码优化等几个方面。下面是针对这些知识点的详细解释: 1. Tomcat参数调优 在Tomcat参数调优部分,首先介绍了...
"Jvm优化的Java - Demo"是一个项目,旨在演示如何调整JVM参数来优化Java应用程序的性能。通过这个项目,我们可以学习到JVM调优的一些核心概念和实践技巧。 首先,JVM参数调整是提高应用程序性能的重要手段。这通常...
【JVM优化】Java的JVM优化涉及到许多关键知识点,主要目标是确保应用程序在生产环境中稳定、高效地运行。在本地开发环境中,由于资源限制不严,通常不需要过多关注JVM优化,但在生产环境中,面临如应用卡死、日志无...
这个压缩包文件"JVM优化3(Tomcat参数调优,JVM参数调优,jvm字节码,代码优化).zip"显然包含了关于如何优化Java应用程序运行效率的四个主要方面:Tomcat服务器的参数调整、JVM参数调优、JVM字节码理解和优化以及代码...
本知识点将详细介绍JVM内存优化相关的概念和工具,帮助初学者快速掌握JVM优化的入门知识。 首先,了解JVM堆内存的设置是非常重要的。堆内存是JVM所管理的内存中最大的一块,主要用于存放对象实例。堆的大小对Java...
在"jvm优化学习资源学习及讲义说明"中,我们可以深入探讨以下几个关键知识点: 1. **JVM架构**:了解JVM的内存模型,包括堆内存(Heap)、方法区(Method Area)、栈内存(Stack)、本地方法栈(Native Method ...
### JVM优化与OOM分析 #### 一、JVM的重要性与作用 JVM(Java Virtual Machine)作为Java程序的运行环境,对于确保Java程序能够跨平台运行具有重要意义。它不仅提供了执行字节码的基础,还负责内存管理、垃圾回收...
"java第五阶段(项目梳理+面试题准备+jvm优化+mysql优化)"这个主题涵盖了多个关键的知识领域,对于提升Java开发者的技术深度和面试竞争力至关重要。以下是这些领域的详细解释: 1. **JVM优化**:Java虚拟机(JVM)...
在Java开发中,JVM(Java Virtual Machine)的优化是提升应用程序性能的关键环节。本文主要围绕HotSpot JVM的优化展开,旨在防止出现内存溢出...记住,JVM优化是一个持续的过程,需要根据应用的实际运行情况进行调整。