在这里插入图片描述
JVM架构图

一,说明

内存是非常重要的系统资源,是硬盘和CPU的桥梁。JVM内存布局规定了Java在运行中内存申请,分配,管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
JVM运行时数据区(Runtime Data Area)有五种类型:
方法区(Method Area)
堆(heap)
Java栈(Java stack)
本地方法栈(Native Method Stack)
程序计数器(Program Counter Register)
其中方法区(Method Area),堆(heap)是随着虚拟机启动而创建,随着虚拟机销毁而销毁,多线程之间共享(一个JVM实例中分别只有一份)。Java栈(Java stack),本地方法栈(Native Method Stack),程序计数器(Program Counter Register)三者是线程私有的,随着线程开始和结束而创建和销毁。

线程:线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。在Hotspot JVM里。每个线程都与操作系统的本地线程直接映射。
当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用Java线程中的run()方法。

二,程序计数器(PC寄存器)

JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。这里并非物理上的寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM的PC寄存器事堆物理的PC寄存器的一种抽象模拟。

2.1,作用

PC寄存器用来存储下一条指令的地址,也就是将要执行的指令代码。由执行引擎读取下一调指令。

2.2,特点

1,它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最块的存储区域。
2,在JVM规范中,每个线程都有他自己的程序计数器,是线程私有的,生命周期于线程的生命周期一致。
3,任何时间一个线程都只有一个方法正在执行,也就是所谓的当前方法。程序计数器会存储当先线程正在执行的Java方法的JVM指令地址;或者,如果是执行native方法,则是未指定值(undified)。
4,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能呢都需要依赖这个计数器来完成。
5,字节码解释器工作时就是通过改变这个计数器的值来选去下一调需要执行的字节码指令。
6,不会报错,不会垃圾回收。它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoyError情况的区域,也没有垃圾回收,只会更改其中的地址值。

问题:
PC寄存器为什么会被设定为线程私有的?
CPU会不停做任务切换,多线程情况下这必然会经常出现任务的中断和恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样各个线程之间可以进行独立计算,不会出现相互干扰的情况。

三,虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建是都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。虚拟机栈是线程私有的,其生命周期与所属线程一致。栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

3.1,作用

专管Java程序的运行,它保存方法的局部变量(八种基本数据类型,对象的引用),部分结果,并参与方法的调用和返回。

3.2,特点(优点)

1,栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
2,JVM直接对Java栈的操作只有两个:
每个方法执行,伴随着进栈(入栈,压栈)
执行结束后的出栈工作
3,对于栈来说不存在垃圾回收问题,但是可能会内存溢出。

3.3,栈可能出现的异常

Java虚拟机允许Java栈的大小是动态的或者是固定不变的。
1,如果采用固定大小的Java虚拟机栈,那每个Java虚拟机栈容量可以在线程创建的时候独立限定。如果线程请求分撇的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会跑出一个StackOverflowError 异常(默认情况)。
2,如果Java虚拟机栈可以动套扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存区创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutofMemoryError异常。
虚拟机栈的大小由-Xss参数决定。
在这里插入图片描述

3.4,栈中存储的内容

3.4.1,栈桢(Stack Frame)

每个线程都有自己的栈,栈中的数据都是以栈桢(Stack Frame)的格式存在的。
栈桢(Stack Frame)是一个内存块,是一个数据集,维系着方法执行过程中的各种数据信息。
在线程上正在执行的每个方法都各自对应一个栈桢(Stack Frame),栈桢与方法调用一一对应。在一个活动线程中,一个时间点上只会有一个活动的栈桢,即只有当前在执行的方法的栈桢(栈顶栈桢)是有效的,这个栈桢被称之为栈顶(Current Frame),与当前栈桢相对应的方法是当前方法(Current meyhod),定义这个方法的类就是当前类(Current Class)。执行引擎运行的所有字节码指令只针对当前栈桢进行操作。如果在该方法中调用了其它方法,对应的新的栈桢就会被创建出来,放在栈的顶端成为新的当前栈桢。
在这里插入图片描述

3.5, 栈运行原理

