jvm
1、jvm概念
1.1、JVM JRE JDK区别
JVM:就是一个虚拟的用于执行bytecode字节码的”虚拟计算机”。一般与OS操作系统打交道
JRE:java运行环境,包含Java虚拟机、库函数、运行Java应用程序所必须的文件
javaw(windos java启动器,不显示黑窗口),libraries(外部类库),rt_jar(核心库)
JDK:java开发工具,包含JRE,以及增加编译器和调试器等用于程序开发的文件
javac(编译器),debugging(调试器),tools,javap(反编译工具)
2、内存结构
注意:常量池,运行时常量池,字符串常量池逻辑上属于方法区,只是存储的物理位置在堆(所以他们属于方法区)
2.1、程序计数器
当前线程所执行的字节码的行号指示器。在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
特点:
(1).为了在线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,独立存储,互不影响。所以,程序计数器是线程私有的内存区域
(2).如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是一个Native方法,计数器的值为空
(3).Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域
2.2、虚拟机栈
2.2.1、定义
每个方法执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈是线程私有的,它的生命周期与线程相同
局部变量表:存放方法参数和方法内部定义的局部变量
操作数栈:操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区
动态链接:将这些符号引用转换为调用方法的直接引用
2.2.2、问题辨析
- 垃圾回收是否涉及栈内存?
- 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
- 栈内存的分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
2.2.3、线程运行诊断(CPU占用过高)
- 查询占用cpu过高程序pid(top)
- 查询该进程下各线程的CPU占用情况 ( ps H -eo pid,tid,%cpu|grep 27598)
- 找到cpu占用最高的线程id,转为16进制
- 打印堆栈 (jstack pid)
jstack 27598 | grep -A 100 6bdf
2.3、本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码),而本地方法栈为虚拟机使用到的Native方法服务(一个Native Method就是一个java调用非java代码的接口)
2.4、堆
2.4.1、定义
Java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(特例:栈上分配策略)
从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老生代。新生代又可分为Eden和Suvivor区
(1)新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
通过参数 –XX:NewRatio 来指定
(2)Eden : from : to = 8 : 1 : 1
以通过参数 –XX:SurvivorRatio 来设定
(3)最大物理内存大小不超过192兆字节(MB)时默认最大堆大小是物理内存的一半,否则占用物理内存的四分之一
2.4.2、堆内存诊断
jps:查看java进程
jmap:查看堆内存占用情况 jmap -heap 进程id
jconsole:图形界面,检测堆栈,CPU占用,类加载情况等(可以连续监测)
jvisualvm:和jconsole功能类似,但功能更强。可以抓取和导入堆dump快照
2.4.3、内存泄漏
内存溢出:是程序在申请内存时,没有足够的内存空间供其使用
内存泄漏:内存空间使用完毕之后未回收
内存泄露的原因?
(1)static字段引起的内存泄露
大量使用static字段会潜在的导致内存泄露,在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期。
解决办法:最大限度的减少静态变量的使用;单例模式时,依赖于延迟加载对象而不是立即加载方式
(2)未关闭的资源导致内存泄露
每当创建连接或者打开流时,JVM都会为这些资源分配内存。如果没有关闭连接,会导致持续占有内存。在任意情况下,资源留下的开放连接都会消耗内存,如果我们不处理,就会降低性能,甚至OOM。
解决办法:使用finally块关闭资源;关闭资源的代码,不应该有异常;jdk1.7后,可以使用try-with-resource块
2.5、方法区
2.5.1、结构
方法区(线程共享)
方法区是java虚拟机的一个模型规范,具体实现是永久代和元空间。方法区存储了每个类的信息(包括类的名称、方法信息、字段信息)、常量以及编译器编译后的代码等。(GC分代收集扩展至方法区,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题。元空间占用本地内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间)
jdk1.7前:采用永久代
jdk1.7:字符串常量池被移到堆内存(1.7前在方法区)
jdk8:废除了永久代。类的元信息会被放入本地内存(元空间)。将类的静态变量和字符串常量池放入到java堆
2.5.2、class常量池
class常量池:每个class文件都包含有一个class常量池,包含符号引用和字面量。符号引用就是类的全限定名和字段名称,描述符。字面量就是具体的值,包含数字型和字符型
2.5.3、运行时常量池
运行时常量池:运行时常量池是方法区的一部分。运行时常量池是当 Class 文件被加载到内存后,Java虚拟机会将 Class 文件常量池里的内容转移到运行时常量池里,并且把里面的符号地址变为真实地址,数值型字面量存放在运行时常量池,字符型字面量存放到字符串常量池**
2.5.4、字符串常量池
JVM为了提高性能,减少内存开销,维护的一个存放字符串常量的内存区域,里面的字符串不允许重复,有长度限制,最大为65535字节
- 两种创建字符串对象不同方式的比较
(1)采用字面值的方式创建字符串对象
1 | public class Str { |
采用字面值的方式创建一个字符串时,JVM 首先会去字符串池中查找是否存在 “aaa” 这个对象,如果不存在,则在字符串池中创建 “aaa” 这个对象,然后将池中 “aaa” 这个对象的引用地址返回给字符串常量 str,这样 str 会指向池中”aaa”这个字符串对象;如果存在,则不创建任何对象,直接将池中 “aaa” 这个对象的地址返回,赋给字符串常量。
对于上述的例子:这是因为,创建字符串对象 str2 时,字符串池中已经存在 “aaa” 这个对象,直接把对象 “aaa” 的引用地址返回给 str2,这样 str2 指向了池中 “aaa” 这个对象,也就是说 str1 和 str2 指向了同一个对象,因此语句 System.out.println(str1== str2) 输出:true。
(2)采用 new 关键字新建一个字符串对象
1 | public class Str { |
采用 new 关键字新建一个字符串对象时,JVM 首先在字符串常量池中查找有没有 “aaa” 这个字符串对象,如果有,则不在池中再去创建 “aaa” 这个对象了,直接在堆中创建一个 “aaa” 字符串对象,然后将堆中的这个”aaa”对象的地址返回赋给引用 str1,这样,str1 就指向了堆中创建的这个 “aaa” 字符串对象;如果没有,则首先在字符串常量池池中创建一个 “aaa” 字符串对象,然后再在堆中创建一个 “aaa” 字符串对象,然后将堆中这个 “aaa” 字符串对象的地址返回赋给 str1 引用,这样,str1 指向了堆中创建的这个 “aaa” 字符串对象。
对于上述的例子:因为,采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str1和str2指向的是两个不同的对象,因此语句System.out.println(str1 == str2)输出:false
- 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
- 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建
- 字符串拼接问题
(1)字符串变量拼接
1 | public class StringTableStudy { |
通过变量拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中
(2)字符串拼接
1 | public class StringTableStudy { |
使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接引用串池中的地址,所以进行的操作和 ab = “ab” 一致。
- intern方法
jdk1.8调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中,如果有则不会放入
jdk1.6调用字符串对象的intern方法,会将该字符串对象复制一份放入到串池中,如果有则不会放入
1 | public class Main { |
- 思考
(1)String str = new String(“abc”);创建了几个对象,常量池有abc字段是1个,常量池没有”abc”字段则是2个。
(2)String str=“abc”;创建了几个对象(如果常量池里面已经有对象了就是0个。如果没有就是1个);
(3)new String(“abc”).intern();创建了几个对象(如果常量池里面已经有该字符串对象了就是1个,如果没有就是两个)
(4)string a = “a”+”b“创建几个对象?0个或1个。因为编译器优化,不会创建”a”或”b”.
3、垃圾回收算法
3.1、如何判断一个对象可以被回收
- 引用计数法
每个对象关联一个引用计数器属性,任何一个对象引用了A,引用计数器的值加1.当引用失效时,引用计数器就减1.当引用计数器的值为0时,表示对象不再被使用,可进行回收
缺点:(1)需要单独的字段存储计数器,这样增加了存储空间的开销 (2)每次赋值都要更新计数器值,增加了时间开销 (3)存在循环引用的问题(所以jvm不用)
- 可达性分析法
设立若干根对象(GC Root Object),当任何一个根对象到某一个对象均不可达时,认为这个对象可以被回收
哪些对象可以被作为根对象?
- 虚拟机栈(Java Stack)中的局部变量和参数(它们是线程私有的,因此可以作为根对象)
- 方法区中静态变量常量(生命周期与应用程序的生命周期相同)
为什么这些对象可以作为根对象?
GC Root 需要确保引用所指的对象都是活着的,而当前线程栈帧中的对象和方法区中的对象,在这一时刻是存活的。
3.2、五种引用
- 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它
在java程序中,一般由Object object = new Object();定义的object就是一个强引用
如上图B、C对象都不引用A1对象时,A1对象才会被回收
- 软引用:当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象
如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收
作用:软引用是用来描述一些有用但并不是必需的对象,JVM 内存空间充足的时候将数据缓存在内存中,如果空间不足了就将其回收掉
1 |
|
- 弱引用:有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象
如上图如果B对象不再引用A3对象,则A3对象会被回收
1 | eakReference<String> sr = new WeakReference<String>(new String("hello")); |
- 虚引用: 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。你声明虚引用的时候是要传入一个queue的。当你的虚引用所引用的对象已经执行完finalize函数的时候,就会把对象加到queue里面。你可以通过判断queue里面是不是有对象来判断你的对象是不是要被回收了
如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存
1 | PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc, referenceQueue); |
- 引用强度
强引用>软引用>弱引用>虚引用
3.3、垃圾回收算法
3.3.1、标记清除算法
- 概念:通过根节点,标记所有根节点开始的可达对象,清除未被标记对象
- 优点:算法简单
- 缺点:(1)产生内存碎片,造成新来的大对象(如数组)可能没有有效的内存空间
3.3.2、标记整理算法(老年代默认)
- 概念:将标记的对象移动到内存的一端,清除边界外的所有空间
- 优点:解决了标记清除算法的碎片问题
- 缺点:效率低(整理后依赖这个对象的对象更新一下引用地址信息)
3.3.3、复制算法(新生代默认)
- 概念:将内存分为一块较大的Eden和两块较小的survivor,每次使用Eden和其中一块survivo,gc时将Eden存活对象复制到suvivorTo,suvivorFrom存活的对象没有达到分代年龄阈值时复制到suvivorTo,达到分带年龄阈值复制到老年区,之后清除Eden和suvivorFrom的对象,交换两个suvivor的角色,后面以此类推
- 优点:(1)不会产生内存碎片(2)在存活对象不多的情况下,效率较高,适合新生代
- 缺点:浪费内存空间,始终要有一个空闲的survivor
3.4、FULL GC原因
Full GC为一次特殊GC行为的描述,这次GC会回收整个堆的内存,包含老年代,新生代,元空间等。是说在这次GC的全过程中所有用户线程都是处于暂停的状态(stop the world)
- System.gc()方法的调用
在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
- 老年代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space
3.5、内存溢出和内存泄漏
内存溢出:是程序在申请内存时,没有足够的内存空间供其使用
内存泄漏:内存空间使用完毕之后未回收
内存泄漏的原因?
(1)static字段引起的内存泄露
大量使用static字段会潜在的导致内存泄露,在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期。
解决办法:最大限度的减少静态变量的使用;单例模式时,依赖于延迟加载对象而不是立即加载方式
(2)未关闭的资源导致内存泄露
每当创建连接或者打开流时,JVM都会为这些资源分配内存。如果没有关闭连接,会导致持续占有内存。在任意情况下,资源留下的开放连接都会消耗内存,如果我们不处理,就会降低性能,甚至OOM。
解决办法:使用finally块关闭资源;关闭资源的代码,不应该有异常;jdk1.7后,可以使用try-with-resource块
3.6、jvm对象何时会进入老年代
- 达到晋升年龄:新生代对象在经历每次GC的时候,如果没有被回收,则对象的年龄+1。当年龄超过阈值的时候,便会进入老年代。默认情况下,阈值为15(为什么15?对象头年龄为4bit)
- 大对象直接进入老年代。比如 -XX:PretenureSizeThreshold =1024,那么就表示超过1kb大小的对象在垃圾回收时直接进入到老年代(大对象:很长的字符串或数组)
4、垃圾回收器
4.1、新生代垃圾收集器
4.1.1、Serial(串行)收集器
- 概念:Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法应用
- 收集过程
4.1.2、ParNew 收集器
- 概念:可以把这个收集器理解为Serial收集器的多线程版本
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
4.1.3、Parallel Scavenge 收集器
- 概念:Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量 。
- 可设置参数:-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间, -XX:GC Time Ratio直接设置吞吐量的大小
吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)
比如虚拟机总共运行了120秒,垃圾收集时间用了1秒,吞吐量=(120-1)/120=99.167%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
4.2、老年代收集器
4.2.1、serial old
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用”标记-整理算法”,运行过程和Serial收集器一样。
4.2.2、Parallel old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理算法”进行垃圾回收,吞吐量优先。
回收算法:标记-整理
适用场景:为了替代serial old与Parallel Scanvenge配合使用。
4.2.3、CMS
特点最短回收停顿时间,
回收算法标记-清除
回收步骤:
(1)初始标记:标记GC Roots直接关联的对象,速度快
(2)并发标记:GC Roots Tracing过程,耗时长,与用户进程并发工作
(3)重新标记:修正并发标记期间用户进程运行而产生变化的标记,好事比初始标记长,但是远远小于并发标记
(4)并发清除:清除标记的对象缺点:
对CPU资源非常敏感,CPU少于4个时,CMS岁用户程序的影响可能变得很大,由此虚拟机提供了“增量式并发收集器”;无法回收浮动垃圾;采用标记清除算法会产生内存碎片,不过可以通过参数开启内存碎片的合并整理。
4.3、整堆收集器
4.3.1、G1
- 基本概念
G1将整个JVM堆划分成多个大小相等的独立区域regin,跟踪各个regin里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收最大的regin,当然还保留有新生代和老年代的概念,但新生代和老年代不在是物理隔离了,他们都是一部分regin集合。内存“化整为零”的思路:在GC根节点的枚举范围汇总加入remembered set 即可保证不对全堆扫面也不会遗漏。
- 回收步骤:
(1)初始标记:标记GC Roots直接关联的对象
(2)并发标记:对堆中对象进行可达性分析,找出存活对象,耗时长,与用户进程并发工作
(3)重新标记:修正并发标记期间用户进程继续运行而产生变化的标记
(4)筛选回收:对各个regin的回收价值进行排序,然后根据期望的GC停顿时间制定回收计划
4.3.2、ZGC
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。
在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。
优点:低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小
缺点:浮动垃圾
目前使用的非常少,真正普及还是需要写时间的。
4.4、垃圾收集器的选择?
jdk1.8 前默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
- 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益
1 | 参数:-XX:+UseSerialGC。 |
- 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的
1 | 参数:-XX:+UseParallelGC。 |
- 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择G1、ZGC、CMS都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些
1 | 参数: |
5、类加载机制
虚拟机把类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的类加载机制
5.1、类加载过程
5.1.1、加载阶段
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
5.1.2、链接阶段
(1)验证: 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等
(验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施)
(2)准备:为类的静态变量分配内存,并将其赋默认值
为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)
(3)解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
符号引用:就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符(eg:java.lang.String)。
直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针
5.1.3、初始化阶段
为类的静态变量赋初值
赋初值两种方式:
(1)定义静态变量时指定初始值。如 private static String x=”123”;
(2)在静态代码块里为静态变量赋值。如 static{ x=”123”; }
注意:只有对类的主动使用才会导致类的初始化。
初始化顺序:
初始化顺序:静态成员 - 父类构造器 - 非静态成员 - 子类构造器
5.2、类加载器分类
- 启动类加载器
使用c++实现,加载jre和jre/lib目录下的核心库
- 扩展类加载器
java编写,父加载器为启动类加载器,从jre/lib/ext下加载类库
- 应用类加载器
负责加载用户类路径(classpath)上的指定类库
5.3、双亲委派机制
概念:
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类
好处:
(1)避免重复加载,通过委托去向上面问一问,加载过了,就不用再加载一遍
(2)保证核心api定义的类型不会被随意篡改,比如自己定义一个java.lang.String,顶级加载器系统类加载器加载时会加载核心包下的String类而不是自定义的。保证了核心类的安全
6、逃逸分析
逃逸分析(Escape Analysis)是编译器优化的一种技术,用于确定对象的生命周期和作用域,以便更好地进行优化。
6.1、逃逸方式
方法逃逸(Method Escape):对象在方法中创建后被返回,逃逸到方法的调用者中
1 | public class EscapeExample { |
线程逃逸(Thread Escape):对象在一个线程中创建后,被其他线程所引用
1 | public class EscapeExample { |
6.2、逃逸结果
(1)栈上分配
逃逸分析的主要目的是减少内存分配的开销和垃圾回收的压力。当对象被分配在栈上时,它的分配和回收都非常高效,因为它们可以通过简单的指针移动来完成。而在堆上分配的对象则需要进行动态内存分配和垃圾回收,这会产生额外的开销(栈上分配)
(2)标量替换
Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
1 | // 标量替换举例 |