Java性能权威指南-总结13

news2024/11/25 18:31:37

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 i
		private 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.coloradmin.cn/o/647770.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nautilus Chain测试网迎阶段性里程碑,模块化区块链拉开新序幕

Nautilus Chain 是目前行业内少有的真实实践的 Layer3 模块化链,该链曾在几个月前上线了测试网,并接受用户测试交互。该链目前正处于测试网阶段,并即将在不久上线主网,这也将是行业内首个正式上线的模块化区块链底层。 而在上个月…

Android 13(T) Media框架 -异步消息机制

网上有许多优秀的博文讲解了Android的异步消息机制(ALooper/AHandler/AMessage那一套),希望看详细代码流程的小伙伴可以去网上搜索。这篇笔记将会记录我对android异步消息机制的理解,这次学完之后就可以熟练运用这套异步消息机制了…

【数据库二】数据库用户管理与授权

数据库用户管理与授权 1.MySQL数据库管理1.1 常用的数据类型1.2 char和varchar区别1.3 SQL语句分类 2.数据表高级操作2.1 克隆表2.2 清空表2.3 创建临时表 3.MySQL的六大约束4.外键约束4.1 外键概述4.2 创建主从表4.3 主从表中插入数据4.4 主从表中删除数据4.5 删除外键约束 5.…

conda环境中配置cuda+cudnn+pytorch深度学习环境

本文参考: 在conda虚拟环境中配置cudacudnnpytorch深度学习环境(新手必看!简单可行!)_conda安装cudnn_江江ahh的博客-CSDN博客 一、创建虚拟环境 conda create -n mytorch python3.8 二、执行sudo nvidia-smi查看CU…

物联网通信技术

通信的技术指标是什么?AB A. 可靠性 B. 有效性 C. 实时性D. 广覆盖 多路复用技术有哪些?ABCD A. FDMA B. CDMA C. SDMA D. TDMA 使用多个频率来传输信号的技术被称为扩展频谱技术,该技术使用的目的是什么? AB A. 抗干扰B. 提…

【VMware】VMware安装CentOS8-Stream虚拟机

本文首发于 慕雪的寒舍 VMware安装CentOS8-Stream虚拟机 1.安装VMware 由于最新版的vm要钱,这里提供一个VMware16pro的安装包;我知道度盘下载速度慢,但确实没啥其他选择,见谅。 后文将用vm来简称VMware 提取嘛: gdt9 亚索包解…

解决UGUI的图集导致Shader采样时UV错误的问题

大家好,我是阿赵。 在我们用UGUI的时候,很多时候需要通过在UI上面挂材质球,写Shader,来实现一些特殊的效果。 这里句一个很简单的例子,只为说明问题。 一、简单例子说明 这个例子是这样的,我想在某个Imag…

Python模块openpyxl 操作Excel文件

简介 openpyxl是一个用于读取和编写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。openpyxl以Python语言和MIT许可证发布。 openpyxl可以处理Excel文件中的绝大多数内容,包括图表、图像和公式。它可以处理大量数据,支持Pandas和NumPy库导入和导出数据。…

chatgpt赋能python:Python本地安装库:一个简单易懂的指南

Python本地安装库:一个简单易懂的指南 Python是一种高级的编程语言,它拥有庞大的社区支持和无数的第三方库。如果你在使用Python时需要一些额外的功能,那么你可能需要安装一些库。本文将介绍如何在本地安装库,以及一些需要注意的…

chatgpt赋能python:如何更新Python库?Python更新库完全指南

如何更新Python库?Python更新库完全指南 Python作为一种最受欢迎的编程语言,其库和工具的数量是惊人的。这些库是Python生态系统的重要组成部分,以便帮助开发人员解决不同类型的问题。然而,这些库会更新,开发人员需要…

什么是椭圆曲线上的加法

椭圆曲线图形示例 注意,椭圆曲线随着你参数的不同,有不同的形态,这里仅是一种示例,详细的关于椭圆曲线的知识可以后附扩展知识连接 椭圆曲线上的加法 椭圆曲线上的加法不是我们通常意义上的数值加法,而是一种特殊的几…

干翻Mybatis源码系列之第十篇:Mybatis Plugins基本概念

给自己的每日一句 不从恶人的计谋,不站罪人的道路,不坐亵慢人的座位,惟喜爱耶和华的律法,昼夜思想,这人便为有福!他要像一棵树栽在溪水旁,按时候结果子,叶子也不枯干。凡他所做的尽…

Oracle中的行列互转———pivot、unpivot函数用法

一、需求说明 项目开发过程中涉及到oracle数据库的数据操作;但是需要将数据进行列的互转,通过查阅资料可知在oracle中有三种方式可以实现行列互转: ①使用decode 函数; ②使用case when 函数; ③使用pivot函数&…

Linux之设置主机名

目录 Linux之设置主机名 查看主机名 语法格式 案例 修改主机名 语法格式 案例 --- 修改静态主机名为joker 配置静态解析 为Linux主机指派域名解析 Linux之设置主机名 查看主机名 语法格式 hostnamectl [status] [--static|--transient|--pretty] 解析: s…

极致呈现系列之:Echarts地图的浩瀚视野(一)

目录 Echarts中的地图组件地图组件初体验下载地图数据准备Echarts的基本结构导入地图数据并注册展示地图数据结合visualMap展示地图数据 Echarts中的地图组件 Echarts中的地图组件是一种用于展示地理数据的可视化组件。它可以显示全国、各省市和各城市的地图,并支持…

整形在内存中的存储-原码补码反码的理解与应用

目录 一、概论 1.1 C语言中基本的数据类型 1.2 类型的基本归类 二、整形在内存中的存储 2.1 原码、反码、补码 2.2 存储补码和大小端存储 三、计算各基本数据类型的范围计算原理 3.1 有符号类型的整形范围 3.2 无符号类型的整形范围 3.3 例题 一、概论 C语言提供了非常…

【Java基础学习打卡07】Java语言概述

目录 引言一、Java语言1.Java语言简介2.Java语言优势3.Java能做什么? 二、Java之父三、Java简史1.Java版本时间线2.Java发展重要节点 总结 引言 一、Java语言 1.Java语言简介 Java语言是一种以面向对象为基础的高级编程语言。吸收了C语言的各种优点,又…

【IMX6ULL驱动开发学习】06.APP与驱动程序传输数据+自动创建设备节点(hello驱动)

一、APP与驱动之间传输数据 /*驱动从APP获取数据*/ unsigned long copy_from_user(void *to, const void *from, unsigned long n)/*驱动传输数据到APP*/ unsigned long copy_to_user(void *to, const void *from, unsigned long n)二、使用copy_to_user、copy_from_user在AP…

32908字长文理解Large CV Model:Segment Anything

作者:猛码Memmat 目录 Abstract1. IntroductionTaskModelData engineDatasetResponsible AIExperimentsRelease 2. Segment Anything TaskTaskPre-trainingZero-shot transferRelated tasksDiscussion 3. Segment Anything ModelImage encoderPrompt encoderMask de…

十个实用MySQL函数

函数 0. 显示当前时间 命令:。 作用: 显示当前时间。 应用场景: 创建时间,修改时间等默认值。 例子: 1. 字符长度 命令:。 作用: 显示指定字符长度。 应用场景: 查看字符长度时。 例子: 2. 日期格式化 命令…