《JAVA程序性能优化》笔记

时间:2021-01-03 05:59:43
一、设计优化
1、单例模式:延时加载(内部类)
反射和序列化会破坏单利


2、代理模式:延时加载
a、静态代理:包括主题接口、真实主题、代理类、main。初始化时使用代理,真正使用时再通过代理加载真实主题。
b、动态代理:jdk动态代理、CGLIB、javassist基于动态代码的代理、ASM
c、动态加载过程(以CGLIB为例):
——根据指定的回调类生成class字节码,并保存在byte数组中
——通过反射,调用ClassLoader.defindClass将字节码定义为类
——使用反射机制生成该类的实例
d、经典框架:Hibernate的load请求


3、AOP的实现方式:
a、静态织入:在编译期将切面织入到目标文件中
b、jdk动态代理:运行期为接口生成代理类,再将切面织入代理类中
c、CGLIB和javassist动态字节码:运行期,在目标加载后,动态构建字节码文件生成目标类的子类,将切面织入子类中


4、享元模式:
a、复用大对象,节省内存和创建的时间
b、和对象池的不同在于:享元对象都是不同的,各自有各自的含义和用途,而对象池的对象都是等价的,如数据库连接池中的连接


5、装饰着模式:
a、可以将功能组件和性能组件分开,需要再结合起来
b、设计原则:使用委托,少用继承
c、经典例子:OutputStream和InputStream


6、观察者模式


二、常用优化组件和方法
1、缓冲:
a、协调上层组件和下层组件的性能差异,最常用于提高I/O的速度
b、经典例子:写文件操作,FileWriter和BufferedWriter,使用缓冲区的writer性能会提升一倍


2、缓存


3、对象复用——池
a、只有对重量级对象使用对象池技术才能提高系统性能,对轻量级对象使用对象池,反而会降低系统性能
b、在Apache中,已经提供了一个Jakarta Commons Pool对象池组件可使用


4、并行替代串行:数据迁移


5、负载均衡


6、时间换空间:CPU与内存


7、空间换时间:缓存


三、java程序优化
1、String:
a、由char数组、偏移量和string长度组成
b、其真实长度由偏移量和长度在这个char数组中进行定位和截取
c、substring容易导致内存泄漏
d、对于字符串相加,如果直接使用string相加,编译期会自动优化成StringBuilder,所以会有所优化。但若是在for循环中string相加,编译期没有那么只能,所以每次循环都会为string创建一个新的StringBuilder,所以相对而言,直接用string相加还是不如直接用StringBuilder来append


2、List(ArrayList、Vector、LinkedList):
a、ArrayList和Vector ——> AbstractList ——> List
  LinkedList ——> AbstractSequenceList ——> AbstractList ——> List
b、ArrayList和Vector都是基于数组实现,封装了对内部数组的操作
  LinkedList使用循环双向链表数据结构。前驱表项(最后一个元素) <—— 当前元素(header) ——> 后驱表项(第一个元素)
c、普通新增操作(即直接新增到最后)
ArrayList:
public boolean add(E e){
encureCapacity(size+1); //确保内部数组有足够空间,不够则进行扩容,1.5倍。性能取决于这个方法
elementData[size++] = e; //将元素加入到数组的末尾,完成添加
return true;
}
LinkedList:
private Entry(E) addBefore(E e, Entry<E> entry){
//下面三行代码是性能消耗的关键点
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); //创建新的元素
newEntry.previous.next = newEntry; //将前驱表项的下一个元素指向当前新增元素
newEntry.next.previous = newEntry; //将后驱表项的上一个元素指向当前新增元素
size++;
modCount++;
return newEntry;
}
d、根据下标新增到指定位置
ArrayList:每次插入操作,都会进行数组复制,下标越前,性能越差
LinkedList:在哪里插入性能都一样
e、删除指定位置
ArrayList:同新增一样,都需要进行数组的复制,从尾到头性能逐渐提升
LinkedList:需要遍历,下标为中间是性能最差,需要遍历1/2的列表元素
f、foreach运行时会被编译期解析成迭代器,反编译的代码看到还多了一步赋值的操作,所以三种循环的性能排序为
for循环 > 迭代器 > foreach


3、Map:
a、properties ——> HashTable ——> Dictionary、Map
  HashMap ——> AbstractMap ——> Map
  TreeMap ——> AbstractMap ——> Map
  LinkedHashMap ——> HashMap ——> AbstractMap ——> Map