不同线程中所包含的栈桢是不允许存在相互引用的,即不可能在一个栈桢之中引用另外一个线程的栈桢。
如果当前方法调用了其它方法,方法返回直接,当前栈桢会穿回次方法的执行结果给前一个栈桢,接着虚拟机会丢弃当前栈桢,使得前一个栈桢重新成为当前栈桢。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令(返回值为void类型的也可以在方法结尾添加return);另一种是抛出异常(方法未处理)。不管适应哪种方式,都会导致栈桢被弹出。

3.6,栈桢中的内容

局部变量表(Local,Variables)
操作数栈(Operand stack)(或者表达式栈)
动态链接(Dynamic Linking)(或只想运行时常量池的方法引用)
方法返回地址(Return Address)(或方法正常或异常退出的定义)
一些附加信息
在这里插入图片描述

3.6.1,局部变量表

也称之为局部变量数组或者本读本地变量表。定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用(reference),以及returnAddress。由于局部变量表是建立在线程的栈上,是线程私有的,所以它没有数据安全问题。
局部变量表所需的容量大小是在编译器确定下来的,并保存在字节码中,在方法运行期间是不会改变其大小的。
Slot(变量槽):是局部变量表的基本存储单位。在局部变量表中32位以内的变量值占用一个Slot(8中基本数据类型,返回值类型以及引用类型),超过32位的(Long,Double)占用两个。
byte,short,插入在存储前变为int;
boolean也变为int,0为true,1为false;

JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可访问到局部变量表中指定的局部变量。当一个实例方法被调用的时候,他的方法参数和方法体内定义的局部变量将会按照顺序复制到局部变量表的每一个Slot。
如果需要访问局部变量中一个64bit的局部变量值时,只需要使用前一个索引即可(long,double);

如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index=0的slot处,其余的参数按照参数定义顺序继续排序。(如果栈帧中有this,那么它就应该在局部变量表中的首位)。

public void test() {
    int a = true;
    long b = 10L;
}

在这里插入图片描述
Slot槽位是可以重用的,如果一个局部变量过了其作用域(出了代码块{}),那么在其作用域之后申明的新的局部变量就很可能会复用过期局部变量的Slot,节省内存。
局部变量表中的变量也是重要的垃圾回收根结点,只要被局部变量表中直接或者间接应用的对象都不会被垃圾回收
局部变量与类变量赋值的区别
类变量分为静态变量和实例变量。
静态变量:在类字节码链接时会赋予默认值,在初始化阶段还可能会显示赋值(类静态代码块中可能有赋值语句);
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
局部变量:创建时必须赋值,否则编译不通过。

3.6.2,操作数栈(Operand stack)(或者表达式栈)

每一个独立的栈帧中除了包含局部变量表以外,好包含一个后进先出的操作数栈,也可以称之为表达式栈。在方法的执行过程中中,根据字节码指令,往栈中写入数据或者提取数据,即入栈/出栈。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们再把结果压入栈。比如:执行赋值,交换,求和等操作。我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈值得就是操作数栈。
如果被调用的方法带有返回值的话,器返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

作用
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期间就定义好了,保存在方法的Code属性中,位max_stack值。
栈中的任何一个元素都是可以任意的Java数据类型。32bit的类型占用一个栈深度,64bit占用俩。
操作数栈只能有出入栈操作,不能随机访问。

public int test() {
    int a = 10;
    int b = 10;
    int c = a + b;
    return c;
}

在这里插入图片描述
方法编译后的字节码命令中,入栈,取数都是在操作数栈上操作的。

3.6.3,动态链接(或指向运行时常量池的的方法引用)

每一个栈帧内部都包含一个指向运行时常量池中的该桢所属方法的引用。包含这个引用的目地就是为了支持当前方法的代码能够实现动态链接。
在这里插入图片描述

3.6.4,方法返回地址

存放调用该方法的PC寄存器的值
一个方法的结束,有两种方式:正常执行完毕或者出现未处理的异常。无论通过哪种方式退出,在方法退出后返回到该方法被调用的地方。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该放的指令的小一条指令的地址。二是通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
本质上方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者的操作数栈,设置PC寄存器值,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于异常情况下不会给上层调用者返回任何值。

3.6.5,附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。(不一定有,上面四个必须有)

四,本地方法栈

本地方法:一个native Method就是一个Java调用非Java代码的接口。该方法的实现由非Java语言实现,比如C。
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用,它也是线程私有的。

