Java性能权威指南-总结13

news/2025/1/14 17:54:41/

Java性能权威指南-总结13

  • 堆内存最佳实践
    • 减少内存使用
      • 减少对象大小
      • 延迟初始化

堆内存最佳实践

减少内存使用

减少对象大小

对象会占用一定数量的堆内存,所以要减少内存使用,最简单的方式就是让对象小一些。考虑运行程序的机器的内存限制,增加10%的堆有可能是无法做到的,但是堆中一半对象的大小减少20%,能够实现同样的目标。

减少对象大小有两种方式:减少实例变量的个数(效果很明显),或者减少实例变量的大小(效果没那么明星)。下表列出了Java中不同类型实例变量的大小:
在这里插入图片描述

这里的引用类型指的是指向任何类型Java对象(包括类或数组的实例)的引用。这个空间存储的只是参数本身。如果对象中包含指向其他对象的引用,其大小会因想考虑Shallow Size,Deep Size还是Retained size(保留大小)而有所不同,不过其中都会包含一些隐藏的对象头字段。对于普通对象,对象头字段在32位JVM上占8字节,在64位JVM上占16字节(跟堆大小无关)。对于数组,对象头字段在32位JVM以及堆小于32GB的64位JVM上占16字节,其他情况下是64字节。
例如,考虑这几个类定义:

	public class A {private int i;}public class B {private int iprivate Locale l = Locale.US;}public class C {private int i;private ConcurrentHashMap chm = new ConcurrentHashMap();
}

在堆小于32GB的64位Java7JVM上,这几个类的实例实际大小如下表所示:
在这里插入图片描述
在B类中,定义Locale应用将对象的大小增加了8字节,但至少在这个例子中,实际的Locale对象是与其他一些类共享的。如果该类实际上从来没用到这个Locale对象,那将这个实例包含进来,只会浪费引用所占的额外空间。当然,如果应用创建了大量B类的实例,还是会积少成多。

另一方面,定义并创建一个ConcurrentHashMap,除了对象应用会消耗额外的字节,这个HashMap对象还会增加200字节。如果这个HashMap从来不用,C的实例就非常浪费。

仅定义需要的实例变量,这是节省对象空间的一种方式。还有一种效果不那么明显的方案,就是使用更小的数据类型。如果某个类需要记录8个可能的状态之一,用一个字节就可以了,而不需要一个int,这就可能会节省3字节。使用float代替double,int代替long,诸如此类,都可以帮助节省内存,特别是在那些会频繁地实例化的类中。使用大小适当的集合类(或者使用简单的实例变量代替集合类)可以达到类似的节省空间的目的。

对象对齐与对象大小
上表中的类,都包含一个额外的整型字段,讨论中并没有引用到。为什么要放这么一个变量呢?事实上,这个变量的目的是让讨论更容易理解:B类比A类多8字节,正是所期望的(这样更明确)。
这掩盖了一个重要细节:为使对象大小是8字节的整数倍(对齐),总是会有填充操作。如果在A类中没有定义i,A的实例仍然会消耗16字节,其中4字节只是用于填充,
使得对象大小是8的整数倍,而不是用于保存i。如果没有定义i,B类的实例将仅消耗16字节,和A一样,即便B中还有额外的对象引用。B中仅包含一个额外的4字节引用,
为什么其实例会比A的实例多8字节呢,也是填充的问题。JVM也会填充字节数不规则的对象,这样不管底层架构最适合什么样的地址边界,对象的数组都能优雅地适应。因此,去掉某个实例字段或者减少某个字段的大小,未必能带来好处,不过没有理由不这么做。

去掉对象中的实例字段,有助于减少对象的大小,不过还有一个灰色地带:有些字段会保存基于一些数据计算而来的结果,这该如何处理呢?这就是计算机科学中典型的时间空间权衡问题:是消耗内存(空间)保存这个值更好,还是在需要时花时间(CPU周期)计算这个值更好?不过在Java中,权衡还会考虑CPU时间,因为额外的内存占用会引发GC消耗更多CPU周期。

比如,String的哈希码值(hashcode)就是对一个涉及该字符串中每个字符的式子求和计算而来的;计算会消耗一点时间。因此,String类会把这个值存在一个实例变量中,这样哈希码值只需要计算一次:最后,与不存储这个值而节省的内存空间相比,重用几乎总能获得更好的性能。另一方面,大部分类的toString()方法不会把对象的字符串表示保存在一个实例变量中,因为实例变量及其引用的字符串都会消耗内存。相反,与保存字符串引用所需的内存相比,计算一个新的字符串所花的时间通常不是很多,性能更好。(还有一个因素,String对象的哈希码值用的较为频繁,而对象的toString()表示使用却很少。)当然,这种情况必定是因人而异的。就时间/空间的连续体而言,究竟是使用内存来存储值,还是重新计算值,都是取决于许多具体因素的。如果目标是减少GC,则更倾向于采用重新计算。

