高效使用内存,这些你得知道
共 7439字,需浏览 15分钟
·
2021-04-01 14:11
❝之后更新文章的频率会变高,不过可能不会只更新技术文章了
最近也在阅读“金字塔原理“和”高效能人士的七个习惯“这两本书,之后也会做个读书心得分享
❞
前言
首先有一个结论:
「减少堆内存的使用可以更高效使用内存」
❝这句话怎么理解呢?
❞
堆内存用的越少,堆被填满的几率就越低,新生代回 收的次数更少,对象的晋升年龄也就不会很频繁地增加,这意味着对象被提升到老年代的 可能性也降低了,因此, Full GC会减少,降低了GC发生的频率
「下面将介绍几种减少内存使用的方式」
减少对象大小
对象会占用一定数量的堆内存,所以要减少内存使用,最简单的方式就是让对象小一些
减少对象大小有两种方式:减少实例变量的个数,或者减少实例变量的大小
注意这里的减少变量大小主要是对每个对象选择更小的类型,避免空间的浪费
「同时对于对象大小的计算,有几个注意点:」
1.对象大小未必和你计算的一样,因为对象会被填充到 8 字节的整数倍
2.对象内部即使为 null 的实例变量也会占用空间
延迟初始化
有时候需要采用延迟初始化来降低初始化类和创建对象的开销
注意我们一般不会在线程安全的代码上引入延迟初始化,这样会增加同步操作的开销
同时对于使用了线程安全对象的代码,如果要采用延迟初始化,也应该使用双重检查锁,比如大家熟悉的单例的写法:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
「尽早清理空值数据」
延迟初始化的对象如果一直不用的话,通过将变量的值设置为 null ,实现尽早清理, 从而使对象可以更快地被垃圾收集器回收
可以看看JDK 中 ArrayList 类的 remove() 方法的实现:
public E remove(int index) {
//检查下标是否越界
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
//将index+1以及之后的元素向前移动以为,覆盖被删除的值
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将最后一个位置的元素清空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
关键就是对于不需要再引用的元素,应该主动将其设置为 null
不可变对象
「使用不可变的对象常量可以很好的减少空间的浪费」
在 Java 中,很多对象类型都是不可变的
包括那些有相应的基本类型的类,如 Integer 、 Double 和 Boolean 等, 以及其他一些基于数值的类型, 如 BigDecimal
最常见的 Java 对象当属不可变的 String
「举个Boolean类的例子说说」
在 Java 中,其实只需要两个 Boolean 示例,一个表 示 true , 一个表示 false
Boolean 类有一个 public 的构造 器,应用喜欢创建多少这类对象就能创建多少,即使它们和两个Boolean 对象其 中之一是完全相同的
更好的设计方案应该是,让 Boolean 类只有一个 private 的构造器, 通过 static 方法根据其参数返回 Boolean.TRUE
或 Boolean.FALSE
,就可以防止它们占用应用中额外的堆空间
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
字符串常量池
字符串是最常见的 Java 对象;应用的堆中几乎到处都是字符串
如果有大量的字符串是相同的,那很大一部分空间都是浪费的
「所以有了字符串常量池的概念」
我们知道String 常见的创建方式有两种,new String()
的方式和直接赋值的方式
直接赋值的方式会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而 new String()
的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串
String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
除此之外编译器还会对 String 字符串做一些优化,例如以下代码:
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2);
虽然 s1 拼接了多个字符串,但对比的结果却是 true,我们使用反编译工具,看到的结果如下:
Compiled from "StringExample.java"
public class com.lagou.interview.StringExample {
public com.lagou.interview.StringExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Java
2: astore_1
3: ldc #2 // String Java
5: astore_2
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
22: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 22
}
从编译代码 #2
可以看出,代码 “Ja”+”va” 被直接编译成了 “Java” ,因此 s1==s2
的结果才是 true,这就是编译器对字符串优化的结果
最后
「喜欢的话,希望帮忙点赞,转发下哈,谢谢」
微信搜索:月伴飞鱼,交个朋友
公众号后台回复666,获得免费电子书籍,必读书籍这里全都有
参考:
书籍:Java性能权威指南
社招一年半面经分享(含阿里美团头条京东滴滴)
条件语句的多层嵌套问题优化,助你写出不让同事吐槽的代码