五,堆

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候就会创建,其空间大小也就确定了,是JVM管理的最大一块内存空间。
对可以出于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆。
所有的对象实例以及数组都应该在运行时分配在堆上,数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向其在堆中的位置(某些情况实际上有优化,栈上对象分配)。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆是GC(Garbage Collection,垃圾回收器)执行垃圾回收的重点区域

5.1,堆内存细分

Java 8及其之后堆内存逻辑上分为三部分:新生区+养老区+元空间。物理上元空间使用的内存是堆外内存。
三部分都有各种不同的叫法:
新生区:新生代,年轻代
养老区:老年区,老年代
元空间:永久区

5.2,堆空间大小的设置

-Xms:用于表示堆区的起始内存,等价于-XX:InitialHeapSize,默认值:物理内存/64
-Xmx:表示堆区的最大内存,等价于-XX:maxHeapSize,默认值:物理内存/4

一旦堆区中的内存大小超过 -Xmx所指定的内存时,将会抛出outOfMemoryError异常。
一般将 -Xms和 -Xmx俩参数配置相同的值,目的在于Java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提高性能。
配置新生代与老年代在堆空间结构的占比:
默认 -XX:NewRatio=2,表示新生代:老年代=1:2,新生代占整个堆空间的1/3.

5.3,年轻代

年轻代可细分为Eden,Survivor0空间和Survivor1空间(也叫from区和to区,不固定,谁空谁是to区)。
HotSpot中默认情况下Eden:Survivor0:Survivor1=8:1:1,可以通过**-XX:SurvivorRation** 参数调整比例。实际情况下内存分配比率有可能是6:1:1,原因是开启了自适应内存分配策略,可通过**-XX:-UswAdaptiveSizePolicy命令关闭自适应的内存分配策略。
几乎所有的Java对象都是在Eden区被new出来的(大对象可能直接存入老年代了);
绝大部分的Java对象的销毁都在新生代进行的。
可以使用
-Xmn** 设置新生代最大内存大小。

TLAB(Thread Local Allocation Buffer)
堆内是线程共享区域,任何线程都可以访问对内的共享数据,在并发环境下从堆中划分内存空间是线程不安全的,
使用同步机制会影响速度。TLAB是一种优化方案,在内存模型中Eden区域进一步划分,JVM为每一个线程
分配一个私有缓存区域。这样以来多线程同时分配内存是,可以避免一系列的非线程安全问题,提升内存分配效率。

该功能开关 -XX:UseTLAB ,默认开启。默认情况下TLAB空间非常小,仅占Eden空间的1%,可以通过参数调整该比率。JVM将TLAB作为内存分配的首选位置,一旦对象在TLAB中分配失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,在Eden空间中分配内存。
在这里插入图片描述

5.4,老年代

老年代中存储的是经过多次Minor GC(默认15次)后依然存活的对象。如果对象申请的内存很大,也可能直接放入老年代了。如果Survivor区空间不足了,其中的对象“年龄”没达到阈值(默认15次)也可能提前进入老年代。

5.5,对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
过程:
1,new的对象先放在Eden区,此区有大小限制。
2,当Eden的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收,将Eden区中的不在被其他对象所引用的对象进行销毁并加载新的对象到Eden区。
3,将Eden中对象移动到Survivor0区
4,如果再次触发垃圾回收,此时上次幸存下来的放到Survivor0的对象,如果没有回收,就会放到Survivor1.
5,如果再次经历垃圾回收,此时会重新放回Survivor0.
6,什么时候去养老区?默认时15次GC后,还存活的对象会放入老年代。如果Survivor区内存不够用了小于15次的对象也会进入老年代。
通过 -XX:MaxtenuringThreshold= 设置阈值。

在这里插入图片描述
总结
Survivor0,Survivor1 复制之后有交换,谁空谁是to。
垃圾回收频繁发生在新生代收集,很少在老年代收集,几乎不在永久区/元空间收集。
内存分配策略
优先分配到Eden;
大对象直接分配到老年代(老年代空间大);
长期存活的对象分配到老年代;
动态对象年龄判断。如果Survivor区中相同年龄的所有对象大小的综合大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

六,方法区

