JVM 基础篇:运行时数据区

目录

1.程序计数器(PC寄存器)

2.Java虚拟机栈

2.1局部变量表

2.2操作数栈

2.3帧数据

2.3.1动态链接

2.3.2方法出口

2.3.3异常表

2.4问题辨析

2.5内存溢出

3.本地方法栈

4.堆

4.1堆的演进

4.2堆的内容

对象实例

字符串常量池

静态变量

4.3问题辨析

4.4内存溢出

5.方法区

5.1方法区和永久代以及元空间的关系

5.2为什么要将 永久代 替换为 元空间

5.3方法区的内容

①类元信息(Klass)

②运行时常量池

5.4问题辨析

5.5方法区的特点

5.6设置方法区的大小

6.再谈字符串常量池(StringTable)

6.1字面量创建字符串

6.2字符串变量拼接

6.3字符串常量拼接

6.4 intern方法

7.直接内存

8.逃逸分析

①栈上分配

②标量分析

③同步消除


Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本略有不同。

JDK 1.7

JDK 1.8

1.程序计数器(PC寄存器)

程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址

一个程序计数器的具体案例: 

  • 在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。
  • 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。

所以程序计数器主要有两个作用:

  • 程序计数器可以控制程序指令的进行,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

程序计数器在运行中会出现内存溢出吗?

因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。

2.Java虚拟机栈

Java虚拟机栈描述的是Java方法执行的内存模型。Java虚拟机栈 采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧(Stack Frame)来保存

栈帧的组成

2.1局部变量表

  • 局部变量表的作用是在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容。
  • 栈帧中的局部变量表是一个数组,主要用于存储方法参数和定义在方法体内的局部变量,数组中每一个位置称之为槽(slot) ,每个变量槽都可以存储32位长度的内存空间,变量类型包括基本数据类型、对象引用(reference)。long和double类型占用两个槽,其他类型占用一个槽。
  • 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。如果是静态方法,则0号位不会存this。
  • 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。
  • 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。

变量槽复用

为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

private static void test1() {
    {
         String a = "a";
    }
    String b = "b";//此时变量a已经出了作用域,变量b会复用a的变量槽
}

代码中注释部分此时变量a已经出了作用域,变量b会复用a的变量槽

2.2操作数栈

  • 操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。
  • 编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。

2.3帧数据

2.3.1动态链接

我们知道在类的生命周期中,解析阶段会将常量池中的符号引用替换为直接引用。但是如果当前类的字节码指令引用了其他类的属性或者方法时,这里的符号引用是不会在连接阶段变成直接引用的

java虚拟机在执行这段字节码指令前,会在栈帧中保存一个动态链接。动态链接就保存了编号到运行时常量池的内存地址的映射关系。

2.3.2方法出口

方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。

2.3.3异常表

异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

2.4问题辨析

一个方法调用另一个方法,会创建很多栈帧吗?

会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者上面

垃圾回收是否涉及栈内存?

不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

栈内存分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

方法内的局部变量是否线程安全?

不一定,如果方法中使用了外部传入的可变对象,或者方法返回了可变对象,那么就可能存在线程不安全的问题。因为可变对象的状态是可以被多个线程同时修改的,所以在多线程环境下使用这些可变对象可能会导致线程安全问题。

2.5内存溢出

Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出

Java虚拟机栈内存溢出时会出现StackOverflowError的错误

Java虚拟机栈的默认大小

设置大小

3.本地方法栈

  • Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。
  • 在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。

本地方法(Native)

由于java是一门高级语言,离硬件底层比较远,有时候无法操作底层的资源,于是,java添加了native关键字,被native关键字修饰的方法可以用其他语言重写,这样,我们就可以写一个本地方法,然后用C语言重写,这样来操作底层资源。当然,使用了native方法会导致系统的可移植性不高,这是需要注意的。

像Object类中的许多方法就是native,即本地方法

4.堆

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。​​​​​​

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。

4.1堆的演进

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代中间一层属于老年代最右面一层属于永久代

(下图有点点误差,JDK7里堆应该是包括永久代的)

JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。 

4.2堆的内容

对象实例

  • 类初始化生成的对象
  • 基本数据类型的数组也是对象实例

字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 可以简单理解为一个固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象

JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。

静态变量

静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中

4.3问题辨析

JDK 1.7 为什么要将字符串常量池和静态变量移动到堆中

主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

4.4内存溢出

  • 堆空间有三个需要关注的值,used total max。
  • used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
  • 随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆。
  • 如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。堆这里最容易出现的就是 OutOfMemoryError 错误。
  • 如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。在实际应用中一般都需要设置 total和max的值。

设置大小

Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。

5.方法区

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的

5.1方法区和永久代以及元空间的关系

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

5.2为什么要将 永久代 替换为 元空间

《深入理解 Java 虚拟机》第 3 版 2.2.5:

整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

5.3方法区的内容

方法区用于存储已被虚拟机加载的Class类型信息、常量、即时编译器编译后的代码缓存等。而堆中主要存放的是实例化的对象。

方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

①类元信息(Klass)

方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象在类的加载阶段完成。

②运行时常量池

  • 方法区除了存储类的元信息之外,还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。
  • 字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,里面的符号地址会变为真实地址,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池
  • 运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法。

运行时常量池、方法区、字符串常量池这些都是不随虚拟机实现而改变的逻辑概念,是公共且抽象的,Metaspace、Heap 是与具体某种虚拟机实现相关的物理概念,是私有且具体的

5.4问题辨析

成员变量、局部变量、类变量分别存储在内存的什么地方?

类变量

  • 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁
  • 在java7之前把静态变量存放于方法区,在java7时存放在堆中

成员变量

  • 成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

由final修饰的常量存放在哪里?

