也以這個圖為例,從.java到.class是編譯過程,從.class到機器碼是解釋過程。下面對其進行分別優化。在最佳化過程中,對編譯階段的最佳化主要是對前端編譯器的最佳化,在執行階段的最佳化,主要是對即時編譯器的最佳化。
編譯器最佳化
編譯過程
#以上為javac的編譯過程圖,以下為javac編譯過程的主體程式碼。
在以下對其步驟進行詳細解讀
1、解析與填滿符號表詞法分析將原始程式碼的字元流轉變為標記(Token)集合,標記是編譯過程中的最小元素,如a,=,b,int。語法分析
根據Token序列建構抽象語法樹。以後編譯器基本上不會再對原始碼檔案進行操作了,後續的操作都是建立在抽象語法樹上。抽象語法樹是一種用來描述程式碼語法結構的樹形表示方式,節點代表程式碼中的一個語法結構,例如修飾符,傳回值等。
填滿符號表符號表是由一組符號位址和符號資訊構成的表格,用於編譯的不同階段。如在語意分析中,用於語意檢查和產生中間代碼;在目標代碼產生階段,用於位址分配的依據。
2、註解處理器
這部分是插入式註解處理器在編譯期間對註解進行處理的過程。其可以對語法樹進行修改,一旦進行了修改,編譯器將回到上面的第一步重新處理,每一次循環稱為一個Round,也就是上圖中的回環過程。
3、語意分析與字節碼生成在經過語法分析後,生成的語法樹是一個結構正確的原始程式的抽象,但無法保證原始程式是符合邏輯的。語意分析的任務是對結構上正確的原始程式進行上下文有關性質的審查。例如下面程式碼中的錯誤只能在語意分析階段檢查出來。boolean a=false; char b=2; int c=a+b
標註檢查
變數使用前是否已被宣告、變數與賦值之間的數據類型是否能夠匹配等。還有一個常量折疊,就是把a=1 2變成a=3。所以在程式碼中的a=1 2和a=3並不會增加程式運行期間cpu指令的運算量。
資料及控制流程分析
檢查程式局部變數在使用前是否有賦值,方法的每條路徑是否都有回傳值,是否所有的受查異常都被正確處理了等問題。在類別載入時也有資料及控制流程分析,其目的基本上是一致的,但校驗的範圍不同,有些校驗項只有在編譯期或執行期才能運作。
解語法糖
語法糖是在電腦語言中加入某種語法,其對語言的功能沒有影響,但是能提高程式的可讀性。語法糖包括泛型,自動拆裝箱等。虛擬機器運行時不支援這些語法,它們在編譯階段還原回基礎語法結構。這個過程稱為解語法糖。 #########字節碼產生#########將先前步驟產生的資訊(語法樹、符號表)轉換成字節碼寫到磁碟中,然後新增並轉換了少量的代碼。如把字串的加運算替換為StringBuffer或StringBuilder的append()操作。 ######至此,Class檔案產生了。 ###语法糖
语法糖是java中添加某种语法,对语言的功能没有影响,但是可以增加程序的可读性。包括泛型、内部类、枚举类等。
1、泛型与类型擦除
泛型可用于类、接口和方法的创建中,用于对放入集合元素的类型的约束。泛型只在程序源码中存在,在编译阶段有解语法糖的步骤,所以在.Class文件中,已经变为了原来的原生类型了。这个过程叫做类型擦除。
泛型擦除前:
public static void main(String[] args){ Map<String,String> map=new HashMap<>(); map.put("姓名","小明"); map.put("性别","男"); sout(map.get("姓名")); sout(map.get("性别")); }
泛型擦除后:
public static void main(String[] args){ Map map=new HashMap(); map.put("姓名","小明"); map.put("性别","男"); sout((String)map.get("姓名")); sout((String)map.get("性别")); }
所以ArrayList和ArrayList在运行期时是同一个类。
2、自动拆装箱、循环遍历
这些是java中使用最多的语法糖。编译前:
public static void main(String[] args){ List<Integer> list=Arrays.asList(1,2,3,4); int sum=0; for(int i:list){ sum +=i; } System.out.println(sum); }
编译后:
public static void main(String[] args){ List list=Arrays.asList(new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)}); int sum=0; for(Iterator localIterator=list.iterator();localIterator.hasNext();){ int i=((Integer)localIterator.next()).intValue(); sum +=i; } System.out.println(sum); }
可见,自动拆装箱在编译后被转化为了对应的包装和还原方法,如Integer.valueOf()和Integer.intValue()。
遍历循环则把代码还原为了迭代器的实现。
3、条件编译
根据布尔常量值的真假,编译器会把分支中不成立的代码块消除掉。
public static void main(String[] args){ if(true){ sout("block 1"); }else{ sout("block 2"); } }
编译后,代码变为:
public static void main(String[] args){ sout("block 1"); }
运行期优化
一般情况下,我们将.java编译成.class,.class再解释成机器码。但是也有特殊的情况。有些代码调用比较频繁,比如某个方法或代码块的运行特别频繁,为了提高程序的执行效率,在运行时,虚拟机会把这个代码直接编译成机器码,并进行各种层次的优化。这样的代码称为热点代码。完成这个任务的编译器被称为即时编译器。但是其并不是虚拟机必需的部分。
即时编译器的概述
(1)为什么虚拟机要使用解释器和编译器并存的架构?
虚拟机里包含着解释器和编译器。当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译来提升效率。
(2)为什么虚拟机要实现两个不同的即时编译器?
虚拟机中内置了两个即时编译器,分别为Client Compiler和Server Compiler,又称为C1和C2。
默认只使用其中的一个,至于选择哪个,取决于虚拟机会根据自身版本和宿主机器的硬件性能自动选择运行模式。用户也可以使用“-client”、“-server”进行指定。
(3)程序何时使用解释器执行?何时使用编译器执行?
虚拟机有一个分层编译策略。
第0层:程序解释执行,解释器不开启性能监控功能,可触发第1层编译
第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
第2层:也称为C2编译,将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
(4)哪些程序代码会被编译为本地代码?如何编译为本地代码?
热点代码包括如下两类,其均把整个方法作为编译对象。
a、被多次调用的方法
b、被多次执行的循环体
热点探测是用来判断一段代码是否为热点代码,其方式有两种:
a、基于采样
b、基于计数器。HotSpot使用的是这种。它为每个方法准备了两类计数器:统计方法被调用次数的方法调用计数器和统计一个方法中循环体代码执行次数的回边计数器。
(5)如何从外部观察及时编译器的编译过程和编译结果?
可以使用 -xx:+PrintCompilation 查看哪些方法被即时编译器编译了。
优化技术有哪些?
虚拟机的即时编译器在生成代码时,采用了如下的代码优化技术。
(1)公共子表达式消除
如果一个表达式E已经计算过了,那如果再次出现E时就不会再对它进行计算。比如:
int d=(a*b)*12+c+(c+b*a)
如果这段代码交给javac编译器,则不会进行任何优化。如果交给即时编译器,会被进行如下步骤的优化:
第一步:消除公共子表达式
int d=E*12+c+(c+E)
第二步:代数化简:
int d=E*13+c*2
(2)数组边界检查消除
数组边界检查是什么?
如果有一个数组foo[],在java语言中访问数组元素foo[i]的时候,系统将会自动进行上下界的范围检查,检查i是否满足0≤i≤foo.length这个条件。
那怎么进行消除呢?
a、把运行期检查提到编译期完成。如foo[3],只要在编译期根据数据流分析来确定foo.length的值,并判断下标“3”没有越界,执行的时候就不用判断了。
b、隐式异常处理。这种思路通常用于空指针检查和算符运算中除数为零的情况。
if(foo!=null){ return foo.value; }else{ throw new NullPointException(); }
被隐式异常处理优化后,变为如下代码:
try{ return foo.value; }catch(segment_fault){ uncommon_trap(); }
除了数组边界检查消除,还有自动装箱消除、安全点消除、消除反射等。
(3)方法内联
把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用。
public int add(int x1, int x2, int x3, int x4) { return add1(x1, x2) + add1(x3, x4); } public int add1(int x1, int x2) { return x1 + x2; }
运行一段时间后JVM会把add1方法去掉,并把代码翻译成:
public int add(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }
(4)逃逸分析
当一个对象在方法中被定义后,它可能被外部方法所引用,比如作为调用参数传递到其它方法中,这称为方法逃逸。同理,如果被外部线程访问到,它就称为线程逃逸。
对变量进行相应分析就叫做逃逸分析。如果能证明别的方法或线程无法通过任何途径访问到这个对象,则可以为这个变量进行一些优化。
优化的手段有栈上分配、同步消除、标量替换等。以同步消除为例,如果逃逸分析能够确定一个变量不会逃逸出线程,即无法被其它线程访问到,那对这个变量实施的同步措施就可以消除掉了。
以上内容便是关于JAVA虚拟机中JVM优化的全部介绍,更多相关问题请访问PHP中文网:JAVA视频教程
以上是JAVA虛擬機器(JVM)詳細介紹(七)-JVM最佳化的詳細內容。更多資訊請關注PHP中文網其他相關文章!