写点什么

Java 程序经验小结:性能优化手段之避免创建不必要的对象

发布于: 2021 年 01 月 17 日
Java 程序经验小结:性能优化手段之避免创建不必要的对象

1、写在开头

一般来说,最好能重用对象,而不是在每次需要的时候创建同一个相同功能的新对象。重用对象是快速又高效的一种编码手段。

本节讨论的目标:就是如何优化已经出现重复创建对象的代码块,以达到优化性能。


本文属于《读懂 Effective Java》系列文章,本系列文章的大纲如下:


开讲前,我们先回顾下 JVM 的基本结构。根据《Java 虚拟机规范(Java SE 7 版)》。

JVM 的内存管理包括以下几个运行时数据区域:

  • 程序计数器(Program Counter Register):当前线程执行的字节码指示器

  • Java 虚拟机栈(Java Virtual Machine Stacks):Java 方法执行的内存模型,每个方法会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 本地方法栈(Native Method Stack):(虚拟机使用到的)本地方法执行的内存模型。

  • Java 堆(Java Heap):虚拟机启动时创建的内存区域,唯一目的是存放对象实例,处于逻辑连续但物理不连续内存空间中。

  • 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 运行时常量池(Runtime Constant Pool):方法区的一部分,存放编译器生成的各种字面值和符号引用。




2、场景 1:不可变对象的重复创建

如果对象是不可变(immutable),它就始终可以被重用。

我们先看 例子 1



public class TestStringCreate {public static void main(String[] args) {String s1 = new String("stringette");//反例String s2 = "stringette";//正解 }}
复制代码


语句 1:String s1 = new String("stringette");

经过编译期,字符串“stringette”在类加载时已经创建好了对象,这里传递给 String 构造器的参数 ("stringette") 本身就是一个 String 实例。(因此属于重复创建对象的案例!这里会涉及 JVM 对 String 对象的内存分配原理,可以参考公众号的另一篇文章:《String 源码剖析》


语句 2:String s2 = "stringette";

这个优化版本,只用了一个实例,而不是执行时创建的新实例。


构造器在每次被调用时,都会创建一个新的对象,而静态方法则不要求也实际上不会这么做。


3、场景 2:可变对象的重复创建

对于已知不会被修改的可变对象,也是可以被重用的。


我们先看 例子 2



//反例public class Person { private final Date birthDate;public Person(Date birthDate) {this.birthDate = birthDate; }public boolean isBababyBoomer(){//每次调用方法,执行一次Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946, Calendar.JANUARY, 1,0,0,0); Date boomStart = gmtCal.getTime(); gmtCal.set(1965, Calendar.JANUARY, 1,0,0,0); Date boomEnd = gmtCal.getTime();return (birthDate.compareTo(boomStart) >= 0) && (birthDate.compareTo(boomEnd)<0); }}
复制代码


代码分析:

isBababyBoomer 方法每次被调用,都会创建一个 Calendar 、一个 TimeZone 和两个 Date,显然这是不必要的。


代码优化:

利用一个静态的初始化器(initializer)避免这个效率低下的情况,如例子 3:



public class Person2 {private final Date birthDate;private static final Date BOOM_START = null;private static final Date BOOM_END = null;
//静态代码块,执行一次static { Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946, Calendar.JANUARY, 1,0,0,0);Date boomStart = gmtCal.getTime(); gmtCal.set(1965, Calendar.JANUARY, 1,0,0,0);Date boomEnd = gmtCal.getTime(); }
public Person2(Date birthDate) {this.birthDate = birthDate; }
public boolean isBababyBoomer(){return (birthDate.compareTo(BOOM_START) >= 0) && (birthDate.compareTo(BOOM_END)<0); }}
复制代码


代码分析:

改进后,Calendar 、一个 TimeZone 和 Date 都只会在初始化时被实例化一次,而不是 isBababyBoomer 方法每次被调用时都会创建。

4、场景 3:自动装箱机制带来的性能开销

jdk 发行版(1.5 之后),有一种创建多余对象的新方法:自动装箱(autoboxing),它允许程序员将基本类型 & 装箱基本类型(Boxed Primitive Type)进行混用,按需装箱与拆箱。

自动装箱机制,导致基本类型与装箱类型这两者在语义上只有微妙差役,但在性能上有比较明显的差别。


我们看下例子 4:



/** * <p> * jdk5之后,自动装箱使得 基本类型 & 装箱基本类型(Boxed Primitive Type)混用。 * 两者在语义上有微妙差役,但在性能上有比较明显的差别。 * </p> */public class TestAutoBoxing {public static void main(String[] args) { testPrimitive(); testBoxing(); } //装箱类型private static void testBoxing() {long start = System.currentTimeMillis();Long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++){ sum += i; } System.out.println("User Boxing, sum = " + sum+ ", cost : " + (System.currentTimeMillis() - start)/1000 + "ms"); } //基本类型private static void testPrimitive() {long start = System.currentTimeMillis();long sum = 0L;for (long i = 0; i < Integer.MAX_VALUE; i++){ sum += i; } System.out.println("User Primitive type, sum = " + sum+ ", cost : " + (System.currentTimeMillis() - start)/1000 + "ms"); }}
复制代码


运行结果:


User Primitive type, sum = 2305843005992468481, cost : 1msUser Boxing, sum = 2305843005992468481, cost : 8ms
复制代码


代码分析:

先说结果:同样一个循环执行代码段,使用自动装箱的运行时间是使用基本类型的 8 倍。

语句:Long sum = 0L;

使用了自动装箱机制,意味着程序构造了大概 2 的 31 次方个多余的 Long 实例(大约每次往 Long sum 增加 long 时构造一个实例)。


语句:long sum = 0L;

使用了基础类型,减少了多余的创建对象的开销。


优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱所带来的性能损耗。


5、总结

实际上,由于小对象的构造器知识做很少的显式工作,所以它的创建 &回收动作是非常廉价的,特别是现代的 JVM 实现上更是如此。

但是,我们对于维护自己的对象池(object pool)来避免创建对象不一定是好事,除非对象池的对象十分重要,如:数据库连接。

另外,在提倡保护性拷贝时,因为重用对象而付出的代码要比创建重复对象的代码要高,这一点我们后续再聊。


6、延伸阅读

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式


《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭


《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》


《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

读懂排序算法(六):二分查找算法》


《设计模式》

设计模式之六大设计原则

设计模式之创建型(1):单例模式

设计模式之创建型(2):工厂方法模式

设计模式之创建型(3):原型模式

发布于: 2021 年 01 月 17 日阅读数: 45
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
Java 程序经验小结:性能优化手段之避免创建不必要的对象