元空间,永久代,方法区的区别:
Jdk8开始把类的元数据放在本地堆内存中,这一块区域就叫做Metaspace,该区域在jdk7及以前是属于永久代的,元空间和永久代都是用来存储class相关信息,包括class对象的Method,Field等,元空间和永久代其实都是方法区的实现,只是实现有所不同,所以说方法区其实只是一种JVM的规范。

栈,堆,方法区之间的交互关系
在这里插入图片描述

6.1,方法区说明

作用
用于存储已被JVM加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。
特点
方法区在逻辑上是属于堆的一部分,但是一些简单的实现kennel不会选择区进行垃圾回收或者压缩。方法区看作是一块独立于Java堆的内存空间。
方法区和Java堆一样,是各个线程共享的内存空间。
方法区在JVM启动的时候就会被创建,并且它的实际的物理内存空间和Java堆区一样都可以是不连续的。
方法区的大小可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类。如果系统定了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:Java.lang.OutOfMemoryError:PermGen space 或者Java.lang.OutOfMemoryError:Metaspace。
关闭JVM就会释放这个区域的内存。

6.2,HotSpot中方法区的演进

在JDK7及之前 ,习惯上把方法区称之为永久代。从JDK8开始,使用元空间取代了永久代。本质上方法区和永久代并不等价
在这里插入图片描述

6.3 ,设置方法区内存的大小

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
Jdk7及以前
-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermSize 设定永久代最大可分配内存空间。32位默认是64M,64位默认是82M。超过该值会报错Java.lang.OutOfMemoryError:PermGen space。
Jdk8及以后
-XX:MetaspaceSize和-XX:MaxMetaspaceSize替换上面的参数,默认值依赖于环境。元空间溢出会报错Java.lang.OutOfMemoryError:Metaspace。
-XX:MetaspaceSize设置的元空间大小就是初始的高水位线,一旦触及到这个高水位线,Full GC将会触发并卸载没用的类(即这些类的类加载器不再存活),然后高水位线将会重置,新的高水位线值取决于释放了多少元空间。如果释放的空间不足,那么会在不超过MaxMetaspaceSize的前提下适当提升该值;如果释放空间过多,则适当减少该值。
如果初始化的高水位线设置过低,Full GC 可能会多次调用。

6.4 方法区内部结构

包含类型信息,常量,静态变量,即时编译器编译后的代码缓存, 运行时常量池。

6.4.1,类型信息:

对每个加载的类型(类 class,接口interface,枚举enum,注解annotation),JVM必须在方法区中存储一下类型信息:
1,类型的完整有效名称(全名=包名.类名)
2,类型直接父类的完整有效名(对于interface或事java.lang.Object,都没有父类)
3,类型的修饰符(public,abstract,final的某个子集)
4,类型直接接口的有序列表

6.4.2,域信息Field

JVM必须在方法区中保存类型的所有域的相关信息以及域的申明顺序。
域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,volatile,transient的某个子集)

6.4.3,方法信息(Method)

方法名称
方法的返回值类型
方法参数的数量和类型(按顺序)
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的子集)
方法的字节码,操作数栈,局部变量表及其大小(native,abstract除外)
方法的异常表(native,abstract除外),每个异常处理的开始位置,结束为止,代码处理在程序计数器中的便宜地址,被捕获的异常类的常 量池索引。

6.4.4,运行时常量池

常量池介绍:

 一个Java源文件中的类,接口,编译后产生一个字节码文件。而Java中的字节码文件需要数据支持,通常这种数据会很大以至于不能直接存在字节码里,换一种方式可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。常量池可以看作是一张表,虚拟机指令根据这张常量表找到要指向的类名,方法名,参数类型,字面量等类型。
 常量池中存储的数据类型:
 数量值
 字符串值
 类引用
 字段引用
 方法引用