快速小结

  1. 减小对象大小往往可以改进GC效率。
  2. 对象大小未必总能很明显地看出来:对象会被填充到8字节的边界,对象引用的大小在32位和64位JVM上也有所不同。
  3. 对象内部即使为null的实例变量也会占用空间。

延迟初始化

很多时候,决定一个特定的实例变量是否需要并不是非黑即白的问题。某个特定的类可能只有10%时间需要一个Calendar对象,但是Calendar对象创建成本很高,所以保留这个对象备用,而不是需要的时候再重新创建,绝对是有意义的。这种情况下,延迟初始化可以带来帮助。

到目前为止,所作讨论的前提是假定实例变量很早就会初始化。需要使用一个Calendar对象(不需要线程安全)的类看上去可能是这样的:

	public class CalDateInitialization {private Calendar calendar = Calendar.getInstance();private DateFormat df = DateFormat.getDateInstance();private void report(Nriter w) {w.write("On" + df.format(calendar.getTine()) + ":" + this);}	}

要延迟初始化其字段,在计算性能上会有一点小小的损失,代码每次执行时都必须测试变量的状态:

	public class CalDateInitialization {private Calendar calendar;private DateFormat df;private void report(Writer w) {if (calendar == null){calendar = Calendar.getInstance();df = DateFormat.getDateInstance();}w.write("On" + df.format(calendar.getTime()) + ":" + this);}}

如果问题中的这个操作使用不太频繁,那延迟初始化最适合:如果操作很常用,实际上没有节省内存(总是会分配这些实例),而常用操作又有轻微的性能损失。