final关键字并不影响在内存中的位置,具体位置请参考上一问题。

类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

  • 类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。
  • 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池
  • 对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。

5.5方法区的特点

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
  • 方法区是线程共享的,多个线程都用到一个类的时候,若这个类还未被加载,应该只有一个线程去加载类,其他线程等待;
  • 方法区有垃圾回收机制,一些类不再被使用则变为垃圾,需要进行垃圾清理。
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace。比如下面三种情况:
    • 加载大量的第三方的jar包;
    • Tomcat部署的工程过多(30~50个);
    • 大量动态的生成反射类;

5.6设置方法区的大小

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

6.再谈字符串常量池(StringTable)

字符串常量池和运行时常量池有什么关系?

早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整, 将字符串常量池和运行时常量池做了拆分。

6.1字面量创建字符串

public static void main(String[] args){
    String s1 = "a"; 
    String s2 = "b";
    String s3 = "ab";
}

在上面这段代码中,通过字符串字面量的方式新建了几个String。对于变量s1,s2,s3,我们都知道它们被存在了栈中。可是后面的字符串呢?它被存储在哪个地方呢?经过反编译,我们得到如下jvm指令。

  1. StringTable是一个哈希表,长度固定,“a”就是哈希表的key。一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]。
  2. 执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]。
  3. 同样地,后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]。

6.2字符串变量拼接

  1. 前面的指令我们已经很熟悉,观察行号为9的指令,这里是个new。这就说明d的创建方式和a、b、c不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。
  2. 观察行号9的指令后面的注释,可以知道这里是new了一个StringBuilder对象。接着看17,21,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。
  3. 最后看24,最后是调用了toString方法生成了新的字符串对象。
// StringBuilder中的toString方法
public String toString(){
    // 即根据拼接好的值,创建一个新的字符串对象
	return new String(value, 0, count);
}

 以上分析就想要说明:即当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象
  最后我们看输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。

6.3字符串常量拼接

观察下面的代码,请问输出结果是什么?

        String a="1";
        String b="2";
        String c="12";
        String d="1"+"2";
        System.out.println(c==d);//true

直接反编译:

可以看到 d 的创建和 c 一样。这其实是编译期的优化。编译期间,编译器发现这是两个常量相加,结果是确定的,所以就直接让 d 等于“12”。

6.4 intern方法

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

JDK7及以后

  • 如果串池中没有该字符串对象或引用,则将堆中对象的引用放入串池
  • 如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象引用
// 堆  new String("a")   new String("b") new String("ab")    ["a", "b"]
String s = new String("a") + new String("b");//此时只是堆中有个“ab”的对象,串池中无
String s2 = s.intern(); //将堆中对象的引用放入串池
String x = "ab";
System.out.println( s2 == x);//true
System.out.println( s == x );//true

JDK6

  • 如果串池中没有该字符串对象或引用,会将堆中的字符串对象复制一份放到串池中,最后返回StringTable中刚加入的对象。
  • 如果有该字符串对象,则放入失败,无论放入是否成功,都会返回串池中的字符串对象引用

7.直接内存

直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。

在 JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:

  1. Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
  2. IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。

现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。

调整直接内存的大小

8.逃逸分析

逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部 方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸

从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。 如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径 访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。

①栈上分配

栈上分配(Stack Allocations):在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是 Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。

如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。

在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

代码示例

private void test2() {
    String a = "a";
}

我们创建了对象“a”,但是该对象无论外部方法或者其他线程都是访问不到的,此时对象是分配在栈上随方法的退出而自动销毁

②标量分析

标量替换(Scalar Replacement):若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量Java 中的对象就是典型的聚合量

如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量 恢复为原始类型来访问,这个过程就称为标量替换。

假如逃逸分析能够证明一个对象不会被方法外部 访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上 (栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。

标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内

代码示例

public int test(int x) { 
	int xx = x + 2;
	Point p = new Point(xx, 42); 
	return p.getX();
}

其中Point类的代码,就是一个包含x和y坐标的POJO类型
经过逃逸分析,发现在整个test()方法的范围内Point对象实例不会发生任何程度的逃逸, 这样可以对它进行标量替换优化,把其内部的x和y直接置换出来,分解为test()方法内的局部变量,从 而避免Point对象实例被实际创建,优化后的结果如下:

public int test(int x) {
	 int xx = x + 2;
	 int px = xx; 
	 int py = 42 return px; 
}

通过数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效 代码消除得到最终优化结果

public int test(int x) { return x + 2; }

③同步消除

同步消除(Synchronization Elimination):线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉

private void test2(){
     System.out.println(new StringBuffer("a").append("b"));
}

这块代码append方法是加锁的,但是经过逃逸分析能够确定变量不会逃逸出线程,无法被其他线程访问,此时虽然append方法是加锁的,但是执行引擎执行时是不会加锁的。

关于逃逸分析的研究论文早在1999年就已经发表,但直到JDK 6,HotSpot才开始支持初步的逃逸 分析,而且到现在这项优化技术尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是逃逸分析 的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗。如果要百分之百准确地 判断一个对象是否会逃逸,需要进行一系列复杂的数据流敏感的过程间分析,才能确定程序各个分支
执行时对此对象的影响。

可以试想一下,如果逃逸分析完毕后发现几乎找不到几个不逃逸的对象, 那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成分析

曾经在很长的一段时 间里,即使是服务端编译器,也默认不开启逃逸分析,甚至在某些版本(如JDK 6 Update 18)中还曾经完全禁止了这项优化,一直到JDK 7时这项优化才成为服务端编译器默认开启的选项。如果有需要,或者确认对程序运行有益,用户也可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析, 开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可以使用参数-XX:+EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消除,使用参数-XX:+PrintEliminateAllocations查看标量的替换情况。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值