常量池是Class文件的一部分,用于存放编译器生成的各种字面量(如文本字符串,被申明为final的常量值等)与符号引用(类名,字段方法的描述信息等),这部分内容将在类加载之后存放到方法区的运行时常量池中。
运行时常量池,在类和接口加载到虚拟机后,就会创建对应的运行时常量池。JVM为每一个已加载的类型(类或者接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。

6.5,方法区内存回收

一般来说这个区域的回收效果比较难令人满意,尤其是类型卸载,条件相当苛刻,

方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型(class,interface)。回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收。
如何判断类型是否可以回收?
1,类的所有实例都被回收,也就是堆中没有任何该类及其派生类的对象。
2,加载类的类加载器已经被回收。
3,该类对应的java.lang.Class 对象没有任何地方被引用,也就是无法在任何地方通过反射访问该类的方法。

七,垃圾回收

在这里插入图片描述

JVM在对堆内存进行垃圾回收时并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。 针对HotSpot VM的实现,它里面的GC按照回收区域可分为两大种类型:一种是部分垃圾回收(Partital GC),一种是整堆收集(Full GC)。

7.1,新生代收集(Minor GC/Young GC)

只是新生代(Eden,Survivor0,Survivor1)的垃圾收集。
当Eden区空间不足时,就会触发Minor GC,Survivor满不会触发(Minor GC触发后会往空的Survivor移动对象,当前为from的Survivor也会垃圾回收和移动对象,顺带处理了Survivor区)。大部分Java对象都具备朝生夕灭的特性。所以Minor GC非常频繁,一般回收速度也比较快。
Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才会恢复。

7.2,老年代收集(Major GC/Old GC)

只是老年代的垃圾收集。 指发生在老年代的GC,对象从老年代消失时,我们会说Major GC或者Full GC发生了,他的速度一般比MinorGC慢10倍以上。
出现了Major GC时经常伴随着至少一次的Minor GC,也就是说在老年代空间空间不足时,会先尝试触发Minor GC,如果空间还不足则触发Major GC。
如果Major GC后内存依然不足,就报错OOM了。

7.3,混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前只有G1GC会有这种

7. 4, 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

触发的情况:
1,调用System.gc()时,系统建议执行Full GC。但是不必然执行。
2,老年代空间不足。
3,方法区空间不足。
4,通过Minor GC后进入老年代的平均大小大雨老年代的可用空间。
5,由Eden区,survivor(from)区向survivor(to)区复制时,对象大小大于survivor(to)可用内存,则吧对象转存到老年代,且老年代的可用内存小于该对象大小。

八,代码优化方案之栈上分配

8.1,逃逸分析

正常情况下对象都是在堆上分配的,如果一个对象在子程序中被分配(方法内局部变量),并且指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
启用该功能需要开启逃逸分析(1.8默认开启)
-XX:+DoEscapeAnalysis

public class StackAllocation {

    public static void main(String[] args) {
        long start=System.currentTimeMillis();
        for(int i=0;i<10000000;i++){
            alloc();
        }
        long end=System.currentTimeMillis();
        System.out.println("共耗时:"+ (end-start)+"毫秒");
        try {
            Thread.sleep(100*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void alloc(){
        new User(1,"zhangsan");
    }

}

class User{
   int id;
   String name;

   public User(int id,String name){
       this.id=id;
       this.name=name;
   }
}

以上代码创建10000000个对象,但是每个对象的引用都没有逃逸出方法之外,符合栈上分配的前提。
在这里插入图片描述
在这里插入图片描述
开启逃逸分析执行结果如上,耗时10毫秒,没出现垃圾回收,Eden区使用30%,Survivo区使用0%(没有Minor GC,不移动对象至该区域)。

在这里插入图片描述

在这里插入图片描述
关闭逃逸分析执行结果如上,耗时 77毫秒,出现多次垃圾回收(Minor GC),Eden区使用64%,Survivor from 区9%。

8.2,同步省略

如果一个对象发现只能从一个线程被访问,那么对于这个对象的操作可以不考虑同步。
线程同步的代价是相当高的,同步的后果是降低并发性和性能。在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样会大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

8.3,分离对象或标量替换

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在栈中。
该功能控制参数:-XX:+EliminateAllocations 默认开启,开启后允许将合适的对象打散分配在栈上。
标量:是指一个无法在分解成更小的数据的数据。Java中的原始数据类型就是标量。
聚合量:相对的那些还可以分解的数据叫做聚合量。Java中的对象就是聚合量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就可以把这个对象拆解为若涵哥其他包含的若干个成员变量来替代。这个过程就是标量替代,替代后可以栈上分配。

总结:堆空间不是对象分配的唯一选择,永远不会逃逸的对象也可能分配在栈空间中。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