大学生创业做网站创办网站
- 作者: 五速梦信息网
- 时间: 2026年03月21日 11:29
当前位置: 首页 > news >正文
大学生创业做网站,创办网站,free免费空间,嘉定区整站seo十大排名概叙 Java 中的对象是否都分配在堆内存中#xff1f; 好了太抽象了#xff0c;那具体一点#xff0c;看看下面这个对象是在哪里分配内存#xff1f; public void test() { Object object new Object(); }这个方法中的object对象#xff0c;是在堆中分配内存么#xff1…概叙 Java 中的对象是否都分配在堆内存中 好了太抽象了那具体一点看看下面这个对象是在哪里分配内存 public void test() { Object object new Object(); }这个方法中的object对象是在堆中分配内存么 说结果object可能在栈上分配内存也可能在堆上分配内存。 重点来了在JVM的实现中为了提高JVM的性能和节省内存空间JVM提供了一种叫做 “逃逸分析” 的特性逃逸分析是目前Java虚拟机中比较前沿的优化技术也是JIT中一个很重要的优化技术。 jdk6才开始引入该技术jdk7开始默认开启逃逸分析jdk8开始完善逃逸分析并默认开启直到 JDK 9 逃逸分析将作为默认的优化手段不再需要特别的编译参数。 现在理解“object可能在栈上分配内存也可能在堆上分配内存” 这句话jdk7之前这里的object肯定是在堆上分配内存jdk7、8则有可能在栈上分配因为jdk7才开始支持逃逸分析jdk9则是大概率是在栈上分配这里的 object对象很小因为jdk9才真正支持和默认开启逃逸分析。 随着JIT编译器(即时编译器)的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致所有的对象都会分配到堆上变的不那么绝对了. 在Java虚拟机中,对象是在堆中分配内存的,但是有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配.当方法执行完毕,栈帧弹出,对象就被释放了.这样就无需在堆上分配内存和经历垃圾回收了(Hotspot目前并未这样做).这也是最常见的堆外存储技术. 在JDK 6u23(可记大版本JDK7)后,Hotspot中默认就已经开启了逃逸分析.如果使用较早版本可以通过选项-XX:DoEscapeAnalysis显示开启逃逸分析.通过-XX:PrintEscapeAnalysis查看逃逸分析的筛选结果. Hotspot通过逃逸分析实现了标量替换(未逃逸的对象被替换为标量和聚合量,能提升代码效率,),但是未逃逸的对象仍然会在堆上分配内存,所以仍然可以说所有的对象都是在堆上分配内存. 此外,基于Open JDK深度定制的TaoBao VM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的对象从heap中移致heap外,并且GC不管理GCIH内部的Java对象,以此达到降低GC回收频率和提升GC回收效率的目的. 栈每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态连接、方法出口等信息。每个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 堆当实例化对象时会把对象分配到堆中然后把指向该堆的引用压入栈中。 逃逸当一个对象的指针被多个方法或线程引用时我们称这个指针发生了逃逸一般情况返回对象、对全局变量的肤质一般都会发生逃逸。 逃逸分析用来分析这种逃逸现象的方法称为逃逸分析 逃逸分析优化-栈上分配栈上分配的意思是方法内局部变量未发生逃逸生成的实例在栈上分配不用在堆中分配分配完成后继续在调用栈内执行最后线程结束栈空间被回收局部变量对象也被回收。 java对象内存分配过程 如果开启栈上分配逃逸分析JVM会先进行栈上分配如果没有开启栈上分配或不符合条件则会进入TLAB分配如果TLAB分配不成功或者不符合则判断是否可以进入年老代分配如果不能进入年老代则进入eden分配并不是所有对象都分配在堆上除了堆意外还可以将对象分配到栈和TLAB中。(大多数的对象都分配在堆中) Java中的对象一定是在堆上分配的吗 答不一定。 如果满足了逃逸分析的条件一个对象完全可以在栈上分配。减少堆内存的分配和GC压力。由于栈内存有限所以 如果对象符合标量替换的条件进一步为对象来一次化整为零的手术。标量替换具体的做法是JVM会将对象进一步打散将对象分解为若干个被这个方法使用的成员变量从而达到更好的利用栈内存和寄存器的目标。 如何将堆上的对象分配到栈需要使用逃逸分析手段。 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析的基本行为就是分析对象动态作用域 当一个对象在方法中被定义后对象只在方法内部使用则认为没有发生逃逸。当一个对象在方法中被定义后它被外部方法所引用则认为发生逃逸。例如作为调用参数传递到其他地方中。 在计算机语言编译器优化原理中逃逸分析是指分析指针动态范围的方法它同编译器优化原理的指针分析和外形分析相关联。当变量或者对象在方法中分配后其指针有可能被返回或者被全局引用这样就会被其他方法或者线程所引用这种现象称作指针或者引用的逃逸(Escape)。通俗点讲如果一个对象的指针被多个方法或者线程引用时那么我们就称这个对象的指针或对象逃逸Escape了因为此时对象逃出了方法或线程的局部作用域。 什么是逃逸分析 简要说明“逃逸分析一种确定指针动态范围的静态分析它可以分析在程序的哪些地方可以访问到指针。 在JVM的即时编译语境下逃逸分析将判断新建的对象是否逃逸。 即时编译判断对象是否逃逸的依据一种是对象是否被存入堆中静态字段或者堆中对象的实例字段另一种就是对象是否被传入未知代码。 逃逸分析Escape Analysis是目前Java虚拟机中比较前沿的优化技术它与类型继承关系分析一样并不是直接优化代码的手段而是为其他优化手段提供依据的分析技术。 逃逸分析Escape Analysis是一个很重要的JIT优化技术用于判断对象是否会在方法外部被访问到也就是逃出方法的作用域。逃逸分析是JIT编译器的一个步骤通过JIT我们能够确定哪些对象可以被限制在方法内部使用不会逃逸到外部然后可以对它们进行优化比如把它们分配在栈上而不是堆上或者进行标量替换把一个对象拆散成多个基本类型来存储。是一种可以有效减少Java程序中同步负载和内存堆分配和垃圾回收压力的跨函数全局数据流分析算法。通过逃逸分析Java Hotspot编译器能够分析出一个新对象的引用的使用范围而决定是否要将这个对象分配到堆上。 逃逸分析主要针对局部变量判断堆上分配的对象是否逃逸出方法的作用域。它同编译器优化原理的指针分析和外形分析相关联。当变量或者对象在方法中分配后其指针有可能被返回或者被全局引用这样就会被其他方法或者线程所引用这种现象称作指针或者引用的逃逸Escape。通俗点讲如果一个对象的指针被多个方法或者线程引用时那么我们就称这个对象的指针发生了逃逸。合理地设计代码结构和数据的使用方式能够更好地利用逃逸分析来优化程序的性能。我们还可以通过逃逸分析减少堆上分配对象的开销提高内存利用率。 逃逸分析是一种用于确定对象在方法的生命周期内是否逃逸出方法外部范围的技术。在Java开发中逃逸分析用于确定对象的生命周期和作用域以便进行相应的优化提高程序的性能和内存利用效率。 当一个对象被创建后它可以在方法内部使用也可以被传递给其他方法或线程并在方法外部继续存在。如果对象没有逃逸出方法的作用域那么JVM可以将其分配在栈上而不是堆上从而避免了堆内存的分配和垃圾回收的开销。 关于逃逸分析的论文在1999年就已经发表但直到Sun JDK 1.6才实现了逃逸分析而且直到现在这项优化尚未足够成熟仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。如果要完全准确的判断一个对象是否会逃逸需要进行数据流敏感的一系列复杂分析从而确定程序各个分支执行时对此对象的影响。这是一个相对高耗时的过程如果分析完后发现没有几个不逃逸的对象那这些运行期耗用的时间就白白浪费了所以目前虚拟机只能采用不那么准确但时间压力相对较小的算法来完成逃逸分析。还有一点是基于逃逸分析的一些优化手段如上面提到的“栈上分配”由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂因此在HotSpot中暂时还没有做这项优化。在测试结果中实施逃逸分析后的程序在MicroBenchmarks中往往能运行出不错的成绩但是在实际的应用程序尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能即使编译的收益有所下降所以在很长的一段时间里即使是Server Compiler也默认不开启逃逸分析在JDK 1.6 Update 23的Server Compiler中才开始默认开启了逃逸分析甚至在某些版本如JDK 1.6 Update 18中还曾经短暂的完全禁止了这项优化。如果有需要并且确认对程序运行有益用户可以使用参数-XX:DoEscapeAnalysis来手动开启逃逸分析开启之后可以通过参数-XX:PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后用户可以使用参数-XX:EliminateAllocations来开启标量替换使用XX:EliminateLocks来开启同步消除使用参数-XX:PrintEliminateAllocations查看标量的替换情况。尽管目前逃逸分析的技术仍不是十分成熟但是他却是即时编译器优化技术的一个重要的方向在今后的虚拟机中逃逸分析技术肯定会支撑起一系列使用有效的优化技术。逃逸分析的基本原理 JVM逃逸分析的基本原理是通过静态和动态两种分析方法来确定对象的逃逸情况。 在Java的编译体系中一个Java的源代码文件变成计算机可执行的机器指令的过程中需要经过两段编译 第一段编译指前端编译器把.java文件转换成 .class文件(字节码文件)。前端编译器产品可以是JDK的Javac、Eclipse JDT中的增量式编译器。 第二编译阶段JVM 通过解释字节码将其翻译成对应的机器指令逐条读入字节码逐条解释翻译成机器码。 很显然由于有一个解释的中间过程其执行速度必然会比可执行的二进制字节码程序慢很多。这就是传统的JVM的解释器Interpreter的功能。 如何去掉中间商提升效率 为了解决这种效率问题引入了JIT即时编译器Just In Time Compiler技术。 引入了 JIT 技术后Java程序还是通过解释器进行解释执行也就是说主体还是解释执行只是局部去掉中间环节。 JIT Compiler(Just-in-timeCompiler) 即时编译。最早的Java建置方案是由一套转译程式interpreter将每个Java指令都转译成对等的微处理器指令并根据转译后的指令先后次序依序执行由于一个Java指令可能被转译成十几或数十几个对等的微处理器指令这种模式执行的速度相当缓慢。 怎么做局部去掉中间环节呢 当JVM发现某个方法或代码块运行特别频繁的时候就会认为这是“热点代码”Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码并进行优化然后再把翻译后的机器码缓存起来以备下次使用。 把翻译后的机器码缓存在哪里呢 这个 缓存叫做 Code Cache。 可见JVM和WEB应用实现高并发的手段是类似的还是使用了缓存架构。 当JVM下次遇到相同的热点代码时跳过解释的中间环节直接从 Code Cache加载机器码直接执行无需 再编译。 所以JIT总的目标是发现热点代码 热点代码变成了提升性能的关键hotspot JVM的名字也就是这么来的把识别热点代码写在名字上作为毕生的追求。 所以JVM总的策略为 对于占据大部分的不常用的代码我们无需耗费时间将其编译成机器码而是采取解释执行的方式运行 另一方面对于仅占据小部分的热点代码我们则可以将其编译成机器码以达到理想的运行速度。 JIT(即时编译)的出现与 解释器的区别 1解释器是将字节码解释为机器码下次即使遇到相同的字节码仍会执行重复的解释。 2JIT 是将一些字节码编译为机器码并存入 Code Cache下次遇到相同的代码直接执行无需 再编译。 3解释器是将字节码解释为针对所有平台都通用的机器码。 4JIT 会根据平台类型生成平台特定的机器码。 JVM包含多个即时编译器主要有C1和C2还有个Graal (实验性的)。 多个即时编译器, 都会对字节码进行优化并生成机器码 C1会对字节码进行简单可靠的优化包括方法内联、去虚拟化、冗余消除等编译速度较快可以通过-client强制指定C1编译C2会对字节码进行激进优化包括分支频率预测、同步擦除等可以通过-server强制指定C2编译 JVM 将执行状态分成了 5 个层次 0 层解释执行Interpreter 1 层使用 C1 即时编译器编译执行不带 profiling 2 层使用 C1 即时编译器编译执行带基本的 profiling 3 层使用 C1 即时编译器编译执行带完全的 profiling 4 层使用 C2 即时编译器编译执行 JVM不会直接启用C2而是先通过C1编译收集程序的运行状态再根据分析结果判断是否启用C2。 分层编译模式下, 虚拟机执行状态由简到繁、由快到慢分为5层 在编译期间JIT 除了对 热点代码做缓存提速会对代码做很多优化。 其中有一部分优化的目的就是减少内存堆分配压力其中JIT优化中一种重要的技术叫做逃逸分析。根据逃逸分析即时编译器会在编译过程中对代码做如下优化 锁消除当一个锁对象只被一个线程加锁时即时编译器会把锁去掉栈上分配当一个对象没有逃逸时会将对象直接分配在栈上随着线程回收由于JVM的大量代码都是堆分配所以目前JVM不支持栈上分配而是采用标量替换标量替换当一个对象没有逃逸时会将当前对象打散成若干局部变量并分配在虚拟机栈的局部变量表中
- 静态分析是在编译时进行的分析 它通过对代码的静态结构进行检查确定对象是否可能逃逸。例如当一个对象被赋值给类的成员变量或返回给外部方法时可以确定该对象逃逸。
- 动态分析是在运行时进行的分析 它通过观察方法调用和对象引用的行为来确定对象是否逃逸。例如当一个对象被多个线程引用时可以判断该对象逃逸。 逃逸分析会对代码进行深度分析以确定对象在方法的生命周期内是否逃逸出方法外部范围。如果对象没有逃逸JVM可以将其分配在栈上而不是堆上。 逃逸状态全局逃逸、参数逃逸、没有逃逸 一个对象有三种逃逸状态全局逃逸、参数逃逸、没有逃逸。 全局逃逸GlobalEscape即一个对象的作用范围逃出了当前方法或者当前线程 一般有以下几种场景 ① 对象是一个静态变量 ② 对象是一个已经发生逃逸的对象 ③ 对象作为当前方法的返回值 参数逃逸ArgEscape即一个对象被作为方法参数传递或者被参数引用但在调用过程中不会发生全局逃逸这个状态是通过被调方法的字节码确定的。 没有逃逸即方法中的对象没有发生逃逸。 逃逸状态示例代码如下 public class EscapeAnalysisTest {public static Object globalVariableObject;public Object instanceObject;public void globalVariableEscape(){globalVariableObject new Object(); // 静态变量外部线程可见发生逃逸}public void instanceObjectEscape(){instanceObject new Object(); // 赋值给堆中实例字段外部线程可见发生逃逸}public Object returnObjectEscape(){return new Object(); // 返回实例外部线程可见发生逃逸}public void noEscape(){Object noEscape new Object(); // 仅创建线程可见对象无逃逸}} 逃逸的方式方法逃逸和线程逃逸 1.方法逃逸在一个方法体内定义一个局部变量而它可能被外部方法引用比如作为调用参数传递给方法或作为对象直接返回。或者可以理解成对象跳出了方法。 方法逃逸包括 通过调用参数将对象地址传递到其他方法中对象通过return语句将对象指针返回给其他方法等等 我们可以用下面的代码来表示这个现象。//StringBuffer对象发生了方法逃逸 public static StringBuffer createStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb; } 上面的例子中StringBuffer 对象通过return语句返回。StringBuffer sb是一个方法内部变量上述代码中直接将sb返回这样这个StringBuffer有可能被其他方法所改变这样它的作用域就不只是在方法内部虽然它是一个局部变量称其逃逸到了方法外部。甚至还有可能被外部线程访问到譬如赋值给类变量或可以在其他线程中访问的实例变量称为线程逃逸。不直接返回 StringBuffer那么StringBuffer将不会逃逸出方法。具体的代码如下// 非方法逃逸 public static String createString(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); } 可以看出想要逃逸方法的话需要让对象本身被外部调用或者说 对象的指针传递到了 方法之外。 如何快速的判断是否发生了逃逸分析大家就看new的对象实体是否在方法外被调用。 public class EscapeAnalysis {public EscapeAnalysis obj;/*** 方法返回EscapeAnalysis对象发生逃逸* return/public EscapeAnalysis getInstance() {return obj null ? new EscapeAnalysis():obj;}/** 为成员属性赋值发生逃逸/public void setObj() {this.obj new EscapeAnalysis();}/** 对象的作用于仅在当前方法中有效没有发生逃逸/public void useEscapeAnalysis() {EscapeAnalysis e new EscapeAnalysis();}/** 引用成员变量的值发生逃逸*/public void useEscapeAnalysis2() {EscapeAnalysis e getInstance();} } 2.线程逃逸这个对象被其他线程访问到比如赋值给了实例变量并被其他线程访问到了。对象逃出了当前线程。 逃逸分析的优化策略 逃逸分析可以为Java程序带来以下优化策略栈上分配同步消除、标量替换、方法内联 逃逸分析相关参数 -XX:DoEscapeAnalysis 开启逃逸分析 -XX:PrintEscapeAnalysis 开启逃逸分析后可通过此参数查看分析结果。 -XX:EliminateAllocations 开启标量替换 -XX:EliminateLocks 开启同步消除 -XX:PrintEliminateAllocations 开启标量替换后查看标量替换情况。 1. 栈上分配Stack Allocation 逃逸分析可以确定哪些对象不会逃逸出方法的作用域将这些对象分配在栈上而不是堆上。栈上分配的对象在方法调用生命周期内创建和销毁无需进行垃圾回收从而提高了程序的执行效率。 一般情况下不会逃逸的对象所占空间比较大如果能使用栈上的空间那么大量的对象将随方法的结束而销毁减轻了GC压力 栈上分配思路 栈上分配是JVM提供的一项优化技术。 其思路是 对于线程私有的对象(不能被其它线程访问的对象)可以将它们分配到栈内存上而不是堆内存中也就是将聚变量进行标量替换的方案。 分配到栈上的优势是可以在方法结束后自动销毁不需要GC介入提供系统性能 对于大量零散的对象栈上分配提供了一种很好的对象分配策略栈上分配速度块可以有效避免GC回收带来的负面影响。 问题由于栈内存比较小因而大对象不能也不适合进行栈上分配。 开启栈上分配 栈上分配是基于逃逸分析和标量替换的所以必须开启逃逸分析和标量替换当然JDK1.8是默认都是开启的。 开启逃逸分析-XX:DoEscapeAnalysis 关闭逃逸分析-XX:-DoEscapeAnalysis 显示分析结果-XX:PrintEscapeAnalysis开启标量替换-XX:EliminateAllocations 关闭标量替换-XX:-EliminateAllocations 显示标量替换详情-XX:PrintEliminateAllocations栈上分配示例 示例1 import java.lang.management.ManagementFactory; import java.util.List; /**
- 逃逸分析优化-栈上分配 * 栈上分配,意思是方法内局部变量未发生逃逸生成的实例在栈上分配不用在堆中分配分配完成后继续在调用栈内执行最后线程结束栈空间被回收局部变量对象也被回收。 * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。 虚拟机参数设置如下表示做了逃逸分析 消耗时间在10毫秒以下 -server -Xmx10M -Xms10M-XX:DoEscapeAnalysis -XX:PrintGC*虚拟机参数设置如下表示没有做逃逸分析 消耗时间在1000毫秒以上 -server -Xmx10m -Xms10m-XX: -DoEscapeAnalysis -XX:PrintGC* author 734621 * */ public class OnStack{public static void alloc(){byte[] bnew byte[2];b[0]1;}
public static void main(String [] args){long bSystem.currentTimeMillis();for(int i0;i100000000;i){alloc();}long eSystem.currentTimeMillis();System.out.println(消耗时间为: (e - b)); ListString paramters ManagementFactory.getRuntimeMXBean().getInputArguments(); for(String p : paramters){ System.out.println(p); }
} }加逃逸分析的结果 [GC (Allocation Failure) 2816K-484K(9984K), 0.0013117 secs] 消耗时间为:7 -Xmx10m -Xms10m -XX:DoEscapeAnalysis -XX:PrintGC没有加逃逸分析的结果如下 [GC (Allocation Failure) 3320K-504K(9984K), 0.0003174 secs] [GC (Allocation Failure) 3320K-504K(9984K), 0.0002524 secs] 消耗时间为:1150 -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:PrintGC以上测试可以看出栈上分配可以明显提高效率 效率是不开启的1150/7 160倍示例2 我们通过举例来说明 开启逃逸分析 和 未开启逃逸分析时候的情况class User {private String name;private String age;private String gender;private String phone; } public class StackAllocation {public static void main(String[] args) throws InterruptedException {long start System.currentTimeMillis();for (int i 0; i 100000000; i) {alloc();}long end System.currentTimeMillis();System.out.println(花费的时间为 (end - start) ms);// 为了方便查看堆内存中对象个数线程sleepThread.sleep(10000000);}private static void alloc() {// 未发生逃逸User user new User(); } } 设置JVM参数表示未开启逃逸分析 -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:PrintGCDetails 花费的时间为664 ms 然后查看内存的情况发现有大量的User存储在堆中开启逃逸分析 -Xmx1G -Xms1G -XX:DoEscapeAnalysis -XX:PrintGCDetails 然后查看运行时间我们能够发现花费的时间快速减少同时不会发生GC操作 花费的时间为5 ms 在看内存情况我们发现只有很少的User对象说明User未发生逃逸因为它存储在栈中随着栈的销毁而消失。 通过对比我们可以看到 开启栈上分配将未逃逸的对象分配在栈内存中明显运行效率更高。关闭栈上分配后GC频繁进行垃圾回收。
- 同步消除Lock Elimination 逃逸分析可以检测到某些对象只被单个线程访问并且不会逃逸到其他线程。因此可以消除不必要的同步操作减少了多线程程序的执行开销。 同步锁时非常消耗性能的所以编译器确定一个对象没有发生逃逸时它会移除该对象的同步锁。JDK1.8 默认开启了同步锁但是建立在开启逃逸分析的基础上。 -XX:EliminateLocks #开启同步锁消除(JVM默认状态) -XX:-EliminateLocks #关闭同步锁消除 通过示例 明显可以看到“逃逸分析和锁消除” 对性能的提升public void testLock(){long t1 System.currentTimeMillis();for (int i 0; i 100_000_000; i) {locketMethod();}long t2 System.currentTimeMillis();System.out.println(耗时:(t2-t1));}public static void locketMethod(){EscapeAnalysis escapeAnalysis new EscapeAnalysis();synchronized(escapeAnalysis) {escapeAnalysis.obj2abcdefg;}}设置JVM参数开启逃逸分析 耗时 java -Xmx64m -Xms64m -XX:DoEscapeAnalysis设置JVM参数关闭逃逸分析 耗时 java -Xmx64m -Xms64m -XX:-DoEscapeAnalysis设置JVM参数关闭锁消除再次运行 java -Xmx64m -Xms15m -XX:DoEscapeAnalysis -XX:-EliminateLocks设置JVM参数开启锁消除再次运行 java -Xmx64m -Xms15m -XX:DoEscapeAnalysis -XX:EliminateLocks 线程同步的代价是相当高的同步的后果是降低并发性和性能。 在动态编译同步块的时候JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略也叫锁消除。 例如下面的代码public void f() {Object hellis new Object();synchronized(hellis) {System.out.println(hellis);} } 代码中对hellis这个对象加锁但是hellis对象的生命周期只在f()方法中并不会被其他线程所访问到所以在JIT编译阶段就会被优化掉优化成public void f() {Object hellis new Object();System.out.println(hellis); } 我们将其转换成字节码此处发现还是有同步锁的身影是因为优化是在编译阶段的在加载进内存后发生。 3. 标量替换Scalar Replacement 逃逸分析可以将一个对象拆分成多个标量如基本类型或其他对象并将它们分配在不同的位置。这样可以减少内存碎片和对象访问的开销提高内存利用效率。 首先要明白标量和聚合量基础类型和对象的引用可以理解为标量它们不能被进一步分解。而能被进一步分解的量就是聚合量比如对象。 对象是聚合量它又可以被进一步分解成标量将其成员变量分解为分散的变量这就叫做标量替换。 这样如果一个对象没有发生逃逸那压根就不用创建它只会在栈或者寄存器上创建它用到的成员标量节省了内存空间也提升了应用程序性能。 标量替换在 JDK1.8 中也是默认开启的但是同样也要建立在已开启逃逸分析的基础之上。 标量scalar是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。 相对的那些还可以分解的数据叫做聚合量AggregateJava中的对象就是聚合量因为他可以分解成其他聚合量和标量。 public static void main(String args[]) {alloc(); } class Point {private int x;private int y; } private static void alloc() {Point point new Point(1,2);System.out.println(point.x point.x ;point.y point.y); } 以上代码经过标量替换后就会变成private static void alloc() {int x 1;int y 2;System.out.println(point.x x ; point.y y); } 在JIT阶段如果经过逃逸分析发现一个对象不会被外界访问的话那么经过JIT优化就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。 可以看到Point这个聚合量经过逃逸分析后发现他并没有逃逸就被替换成两个标量了。那么标量替换有什么好处呢就是可以大大减少堆内存的占用。因为一旦不需要创建对象了那么就不再需要分配堆内存了。 标量替换为栈上分配提供了很好的基础。 逃逸分析测试 逃逸分析测试 代码如下大致思路就是 for 循环 1 亿次循环体内调用外部的 allot() 方法而 allot() 方法的作用就是简单创建一个对象但是这个对象是内部的所以是未逃逸的所以理论上 JVM 是会进行优化的我们拭目以待。并且我们会对比开启和关闭逃逸分析之后各自程序的运行时间/*** ClassName: EscapeAnalysisTest* Description: http://www.jetchen.cn 逃逸分析 demo* Author: Jet.Chen* Date: 2020/11/23 14:26* Version: 1.0/ public class EscapeAnalysisTest {public static void main(String[] args) {long t1 System.currentTimeMillis();for (int i 0; i 100000000; i) {allot();}long t2 System.currentTimeMillis();System.out.println(t2-t1);}private static void allot() {Jet jet new Jet();}static class Jet {public String name;}} 上面就是我们进行逃逸分析测试的代码 mian() 方法末尾有一个线程暂停目的是为了观察此时 JVM 中的内存情况。Step 1测试开启逃逸 由于环境是 jdk1.8默认开启了逃逸分析所以直接运行得到结果如下程序耗时 3 毫秒此时线程是处于睡眠状态的我们观察下内存情况发现堆内存中一共新建了 11 万个 Jet 对象。Step 2测试关闭逃逸 我们关闭逃逸分析再来运行一次使用 java -XX:-DoEscapeAnalysis EscapeAnalysisTest 来运行代码即可得到结果如下程序耗时 400 毫秒此时我们观察下内存情况发现堆内存中一共新建了 3 千多万个 Jet 对象。所以无论是从代码的执行时间3 毫秒 VS 400 毫秒还是从堆内存中对象的数量11 万个 VS 3 千万个来分析在上述场景下开启逃逸分析是有正向益的。Step 3测试标量替换 我们测试下开启和关闭 标量替换如下图由上图我们可以看出在上述极端场景下开启和关闭标量替换对于性能的影响也是满巨大的另外同时也验证了标量替换功能生效的前提是逃逸分析已经开启否则没有意义。Step 4测试锁消除 测试锁消除我们需要简单调整下代码即给 allot() 方法中的内容加锁处理如下private static void allot() {Jet jet new Jet();synchronized (jet) {jet.name jet Chen;} } 然后我们运行测试代码测试结果也很明显在上述场景下开启和关闭锁消除对程序性能的影响也是巨大的。/* 进行两种测试* 关闭逃逸分析同时调大堆空间避免堆内GC的发生如果有GC信息将会被打印出来* VM运行参数-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:PrintGCDetails -XX:HeapDumpOnOutOfMemoryError** 开启逃逸分析 jdk8默认开启* VM运行参数-Xmx4G -Xms4G -XX:DoEscapeAnalysis -XX:PrintGCDetails -XX:HeapDumpOnOutOfMemoryError** 执行main方法后* jps 查看进程* jmap -histo 进程ID/ Slf4j public class EscapeTest {public static void main(String[] args) {long start System.currentTimeMillis();for (int i 0; i 500000; i) {alloc();}long end System.currentTimeMillis();log.info(执行时间 (end - start) ms);try {Thread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e1) {e1.printStackTrace();}}/* JIT编译时会对代码进行逃逸分析* 并不是所有对象存放在堆区有的一部分存在线程栈空间* Ponit没有逃逸*/private static String alloc() {Point point new Point();return point.toString();}/*同步省略锁消除 JIT编译阶段优化JIT经过逃逸分析之后发现无线程安全问题就会做锁消除*/public void append(String str1, String str2) {StringBuffer stringBuffer new StringBuffer();stringBuffer.append(str1).append(str2);}/* 标量替换**/private static void test2() {Point point new Point(1,2);System.out.println(point.xpoint.getX(); point.ypoint.getY());// int x1; // int y2; // System.out.println(point.xx; point.yy);}}Data AllArgsConstructor NoArgsConstructor class Point{private int x;private int y; } 4. 方法内联Method Inlining 逃逸分析可以确定某些方法调用不会逃逸出当前方法的作用域。因此可以对这些方法进行内联优化减少方法调用的开销提高程序的执行效率。 通过这些优化策略逃逸分析可以帮助JVM更好地优化代码减少垃圾回收的开销提高程序的执行效率和响应性并减少内存的占用。 实际应用场景 逃逸分析在实际的Java应用中具有广泛的应用场景,以下是一些常见的应用场景 对象作为方法参数传递时逃逸分析可以确定对象是否逃逸从而决定对象是在堆上还是栈上分配 对象作为方法返回值时逃逸分析可以确定对象是否逃逸从而决定对象是在堆上还是栈上分配 对象被线程共享时逃逸分析可以确定对象是否逃逸从而决定是否需要进行同步操作 循环中的临时对象创建时逃逸分析可以确定对象是否逃逸从而决定对象是否需要频繁创建和销毁。 逃逸分析的不足 关于逃逸分析的论文在1999年就已经发表了但直到JDK1.6才有实现而且这项技术到如今也并不是十分成熟。 其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的这其实也是一个相对耗时的过程。 一个极端的例子就是经过逃逸分析之后发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。 虽然这项技术并不十分成熟但是它也是即时编译器优化技术中一个十分重要的手段。注意到有一些观点认为通过逃逸分析JVM会在栈上分配那些不会逃逸的对象这在理论上是可行的但是取决于JvM设计者的选择。据我所知oracle Hotspot JVM中并未这么做这一点在逃逸分析相关的文档里已经说明所以可以明确所有的对象实例都是创建在堆上。 目前很多书籍还是基于JDK7以前的版本JDK已经发生了很大变化intern字符串的缓存和静态变量曾经都被分配在永久代上而永久代已经被元数据区取代。但是intern字符串缓存和静态变量并不是被转移到元数据区而是直接在堆上分配所以这一点同样符合前面一点的结论对象实例都是分配在堆上。上面的示例之所以加快了是因为标量替换。 逃逸分析的好处 如果一个对象不会在方法体内或线程内发生逃逸或者说是通过逃逸分析后认定其未能发生逃逸就可以做如下优化 栈上分配 一般情况下不会逃逸的对象所占空间比较大如果能使用栈上的空间那么大量的对象将随方法的结束而销毁减轻了GC压力 同步消除 如果你定义的类的方法上有同步锁但在运行时却只有一个线程在访问此时逃逸分析后的机器码会去掉同步锁运行。 标量替换 Java虚拟机中的原始数据类型intlong等数值类型以及reference类型等都不能再进一步分解它们可以称为标量。相对的如果一个数据可以继续分解那它称为聚合量Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问并且这个对象是可分解的那程序真正执行的时候将可能不创建这个对象而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化属性被扁平化后可以不用再通过引用指针来建立关系可以连续紧凑的存储对各种存储都更友好执行期间能省去大量由于数据搬运造成性能损耗。同时还可以各自分别在栈帧或寄存器上分配空间原本的对象就无需整体分配空间了。 小结 年轻代是对象的诞生、成长、消亡的区域一个对象在这里产生、应用最后被垃圾回收器收集、结束生命。 老年代放置长生命周期的对象通常都是从survivor区域筛选拷贝过来的Java对象。当然也有特殊情况我们知道普通的对象会被分配在TLAB上如果对象较大JVM会试图直接分配在Eden其他位置上如果对象太大完全无法在新生代找到足够长的连续空闲空间JVM就会直接分配到老年代。当GC只发生在年轻代中回收年轻代对象的行为被称为MinorGc。 当GC发生在老年代时则被称为MajorGc或者FullGC。一般的MinorGc的发生频率要比MajorGC高很多即老年代中垃圾回收发生的频率将大大低于年轻代。 JVM逃逸分析通过静态和动态两种分析方法确定对象是否可能逃逸出方法的范围。它可以帮助JVM优化代码提高Java程序的性能和内存利用效率。 逃逸分析的优化策略包括栈上分配、同步消除、标量替换和方法内联。这些优化策略可以减少垃圾回收的开销提高程序的执行效率和响应性并减少内存的占用。 参考 https://zhuanlan.zhihu.com/p/693382698 JVM-堆-逃逸分析-08-CSDN博客 JIT内存逃逸分析_java关闭标量替换-CSDN博客 查看JVM所有配置的参数值 java -XX:PrintFlagsFinal #输出打印所有参数jvm参数
- 上一篇: 大学生asp网站开发的实训周电脑培训机构哪里有
- 下一篇: 大学生创业做网站的筹资方式网站服务器类型
相关文章
-
大学生asp网站开发的实训周电脑培训机构哪里有
大学生asp网站开发的实训周电脑培训机构哪里有
- 技术栈
- 2026年03月21日
-
大学里读网站建设工业和信息化部网站备案管理系统
大学里读网站建设工业和信息化部网站备案管理系统
- 技术栈
- 2026年03月21日
-
大学计算机网页设计作业关键词seo优化
大学计算机网页设计作业关键词seo优化
- 技术栈
- 2026年03月21日
-
大学生创业做网站的筹资方式网站服务器类型
大学生创业做网站的筹资方式网站服务器类型
- 技术栈
- 2026年03月21日
-
大学生互助联盟网站建设需求分析说明表企业网站做留言板有什么优势
大学生互助联盟网站建设需求分析说明表企业网站做留言板有什么优势
- 技术栈
- 2026年03月21日
-
大学生旅游网站策划书seo网站营销公司哪家好
大学生旅游网站策划书seo网站营销公司哪家好
- 技术栈
- 2026年03月21日






