首页 > 编程知识 正文

jvm调优实战简书,jvm调优过程

时间:2023-05-05 20:30:19 阅读:203469 作者:951

是不是所有的对象和数组都会在堆内存分配空间?

逃逸分析(Escapse Analysis)是目前虚拟机中比较前沿的优化技术。之所以称为优化技术,他并不是一种直接优化代码的手段,而是穿插与其他优化技术之中,为其他优化技术提供分析技术

1.概述

在讨论逃逸分析之前,我们先举一个生活中的例子——交通肇事逃逸。说道这个词,大家就熟悉多了,机动车驾驶员在发生交通事故的同时,擅自逃离事故现场,使交通事故所引起的民事、刑事、行政责任无法确定,其目的在于推卸、逃脱责任的行为。也就是说,逃逸就是擅自离开规定的范围。而对于Java对象的逃逸分析也是一样,他主要分析对象动态的作用域(这个作用域一般指的是方法体内的作用域)。按照我们正常的思维,一个对象在一个方法总被创建,那么当方法消亡,他也会随着一起消亡。然而,作为一个有经验的开发人员可知,当对象的引用通过调用参数或者返回参数这条途径,会成功逃出方法体本身,进而被其他方法变量引用,这样就成功的逃离了作案现场,我们称之为方法逃逸。甚至还有可能被外部线程访问到,我们最常见的就是静态变量(类变量)赋值,我们称之为线程逃逸。

大家想一下,如果一个方法内的对象没有发生逃逸,我们可不可以在方法被销毁的时候直接把对象销毁掉,这样变可以大大减轻垃圾收集器的压力,缩短了“stop the world”,进而提高的性能,增强了用户体验。
看代码:

class A { public static B b; public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸 b = new B(); } public B methodPointerEscape() { // 方法返回值,发生逃逸 return new B(); } public void instancePassPointerEscape() { methodPointerEscape().printClassName(this); // 实例引用传递,发生逃逸 }} class B {public void printClassName(A a) { System.out.println(a.class.getName());}}

在这个例子中,一共举了3种常见的指针逃逸场景。分别是 全局变量赋值,方法返回值,实例引用传递。
其实逃逸主要就是分为从不逃逸、方法逃逸和线程逃逸
逃逸分析优化JVM原理我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针。
当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力,也间接影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。
接下来,举一个场景来阐述。
假设在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。
按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。
这是优化前,JVM的处理方式。
逃逸分析优化 – 栈上分配
优化原理:分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量也被回收。
这是优化后的处理方式,对比可以看出,主要区别在栈空间直接作为临时对象的存储介质。从而减少了临时对象在堆内的分配数量。
逃逸分析另一个重要的优化 – 锁省略
如果通过逃逸分析能够判断出指向某个局部变量的多个引用被限制在同一方法体内,并且所有这些引用都不能“逃逸”到这个方法体以外的地方,那么HotSpot会要求JIT执行一项优化动作 – 将局部变量上拥有的锁省略掉。
这就是锁省略(lock elision)。
性能测试

class DoubleSlot { final int value1; final int value2; public DoubleSlot(int value1, int value2) { this.value1 = value1; this.value2 = value2; }}static int slotValue(DoubleSlot slot) { return slot.value1 + slot.value2;}static int sum(int[] values) { int sum = 0; int length = values.length; for(int i=1; i<100; i++) sum(values); for(int i=0; i<100; i++) test(values);}

测试结果是:
$ /usr/jdk/jdk1.6.0_14/结实的大白/java -server EscapeAnalysisTest
time 8889261
$ /usr/jdk/jdk1.6.0_14/结实的大白/java -server -XX:+DoEscapeAnalysis EscapeAnalysisTest
time 1408140
从结果中,可以看到,启用逃逸分析的运行性能6倍于未启用。
JVM中启用逃逸分析 DoEscapeAnalysis安装jdk1.6.0_14,运行java时传递jvm参数 -XX:+DoEscapeAnalysis

2.优化方式 2.1 栈上分配(Stack Allocation)

在Java虚拟机中,对象是创建在Java堆中,这是每一位有经验Java开发人员在熟悉不过的常识。Java堆中的对象是对于各个线程都是共享和可见的,主要持有这个对象的引用,就可以轻松访问存储在对象的数据。虚拟机的垃圾收集器也可以回收不再使用的对象,但是回收动作无论是筛选可回收对象,还是回收和整理内存都需要消耗时间和性能。如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将是一个大胆而又很不错的注意。大家都知道,JVM虚拟机中80-90%的对象都是朝生夕死的,如果对象所占用的内存空间随着栈帧出栈而销毁,那垃圾收集器的压力就会大大减小。

2.2 同步消除(Synchronization Elimination)

对于多线程的应用而言,我们通过同步代码块实现线程的安全。然而线程同步代码块也是一个相当耗费性能的过程。如果通过逃逸分析确定这个对象不会逃逸出线程,即无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,这样,我们就可以大胆的把同步线程代码块消除。

2.3 标量替换(Scalar Replacement)

如果我问一下什么是对象,大家一定会想到封装继承多态。然而大家想过没有,所谓的对象,也可以理解成最终由一个或多个原始数据类型(int,long等)组成。这里的对象可以认为是聚合量,原始数据类型就是所谓的标量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象呗拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建他的若干个呗这个方法使用到的成员变量带代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创建条件。

3.权衡分析

逃逸分析是一个相当复杂的过程,那么逃逸分析所要消耗的性能与逃逸分析多带来的性能提升究竟哪个大,还是一个未知事项。如果经过逃逸分析后发现没有几个不逃逸的对象,那简直就是得不偿失了。所以java虚拟机只能对于时间压力相对较小的算法来完成逃逸分析。

4.参数设置

手动开启逃逸分析:-XX:+PrintEscapeAnaysis

开启标量替换:-XX:EliminateAllocations

开启同步消除:+XX:+EliminateLocks

标量替换情况:-XX:+PrintEliminateAllocations

jvm在编译阶段引入了JIT(即时编译) 技术,而随着这种技术的成熟,栈上分配、标量替换优化技术也产生了一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了,
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。