Java对象逃逸

关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业变现、人工智能等,希望大家多多支持。
未经允许不得转载

在这里插入图片描述

一、导读

我们继续总结学习Java基础知识,温故知新。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

二、概览

Java对象逃逸指的是一个对象在其应该被限制访问的范围之外被引用或访问的情况,简单解释就是我有一个方法,在方法内创建了一个对象,但是这个对象传递到其他地方了。
在Java中,对象一般在包含它们的方法中创建和使用,当方法结束时,这些对象会被回收。然而,当对象在方法中被引用或传递到其他方法中时,就会发生对象逃逸。

我们举例

    这种写法直接返回的是对象,用处就是被别的变量所引用,会造成对象逃逸,从而增加了GC的压力。
    public StringBuilder getSb(){
    
    
        StringBuilder sb = new StringBuilder("");
        return sb;
    }
    
    不如改成下面这样
    public String getSb1(){
    
    
        StringBuilder sb = new StringBuilder("");
        return sb.toString();
    }

第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。

三、相关知识

在这之前,我们要先了解一些jvm的基本知识。

Java运行时数据区(Runtime Data Area)是指在Java程序执行期间,Java虚拟机所管理的诸多内存区域(分别用于存储不同的数据),如上图所示,包含了以下几个部分:

  • 堆区 (主要用于存储对象实例、数组)
  • 栈区 (主要存放java方法、局部变量、操作数栈、动态链接、方法出口、基本类型的变量数据、对象的引用等)
  • 方法区 (主要用于存储类型信息、常量、静态变量、即时编译代码等)
  • 程序计数器

一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,
第一段是把.java文件转换成.class文件。
第二段是把.class转换成机器指令的过程,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译,后来为了解决效率问题,引入了 JIT(即时编译) 技术。

JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。通过逃逸分析,Java Hotspot编译器能够
分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    
    
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

sb是一个方法内部变量,上述代码中直接将sb返回,这样这个sb 有可能被其他方法所改变,这样它的作用域就不只是在方法内部

3.1 逃逸分析

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

  • -XX:+DoEscapeAnalysis : 开启逃逸分析. 从jdk 1.7开始已经默认开始逃逸分析
  • -XX:-DoEscapeAnalysis : 关闭逃逸分析 。
  • -XX:+PrintEscapeAnalysis : 显示分析结果

使用逃逸分析,编译器可以对代码做如下优化:

  • 同步省略 (锁消除)
public void f() {
    
    
    Object hollis = new Object();
    synchronized(hollis) {
    
    
        System.out.println(hollis);
    }
}

优化后变成 
public void f() {
    
    
    Object hollis = new Object();
    System.out.println(hollis);
}
  • 分离对象或标量替换
  • 将堆分配转化为栈分配
    当对象没有发生逃逸时,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配,该对象就可以通过标量替换分解成成员标量分配在栈内存中,
    和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能

3.2 对象逃逸状态

1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

  • 对象是一个静态变量
  • 对象是一个已经发生逃逸的对象
  • 对象作为当前方法的返回值

2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

对象逃逸可能会导致以下问题:

  1. 安全问题:对象逃逸可以使对象暴露给不受信任的代码,可能导致数据泄露或被篡改。
  2. 性能问题:对象逃逸可能导致对象的生命周期变得不可预测,增加垃圾回收的负担,降低系统性能。
  3. 并发问题:对象逃逸可能导致多个线程同时访问同一个对象,造成线程安全问题。

为了解决对象逃逸问题,可以采取以下措施:

  1. 限制对象的访问范围:将对象的作用域限制在方法内部,避免在方法外部引用或传递对象。
  2. 使用局部变量代替成员变量:将对象定义为方法内的局部变量而不是成员变量,这样可以避免对象在方法外部被引用。
  3. 使用不可变对象:如果对象是不可变的,那么即使发生逃逸,也不会出现安全和并发问题。
  4. 使用线程安全的数据结构:如果对象需要在多个线程之间共享,应该使用线程安全的数据结构或采用同步控制机制来保证并发安全性。

通过避免对象逃逸,可以提高代码的安全性、性能和并发性能。

3.3 Java中的对象都是在堆中分配吗?说明为什么!

对象和数组并不一定都在堆上分配内存的,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,
那么有可能堆内存分配会被优化成栈内存分配,
JIT编译器就可以在编译期间根据逃逸分析的结果,来决定是否可以将对象的内存分配从堆转化为栈。

举个栗子:

先定义一个类 XYZ

class XYZ {
    
    
    int i;
}

在定义一个方法 abc(),方法内使用了XYZ类,但是并没有外部引用,也就说这个对象不会发生逃逸。

public void abc() {
    
    
    XYZ xyz = new XYZ();
}

最后再定义一个for循环来调用abc()方法,假设我们在代码中创建100万个XYZ对象,
 for (int i = 0; i < 1000000; i++) {
    
    
     abc();
 }


假设我们先关闭逃逸分析,在代码结束前使用[jmap][1]命令,来查看下当前堆内存中有100万个XYZ对象.
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

接下来,我们开启逃逸分析,再来执行下以上代码,使用jmap命令,来查看下当前堆内存中有几万个XYZ对象,不是一个量级。
堆内存中分配的对象数量大量减少,
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

四、 推荐阅读

Java 专栏

SQL 专栏

数据结构与算法

Android学习专栏

未经允许不得转载

ddd

猜你喜欢

转载自blog.csdn.net/fumeidonga/article/details/134063712
今日推荐