别让Java对象逃逸(Object Escape)
翻译:吴嘉俊,叩丁狼高级讲师
关于逃逸分析
我在开源项目Speedment的开发过程中,我和项目的贡献者都意识到我们的代码不仅要良好并易懂,同时还要有较高的性能,否则他们很容易转向使用其他的解决方案。
逃逸分析(Escape Analysis)允许我们在写出性能较好的代码的同时,能通过恰当的抽象,保证良好的代码风格。
逃逸分析(简写为“EA”)允许java编译器在多种情况下优化我们的代码。请考虑一下代码:
public class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { final StringBuilder sb = new StringBuilder() .append("(") .append(x) .append(", ") .append(y) .append(")"); return sb.toString(); } }
每一次我们调用Point::toString方法的时候,都会创建一个新的StringBuilder对象。这个创建出来的对象,对于外面的方法或者运行当前代码的其他线程都是不可见的(因为其他线程只能看到自己的StringBuilder版本)。
所以,当我们大量的调用了这个方法之后,会出现大量的StringBuilder对象么?不会。因为逃逸分析的作用,编译器会可以在栈上为StringBuilder分配空间。所以,当我们的方法返回的时候,对象会自动的被删除,栈上的指针会自动的回退到这个方法调用之前的值。
逃逸分析在Java中已经存在了很久了。在最开始的时候,我们需要在命令行选项中手动开启逃逸分析(通过-XX:+DoEscapeAnalysis开启),现在它已经作为默认开启的选项了。Java8在以前的Java基础上,对于逃逸分析,又有了新的增强。
逃逸分析如何工作
基于逃逸分析,一个对象会可能会被用三种逃逸状态标记:
- 全局级别逃逸:一个对象可能从一个方法或者当前线程中逃逸。再明确一点,如果一个对象被作为一个方法的返回值,那么对象被标记为全局逃逸状态。如果一个对象作为类静态字段(static field)或者类字段(field),同样会被标记为全局逃逸状态。另外,如果我们复写了一个方法的finalize()方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中,这也符合情理,因为这些对象需要对于JVM的finalizer必须是可见的(所以发生逃逸了)。当然,还有其他的一些情况也会让对象标记为全局逃逸状态。
- 参数级别逃逸:如果一个对象被作为参数传递给一个方法,但是在这个方法之外无法访问或者对其他线程不可见,这个对象标记为参数级别逃逸。
- 无逃逸状态:一个对象不会产生逃逸
标记为全局级别逃逸或者参数级别逃逸的对象必须在堆中分配空间,但是参数级别逃逸是可能在内存中去掉对象同步锁的,因为上面已经解释,参数级别逃逸对象不会被其他线程访问。
无逃逸状态的对象的内存分配会更加自由,可能会在栈上分配,也可能会在堆上分配。事实上,在某些情况下,甚至根本不会去创建一个对象,而直接使用该对象的标量值代替,比如仅仅在栈上创建一个int,去代替一个Integer对象。因为只有一个线程可以访问该对象,所以对象上的同步锁自然会被去掉。例如,我们使用无逃逸状态的StringBuffer(较之StringBuilder,StringBuffer是线程安全的,所有方法都是synchronized),那么这种情况下,所有方法的同步锁都会被去掉,提高执行效率。
EA目前只在C2 HotSpot编译器下有用,所以请确保我们运行在-server模式下。
为什么它很重要
理论上来说,无逃逸状态对象可以直接在栈上分配内存,甚至直接在CPU寄存器中分配空间,以提供非常快速的执行。
当我们在堆上分配空间的时候,因为对象可能会被分配在不连续的彼此远离的堆地址上,这种地址分配方式会非常快的耗尽我们L1 CPU缓存,性能会受到影响。而当使用EA通过栈来分配空间,在大部分情况下,使用的都是L1缓存中已经分配的空间。所以,EA在栈上分配空间,会在数据存储位置分配上面更加优化。这对于性能是有益的。
当我们使用EA在栈上分配空间,垃圾回收器的工作量会极大减低,这可能是EA带来的一个最大的性能提升点。因为每一次在垃圾回收执行的时候,会对堆进行一次完整的扫描,这对于我们的CPU性能,和CPU缓存的消耗是非常大的。更不用说,如果服务器部分虚拟内存在独立的存储设备上,过于频繁的GC带来的绝对是灾难性的影响。
EA带来的最重要的提升不仅仅体现在性能上。EA允许我们使用本地抽象(local abstractions),比如Lambdas, Functions, Streams, Iterators等等。依赖于EA,我们能轻松的写出既易读,性能又高的代码,让代码专注于我们在做的事情。
一个例子
public class Main { public static void main(String[] args) throws IOException { Point p = new Point(100, 200); sum(p); System.gc(); System.out.println("Press any key to continue"); System.in.read(); long sum = sum(p); System.out.println(sum); System.out.println("Press any key to continue2"); System.in.read(); sum = sum(p); System.out.println(sum); System.out.println("Press any key to exit"); System.in.read(); } private static long sum(Point p) { long sumLen = 0; for (int i = 0; i < 1_000_000; i++) { sumLen += p.toString().length(); } return sumLen; } }
上面的代码中,创建了一个Point实例,并且通过调用sum方法,大量的调用Point对象的toString()方法。我们分了三个阶段,首先,执行一次sum,然后立刻GC掉所有创建的对象,接下来两次我们又调用两次sum方法,但没有从堆中删除任何东西,我们来验证一下每一步执行之后堆的状态。
我们使用如下的参数来执行应用,我们就可以看到在JVM中发生了什么:
-server
-XX:BCEATraceLevel=3
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
-verbose:gc
-XX:MaxInlineSize=256
-XX:FreqInlineSize=1024
-XX:MaxBCEAEstimateSize=1024
-XX:MaxInlineLevel=22
-XX:CompileThreshold=10
-Xmx4g
-Xms4g
大量的运行参数来更清楚的让我们看到到底发生了什么。
当第一步调用完成,我们来看看堆的使用(在System.gc()执行之后)
pemi$ jps | grep Main
50903 Main
pemi$ jps | grep Main
50903 Main
pemi$ jmap -histo 50903 | headnum #instances #bytes class name----------------------------------------------1: 95 42952184 [I2: 1079 101120 [C3: 485 55272 java.lang.Class4: 526 25936 [Ljava.lang.Object;5: 13 25664 [B6: 1057 25368 java.lang.String7: 74 5328 java.lang.reflect.Field
后面两次调用完成之后:
pemi$ jmap -histo 50903 | headnum #instances #bytes class name
----------------------------------------------1: 2001080 88101152 [C2: 100 36777992 [I3: 1001058 24025392 java.lang.String4: 64513 1548312 java.lang.StringBuilder5: 485 55272 java.lang.Class6: 526 25936 [Ljava.lang.Object;7: 13 25664 [Bpemi$ jmap -histo 50903 | headnum #instances #bytes class name
----------------------------------------------1: 4001081 176101184 [C2: 2001059 48025416 java.lang.String3: 105 32152064 [I4: 64513 1548312 java.lang.StringBuilder5: 485 55272 java.lang.Class6: 526 25936 [Ljava.lang.Object;7: 13 25664 [B
可以看到,EA最终能删掉在堆上创建的StringBuilder实例。两个操作对比,一个只需要63K空间,另一个需要2M。确实是一个很大的进步。
原文地址:https://minborgsjavapot.blogspot.com/2015/12/do-not-let-your-java-objects-escape.html
别让Java对象逃逸(Object Escape)相关推荐
- JVM - 剖析Java对象头Object Header之指针压缩
文章目录 Pre 指针压缩 论证压缩效果 UseCompressedOops & UseCompressedClassPointers [指针压缩]开启 VS 关闭 指针压缩的目的 为什么堆内 ...
- Java虚拟机-逃逸分析(Escape Analysis)和栈上分配
我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存.如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗, ...
- java 对象逃逸 解决_Java中的逃逸问题心得
大家一般认为new出来的对象都是被分配在堆上,但这并不是完全正确,通过对Java对象分配过程分析,我们发现对象除了可以被分配在堆上,还可以在栈或TLAB中分配空间.而栈上分配对象的技术基础是逃逸分析和 ...
- JVM - 剖析Java对象头Object Header之对象大小
文章目录 Pre 总览 对象头剖析 查看对象内存的占用情况 对象头C++源码 注释 Pre JVM - 写了这么多年代码,你知不道new对象背后的逻辑? 中大体介绍了Java中 new 对象背后的主要 ...
- Java 并发编程解析 | 如何正确理解Java对象创建过程,我们主要需要注意些什么问题?
苍穹之边,浩瀚之挚,眰恦之美: 悟心悟性,善始善终,惟善惟道! -- 朝槿<朝槿兮年说> 写在开头 从接触 Java 开发到现在,大家对 Java 最直观的印象是什么呢?是它宣传的 &qu ...
- tlab java_Java中的逃逸分析和TLAB以及Java对象分配
我们在学习使用Java的过程中,一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中new出来的对象并一定分别在所 ...
- java的头怎么写_JAVA对象布局之对象头(Object Header)
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能 .在学习并发编程知识synchronized时,我们总是难以理解其实 ...
- JAVA对象布局之对象头(Object Header)
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能 .在学习并发编程知识synchronized时,我们总是难以理解其实 ...
- JOL(java object layout --java 对象内存布局)
JOL(java object layout --java 对象内存布局) ⚠⚠⚠本文以java普通对象为切入点,分析java的对象内存布局,数组见文末 maven地址
- JOL(java object layout): java 对象内存布局
我们天天都在使用java来new对象,但估计很少有人知道new出来的对象到底长的什么样子?对于普通的java程序员来说,可能从来没有考虑过java中对象的问题,不懂这些也可以写好代码.今天,给大家介绍 ...
最新文章
- 网站服务器部署注意事项,服务器部署改云部署注意事项
- npm 卸载_GitHub 收购 npm:天下开源是一家,有个爸爸叫微软
- wince2秒快速启动TOC分析
- vue学习笔记-1-初步认识
- 简练软考知识点整理-中国制造2025
- AlterID.exe 报错问题
- 什么是数据挖掘技术,基本概念是什么?
- 周志华----机器学习
- IDEA添加项目启动配置
- Scikit-Learn入门教程
- 热身赛T3(奖学金评定)
- 100个优秀jQuery插件精选
- ALV导出到EXCEL数据被截断
- vue中data数据之间如何赋值
- P1118 [USACO06FEB]数字三角形`Backward Digit Su`…
- 使用Teleport pro整站下载相关问题
- 智慧水务平台、智慧水务监管平台——智慧水务整体解决方案
- 网络安全学术顶会——SP 2023 议题清单、摘要与总结(中)
- Linux系统下压缩与解压命令
- virsh日常管理命令
热门文章
- jQuery实现表格行的动态增加与删除(改进版)
- python seaborn教程_Seaborn官方教程中文教程(一)
- C语言 :探究Char 到底是啥
- 将进酒服务器是哪个位置的,李白的《将进酒》是他什么时候写下的?又是在哪里写的呢?...
- 为什么不建议在外包公司长期工作及外包公司的简历怎么写
- 自强不息系列之Python 线性查找
- 现在流行的少儿编程是不是再收大家的智商税?来看看最中肯的回答
- ftp服务器文件访问路径,ftp服务器访问路径格式
- 小学4年级计算机课作业题目,部编版小学语文四年级下册课堂同步作业试题及答案(全册).doc...
- python统计三国_如何用python对《三国演义》、《红楼梦》等名著开展词云分析及字频统计、出场统计等工作。...