当所涉及的代码需要保证线程安全时,延迟初始化会更为复杂。第一步,最简单的方式是添加传统的同步机制:

	public class CalDateInitialization {private Calendar calendar;private DateFormat df;private synchronized void report(Writer w) {if(calendar == null) {calendar = Calendar.getInstance();df = DateFormat.getDateInstance();}w.write("On" + df.format(calendar.getTime()) + ":" + this);}

在解决方案中引入同步,会使得同步也有可能成为性能瓶颈。不过这种情况很罕见。对于问题中的对象而言,只有当初始化这些字段的几率很低时,延迟初始化才有性能方面的好处。因为,如果一般情况下都会初始化这些字段,那实际上也不会节省内存。因此对于延迟初始化的字段,当不常用的代码路径突然被大量线程同时使用时,同步就会成为瓶颈。这种情况是可以想象的,不过好在并不多见。

只有延迟初始化的变量本身是线程安全的,才有可能解决同步瓶颈。DateFormat对象不是线程安全的,所以在现在的这个例子中,锁中是否包含Calendar对象并不重要:如果延迟初始化的对象突然被频频使用,那无论如何,围绕DateFormat对象所需的同步都会成为问题。线程安全的代码应该是这样的:

	public class CalDateInitialization {private Calendar calendar;private DateFormat df;private void report(Writer w) {unsychronizedCalendarInit();synchronized(df) {w.write("On" + df.format(calendar.getTime()) + ":" + this);}}}

涉及非线程安全的实例变量的延迟初始化,总会围绕这个变量做同步(例如,像前面所示的那样使用方法的同步版本)。

考虑一个有点不一样的例子,其中有一个比较大的ConcurrentHashMap对象,就采用了延迟初始化:

	public class CHMInitialization {private ConcurrentHashMap chm;public void dooperation() {synchronized(this) {if(chm == null) {chv = new ConcurrentHashMap();	}}}}

因为多个线程可以安全地访问ConcurrentHashMap,所以这个例子中的多余的同步,就是一种不太常见的情况,因为即便是恰当地使用延迟初始化,也引入了同步瓶颈。(不过这种瓶颈应该极为少见;如果这个HashMap访问非常频繁,那就应该考虑延迟初始化到底有什么好处了。)该瓶颈可以使用双重检查锁这种惯用法来解决:

	public class CHMInitialization {private volatile ConcurrentHashMap instanceChm;public void dooperation() {ConcurrentHashMap chm = instanceChm;if (chm == null) {synchronized(this) {chm = instanceChm;if (chm == null) {chm = new ConcurrentHashMap();instanceChm = chm;}	}}

这里有些比较重要的多线程相关的问题:实例变量必须用volatile来声明,而且将这个实例变量赋值给一个局部变量,性能会有些许改进。

尽早清理
从延迟初始化变量可以推出另一种行为,即通过将变量的值设置为null,实现尽早清理,从而使问题中的对象可以更快地被垃圾收集器回收。不过这只是理论上听着不错,真正能发挥作用的场合很有限。
可以选择延迟初始化的变量,可能看上去也可以选择尽早清理:在上面的例子中,一完成report()方法,Calendar和DateFormat对象就可以设置为null了。然而,如果后面再调用到这个方法(或者同一个类中的其他地方)时,并没有用到该变量,那最初就没有理由将其设计为实例变量:在方法中创建一个局部变量就可以了,而且当方法完成时,局部变量就会离开作用域,然后垃圾收集器就可以释放它了。

不需要尽早清理变量,这个规则有个很常见的例外情况,即对于类似Java集合类框架中的那些类:它们会在较长的时间内保存一些指向数据的引用,当问题中的数据不再需要时会通知它们。考虑JDK中ArrayList类的remove()方法的实现(部分代码有所简化):

	public E remove(int index) {E oldValue = elementData(index);int numMoved = size - index - 1;if(numMoved>θ)System.arraycopy(elementData, index+1,elementData, index,numMoved);elementData[--size]= null;//清理,让GC完成其工作return oldValue;
}

JDK源代码中有一行关于GC的注释:像这样将某个变量的值设置为null,这种操作并不常见,需要解释一下。在这种情况下,我们可以看看当数组的最后一个元素被移除时,会发生什么。仍然存在于数组中的条目数,也就是实例变量size,会被减1。比如说size从5减少到4。现在不管elementData中存的是什么,都不能访问了:它超出了数组的有效范围。

在这种情况下,elementData是一个过时的引用。elementData数组可能仍会存活很长时间,因此对于不需要再引用的元素,应该主动将其设置为null。
过时引用的概念是这里的关键:如果一个长期存活的类会缓存以及丢弃对象引用,那一定要仔细处
理,以避免过时引用。否则,显式地将一个对象引用设置为null在性能方面基本没什么好处。

快速小结

  1. 只有当常用的代码路径不会初始化某个变量时,才去考虑延迟初始化该变量。
  2. 一般不会在线程安全的代码上引入延迟初始化,否则会加重现有的同步成本。
  3. 对于使用了线程安全对象的代码,如果要采用延迟初始化,应该使用双重检查锁。

http://www.ppmy.cn/news/393700.html

相关文章

【华为OD机试真题 C语言】60、翻牌求最大分 | 机试真题+思路参考+代码解析(未)

文章目录 一、题目🔸题目描述🎃输入输出🎃样例1 二、思路参考三、代码参考 作者:KJ.JK 🍂个人博客首页: KJ.JK 🍂专栏介绍: 华为OD机试真题汇总,定期更新华为OD各个时间…

GPU 并行计算入门

文章目录 0. 前言1. CPU vs GPU2. 并行计算简介3. CUDA 简介4. CUDA 的处理流程 0. 前言 在没有GPU之前,基本上所有的任务都是交给CPU来做的。有GPU之后,二者就进行了分工,CPU负责逻辑性强的事物处理和串行计算,GPU则专注于执行高…

JavaScript 进阶 - 第3天

文章目录 JavaScript 进阶 - 第3天1 编程思想1.1 面向过程1.2 面向对象(oop) 2 构造函数3 原型对象3.1 原型3.2 constructor 属性3.3 对象原型3.4 原型继承3.5 原型链(面试高频) JavaScript 进阶 - 第3天 了解构造函数原型对象的语…

解决联想笔记本安装银河麒麟系统安装时只有机械硬盘,没有固态硬盘的方法

现象:联想笔记本电脑,配了一块固态硬盘和一块机械硬盘,在进入银河麒麟系统安装界面后,到选择安装位置的时候,只读出了一块机械硬盘,没有找到固态硬盘。 考虑到需要在kylin上开发,所以想把kylin放…

三星530换固态硬盘_小米笔记本Air13.3加装固态硬盘(三星860EVO)

由于本人的小米笔记本256G只剩下20G不到了,最近入手了一块250G的三星860evo固态硬盘,扩展一下电脑存储,安装过程很顺利。 安装之前 安装之前要先确认自己电脑预留接口的类型,小米笔记本预留接口是M.2 SATA,满足该协议的固态都能装,三星的这块固态应该是最好的,但价格也相…

KaiwuDB 数据库高可用方案及落地实现

5月23日(上周二)由 KaiwuDB 高级架构师冯友旭,针对数据库停机可能带来的严重后果,为大家分享 KaiwuDB 数据库高可用方案及落地实现。 欢迎大家点击观看本次直播回放,一起学习数据库的高可用技术方案 ↓↓↓ 数据库高可…

在线式测斜仪参数介绍

在线式测斜仪是⼀款新型的、智能的、适应多种⾏业应⽤的三轴智能测斜仪,主要⽤于在三维空间内进⾏多⽅位倾⻆测量和振动(频率、振幅)测量。将多个传感器串联安装到⼟体、岩⽯和挡⼟墙结构中沿垂直⽅向埋设的测斜管中,便可以实现对…

【刷题】2.BM3 链表中的节点每k个一组翻转

题目 分析 判断是否为k长链表单节点处理k长链表反转链接前后链表 代码 import java.util.*;/** public class ListNode {* int val;* ListNode next null;* }*/public class Solution {/**** param head ListNode类* param k int整型* return ListNode类*/public ListN…