b、HashTable不允许key或value使用null值,但HashMap可以
c、HashMap原理:将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应的数据。底层数据结构是数组,内存地址即数组的下标索引
d、为何HashMap是高性能的:
—— hash算法高效(多采用navive本地方法和位运算)
—— hash值到内存地址(数组索引)的算法高效(根据hash值和数组长度 按位与计算)
—— 根据内存地址(数组索引)可以直接取得对应的值
e、hash算法和查找的源码:
—— hash算法:
int hash = hash(key.hashCode()); //计算key的hash值
public native int hashCode(); //可以重写,性能关键点,所以重写的hashCode方法是否冲突很重要
static int hash(itn h){ //基于位运算
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
—— 查找内存地址的算法:
int i = indexFor(hash, table.length);
static int indexFor(int h, int length){
return h & (length-1);
}
f、HashMap高性能的注意条件:
—— hashCode()方法的实现,要尽可能减少冲突,这样子对HashMap的操作就近乎对数组的随机访问。若冲突多,则相当于退化成几个链表,等价于遍历链表,性能很差。一般hashCode的生成可以直接使用eclipse IDE提供的方法,或者引入第三方库如Apache commons来生成
—— 容量参数,HashMap的扩容会遍历整个HashMap,对里面的数据进行重新计算在新数组的位置,所以应尽量避免扩容,在初始化的时候就估算好大概的容量
—— 负载因子 = 元素个数 / 内部数组大小,默认为0.75,尽量不要大于1,这样子会带来冲突
g、HashMap的表项结构,实际上是一个链表的数组
Entry1(每个Entry包括key、value、next、hash)
Entry2
...
...
Entryn ——> Entryn1 ——> Entryn2(hash冲突的链表结构)
h、LinkedHashMap:维护了元素次序表的HashMap,在每个Entry对象中添加了before、after两个属性。有两种排序类型,按照元素进入集合的顺序或者被访问的先后顺序排序
i、TreeMap:基于匀速的固有顺序排序(由Comparator或者Comparable确定)。其内部实现是基于红黑树,是一种平衡查找树,性能要优于平衡二叉树,可以在O(log n)时间内查找、插入和删除


4、Set:
a、HashSet、LinkedHashSet、TreeSet都只是对应的Map的一种封装,所有操作都委托给HashMap对象完成。


5、NIO:
a、与流式I/O不同,它是基于块(Block)的
b、最重要的两个组件:缓冲Buffer和通道Channel。通道表示缓冲数据的源头或目的地,用于向缓冲读取或者写入数据,是访问缓冲的接口。应用程序不能直接对Channel进行读写操作,而必须通过Buffer来进行。
c、NIO进行文件复制的例子:
public static void nioCopyFile(String resource, String destination){
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel(); //读文件通道
FileChannel writeChannel = fos.getChannel(); //写文件通道
ByteBuffer buffer = ByteBuffer.allocate(1024); //读入数据缓存
while(true){
buffer.clear();
int len = readChannel.read(buffer);
if(len == 1){
break;
}
buffer.flip();
writerChannel.write(buffer);
}
readChannel.close();
writeChannel.close();
}
d、Buffer3个重要的参数:position(位置)、capacity(容量)、limit(上限)
当执行flip()操作会将写模式转换为读模式,并将limit设置为当前position和将position置为0
e、三种文件流操作性能对比:
基于Buffer的性能比普通的基于流的性能要高一倍,基于Buffer并将文件映射到内存的性能要高出一个数量级


四、并行程序开发及优化
1、Future模式(可用于一个大方法中,某些小方法比较耗时,则可以将这些小方法用这种模式处理)
a、核心在于去除主函数等待时间,并使得原本需要等待的时间段可以用于处理其他业务,充分利用计算机资源。
b、jdk的并发包中已经内置了一种Future模式的实现,关键是callable接口的call()方法,重载定义业务逻辑。

2、Master-Worker模式(可用于数据迁移前的准备数据,多进程收集数据并异步计算结果)
a、Master进程为主要进程,维护了一个worker进程队列、子任务队列和子结果集。Worker进程队列中的worker进程不停地从任务队列中提取要处理的子任务,并将子任务的处理结果写入结果集。


3、生产者-消费者模式
a、模式架构图:生产者 ——> 内存缓冲区 ——> 消费者
b、生产者Producer:用于提交用户请求,提取用户任务,并装入内存缓冲区
  消费者Consumer:在内存缓冲区中提取并处理任务
  内存缓冲区BlockingQueue:缓存生产者提交的任务或数据,供消费者使用