You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 23 | 怎么减少内存使用,减轻内存管理负担?
管理内存不管是什么编程语言向来都是一个难题。Java语言能够长期领先的一个重要原因就是它拥有强大的内存管理能力并且这种能力还在不断地进化。然而只依靠Java内在的内存管理能力是远远不够的。
2018年9月亚马逊向OpenJDK社区提交了一个改进请求。这个改进涉及到一个问题如果一个服务的缓存数量巨大比如说有10万个连接会话Java的垃圾处理器要停滞几分钟才能清理完这么巨大的缓存。而这几分钟的停滞是不可忍受的事故。
这是一个值得我们关注的细节。缓存的本意,就是为了提高效率。然而,拥有过多的用户,过多的缓存,反而会让效率变低。
随着大数据、云计算以及物联网的不断演进很多技术都面临着巨大的挑战。七八年前2010年左右能解决C10K同时处理1万个用户连接问题感觉就可以高枕无忧了。现在有不少应用需要开始考虑C10M同时处理1千万个用户连接问题甚至是更多的用户连接以便满足用户需求。很多以前不用担心的问题也会冒出来算旧账。
要想让内存使用得更有效率,我们还需要掌握一些成熟的实践经验。
## 使用更少的内存
提高内存使用最有效率的办法,就是使用更少的内存。这听起来像是废话,却也是最简单直接、最有用的办法。减少内存的使用,意味着更少的内存分配、更少的内存填充、更少的内存释放、更轻量的垃圾回收。内存的使用减少一倍,代码的效率会成倍地提升,这不是简单的线性关系。
减少内存的使用,办法有且只有两个。第一个办法是减少实例的数量。第二个办法是减小实例的尺寸。
## 减少实例数量
还记得以前我们用到的,在不同的语言环境下,该怎么打招呼的代码吗?上一次,我们把它改成了不可变的类,避免了线程同步的问题。我把这段代码重新抄录在下面。
![](https://static001.geekbang.org/resource/image/7e/ea/7ef80d04c5d8da0aeac59bafac557dea.png)
这段代码还有个问题,就是内存使用不够友好。对于汉语环境来说,打招呼用“你好”。如果使用上面的设计,那么每次使用汉语环境,调用构造方法,都产生一个不同的实例对象。
如果只实例化一次当然没有什么问题。如果要实例化10次100次1000次10000次而且每个实例都是固定的汉语加“你好”这就是很大的浪费了。内存的使用随着实例的数量线性增长100个实例就要使用100倍的内存。即便实例的产生和废弃都非常迅速在巨大的实例数量面前Java的垃圾处理器也会有很大的压力。
```
HelloWords helloWords = new HelloWords("Chinese", "Ni Hao");
......
System.out.prinyln(
"The hello words in Chinese: " + helloWords.getGreeting());
```
如果一种语言环境的打招呼的办法是固定不变的,而且语言环境的数量有限的话,我们就只有必要使用一个实例。
如果有了这个意识的话,那么对于这个打招呼的代码,我们就可以很自然地想到使用枚举类型,把它改进成下面的样子。
![](https://static001.geekbang.org/resource/image/d9/27/d9230afaccf1c0e6d34b5400e35ffa27.png)
使用了枚举类型后每一种语言就只有一个实例了。不管使用多少次对内存的影响对Java的垃圾处理器的影响几乎可以忽略不计。
对于数量有限的对象,我们应该优先考虑使用枚举类型,比如交通标志,国家名称等等。其实,枚举类型就是一种常用的数据静态化的范例。我们还会在后面讨论其他类似的数据静态化的处理方式。
### 避免不必要的实例
Java语言里有一些历史遗留的接口设计问题会无意中导致不必要的实例。我们下面来看看两个例子。
第一个例子是应用程序使用了不必要的构造函数。比如使用String的构造函数实例化一串字符。
![](https://static001.geekbang.org/resource/image/6a/0d/6a4396ae788969f3ce2d709f22d8cc0d.png)
上面的反面实例每次调用都会产生一个实例对象而这个实例对象需要交给Java垃圾处理器管理。事实上由于String是一个不可变的类每次调用产生的实例没有任何的区别。如果这样的代码经常使用比如说被调用了十万次就会有十万个实例对象产生Java垃圾处理器就需要管理十万个实例。
这是一个很大的不必要的开销。上面的两个正面案例使用单实例的编码习惯无论这段代码被调用了多少次在Java的运行环境下都只有一个实例。而且相同的字符串即使位于不同的代码空间里在同一Java的运行环境下也都只有一个实例。
String类的这个构造函数是一个接口设计的历史遗留问题价值小问题多。Java基础类库里还有一些类似的历史遗留问题特别是原始数据类型primitive type对应的类。我们要避免使用它们的构造方法甚至避免使用这些类。
![](https://static001.geekbang.org/resource/image/f1/d0/f111895c74316514bdcd0f019fcaa1d0.png)
幸运的是这些原始数据类型对应类的构造方法从Java 9开始就已经被废弃了。但是这些方法依然存在这些类依然存在。不论在哪里如果你看到还有代码使用原始数据类型的构造函数都可以提交一个问题报告。这样的更改付出少收益大。
### 避免使用原始数据类
通过上面的讨论我们可以理解为什么要避免使用原始数据类型的构造方法。可是为什么还要避免使用原始数据类呢这里涉及到Java原始数据类型的自动装箱boxing与拆箱unboxing的类型转换。
比如说下面的代码就涉及到一个装箱的过程。整数0和2都要先被转换成一个Long类的实例然后才执行赋值操作。
![](https://static001.geekbang.org/resource/image/2b/46/2bcb06d24ede01daaa2b323114748d46.png)
这个装箱的过程,就产生了不必要的实例。如果这样的转换数量巨大,就会有明显的性能影响。
![](https://static001.geekbang.org/resource/image/c5/71/c5d0d51a6643b9afce5d18d990dab871.png)
### 使用单实例模式
由于Java内在的单实例模式我们可以很方便地使用Java的原始数据类型而不用担心实例数量的增长。对于复合的类我们也可以自己设计单实例模式从而减少多实例带来的不必要的开销。
比如,下面的代码,就是一个单实例模式例子。
![](https://static001.geekbang.org/resource/image/3a/8e/3a8261b250cac596481e5661a244818e.png)
单实例的设计方法有很多种方式,也有很多小细节需要处理,限于篇幅,我们就不在这里讨论这些技术了。欢迎你在讨论区分享你的经验和想法,来丰富这一部分的内容。
## 减小实例的尺寸
减少内存的使用还有另外一个办法,就是减小实例的尺寸。所谓减少实例的尺寸,就是减少这个实例占用的内存空间。这个空间,不仅包括实例的变量标识符占用的空间,还包括标识符所包含对象的占用空间。
比如下面的例子中使用了String构造方法的变量就独占了包括“Java”这四个字符的String实例空间。而使用了字符串赋值的变量就和其他代码一起共享“Java”这四个字符的缺省的实例空间。
![](https://static001.geekbang.org/resource/image/1f/68/1f202ccac93835e7ca0adae9f47cf468.png)
在减少变量数量这一方面,我们一般没有太多的自由空间。那么,在减少实例尺寸方面,我们能有所作为的,就是在标识符所指对象方面多费心思。简单地说,就是减少标识符所引用对象的尺寸。办法也有两个,第一个是尽量减少独占的空间;第二个是尽量使用共享的实例。
尽可能多地共享资源,这是一条提高效率的基本原则。在编写代码时,如果能够引用,就坚决不要拷贝;如果能够复用,就坚决不要新创。当然,资源的共享,除了上一次提到的线程同步问题,还有一个资源的维护问题。一个资源,如果不需要维护,那就太理想了。
有两类理想的共享资源一类是一成不变immutable的资源另一类是禁止修改unmodifiable的资源。
## 不可变的类
上一次,在讨论线程同步问题时,我们也讨论了不可变的类。由于不可变的类一旦实例化,就不再变化,我们可以放心地在不同的地方使用它的引用,而不用担心任何状态变化的问题。
## 无法修改的对象
还有一类对象,虽然不是不可变的类的实例,但是它的修改方法被禁止了。当我们使用这些对象的代码时,没有办法对它做出任何修改。这样,这些对象就有了和不可变的实例一样的优点,可以放心地引用。
从Java 8开始Java核心类库通过Collections类提供了一系列的生成不可更改的集合的方法。这些方法极大地减轻了集合的共享和维护问题。
比如,下面的这个方法,就返回了一个不可更改的列表对象。这个对象,可以赋值给多个标识符,不需要列表的拷贝,也不用担心列表的维护问题。在合适的场景,考虑使用好不可更改的集合,是一个值得推荐的编码习惯。
```
public List<byte[]> getStatusResponses() {
List<byte[]> responses = new ArrayList<>();
// snipped
return Collections.unmodifiableList(responses);
}
```
毋庸置疑的是,我们不能总是使用不变的共享资源。可以变化的共享资源也有难以替代的作用。后面的章节,我们再接着讨论使用可变的共享资源的技巧。
## 小结
今天,我们主要讨论了怎么减少内存使用。基本的方向有两个,一个是减少实例数量,另一个是减少实例的尺寸。这两个方向看着都很简单,我们在编码时,要养成考虑这两个因素的习惯。想得多了,用得多了,你编写的代码对内存就会越来越友好,设计的接口也会越来越好用。
应用程序方面,内存使用的优化技术和实践有很多。欢迎你在留言区,讨论这些技术和经验,分享你使用这些技术的心得体会,我们一起来学习、精进。
## 一起来动手
我上面使用的一个例子,写得确实很丑陋。不过,当我想到,可以把它当作一个练手题的时候,我就稍微宽心了点。
你琢磨琢磨下面的这段代码看看能不能实现getInstance()这个方法。该怎么修改,才能让这个方法更有效率?
另外你能想明白为什么构造方法会设计成私有方法吗变量为什么没有使用private关键字这些小细节很有意思如果你已经清楚了这些细节背后的原因也欢迎你分享在讨论区。
欢迎你在留言区讨论上面的问题,也可以把这篇文章分享给你的朋友或者同事,我们一起来看看这个有点丑的代码,可以变得有多美。
![](https://static001.geekbang.org/resource/image/3d/9e/3d160600ffe29fdb5fd1b6df9463469e.png)