Java性能优化
2018-02-09部分代码在不同的JDK版本可能会有出入
第1章 Java性能调优概述
评价性能的主要指标,服务器响应时间
服务器吞吐量
- 程序性能关键指标:
执行响应时间
内存分配
启动时间
负载承受能力
CPU占用时间
磁盘I/O吞吐量
网络吞吐量
- 木桶原理的概念及其在性能优化中的应用,即对系统中响应时间最差的进行优化。一般来讲
内存读写 > 本地磁盘I/O > 网络I/O
, 主要看任务是CPU密集
还是I/O密集
,异常捕获和处理非常消耗计算机资源,数据库读写瓶颈,锁竞争会增加线程上下文切换开销白白占用CPU资源。内存。以上是在木桶原理中可能的短板块。 Amdahl
定律,使用多核CPU对系统进行优化,优化的效果取决于对CPU的数量以及系统中的串行化程序的比重,CPU数量越多,串行化比重越低,则优化效果越好,仅提高CPU数量而不降低程序的串行化比重,也无法提高系统性能。- 性能调优的层次,
设计调优:处于最上层,好的系统设计可以规避很多潜在的性能问题,所以尽可能多花些时间在系统设计上是创建高性能程序的关键
,代码调优微观也是产生最直接的优化方法
,JVM调优
,数据库调优,SQL优化,数据库表优化拆解合并
,操作系统调优,共享内存段信号量,共享内存最大值,共享内存最小值,最大文件句柄数,磁盘的块大小等
- 系统优化的一般步骤和注意事项.
系统优化步骤: 1. 明确优化目标(吞吐量/响应时间..) 2. 测试 3. 是否达到目标,Yes则终止,No则继续4. 4. 查找瓶颈 5. 改进实现(修改代码,优化算法,对JVM,OS,DB或系统设计或更新硬件) 6. 继续进行2步骤测试,如此循环直到达到步骤1的目标
系统优化注意事项: 优化前进行系统评估,在软件功能,正确性和可维护性间取得平衡,而不应该过分追求软件的性能.性能调优必须有明确目标,不要为了调优而调优,如果当前程序并没有明显的性能问题,盲目地进行调整,风险性可能远大于收益。
第2章 设计优化
单例模式的使用和实现.优点: 1. 对于频繁使用的对象,减少反复创建的带来的开销 2. 对于new操作次数少了,因而系统内存的使用率也会降低,减轻GC压力,缩短GC停顿时间
public class Singleton implements java.io.Serialisable {
private Singleton(){}
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance () {
return SingletonHolder.instance;
}
//JVM反序列化地"组装"一个新对象时,会调用readResolve方法,
private Object readResolve() {
return SingletonHolder.instance;
}
}
这种实现方式做到 1. 延迟加载 2. 不使用锁性能高,采用JVM类加载时创建,对多线程友好 3. 避免反序列化创建多个实例对象.
代理模式的实现和深入剖析
享元模式的应用
装饰着模式对性能组件的封装
观察者模式的使用
使用Value Object
模式减少网络数据传输
使用业务代理模式添加远程调用缓存
缓冲和缓存的定义和使用
对象池的使用场景及其基本实现
构建负载均衡系统以及Terracotta
框架的简单使用
时间换空间和空间换时间的基本思路
第3章 Java程序优化
Java语言中字符串的优化,如何更高效利用字符串
- String对象特性:
不变性
,针对常量池的优化
,类的final定义
; 内部结构:char数组
,offset偏移
,count长度
- 不变性: 一个对象状态在对象被创建之后就不再发生改变,主要作用在当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。不变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式。
- 针对常量池的优化: 当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节约内存空间。
- 类的final定义: 对系统安全性的保护,JDK1.5版本之前可以提高系统效率,1.5之后效果不明显
String s1 = "abc", s2 = "abc", s3 = new String("abc");
s1 == s2 //true
s1 == s3 //false
s1 == s3.intern() //true,不同版本intern()不同
- subString(int beginIndex, int endIndex);底层实现是
new String(offset+beginIndex, endIndex-beginIndex, value)
高效但是如果原字符串很大,截取的却很短,则将会有大量的内从空间浪费,甚至发生内存溢出情况,因为采用了共享原数组导致不能释放造成的。采取以空间换时间的策略.这个问题JDK1.7已修复由共享数组变成了传统的拷贝,老的JDK版本可以采用new String(s1.substring(0,2))
这种方式获取。参考http://www.cnblogs.com/hxy520/p/5450893.html https://yq.aliyun.com/articles/232605 - String[] split(String regex)简单功能强大性能不尽人意。可以使用更高效的
StringTokenizer(String str, String delim)
,indexOf()
结合使用更高效 - 高效的
charAt()
StringBuffer
同步低效和StringBuilder
非线程安全高效。capacity
容量可以进行初始化,当所需容量超过char数组长度,需要进行扩容,翻倍操作,然后进行数组复制
这里如果大对象会涉及到大量内存复制操作,如果能预估出大小进行初始化,则可以有效减少因复制带来的内存消耗,从而提高系统的性能
String result = "string" + "and" + "hello";//高效编译器已优化
String s1 = "string", s2 = "s2", s3 = "hello";
String s = s1 + s2 + s3;//底层编译器做的优化实现 s = (new StringBuilder(String.valueOf(s1))).append(s2).append(s3).toString();
Vector, ArrayList等核心数据结构优化方法介绍
- List:
- ArrayList/Vector: 底层实现为对数组的操作,有扩容机制,对查询相比链表要高效,对删除或者插入操作则需要移动该元素后面的数组元素,所以性能相对会比较低,还有如果是删除的后面元素则相对前面的元素复制的元素相对少.复制采用
public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
实现。ArrayList,线程不安全。Vector线程安全。都有扩容机制,扩容时需要做数组复制操作,调用System.arraycopy()
.注意不同版本JDK版本实现也不同,比如JDK1.9中ArrayList的add操作是插入到最后一个数组下标中,而set操作则将是替换元素的操作。注意说特点时要有特定的上下文. - LinkedList采用双向循环链表作为数据结构,内存空间不连续性,相比数组占用内存空间更多比如节点的前指针后指针等。还有因为是多个节点新增时需要新建节点对象删除需要销毁,所以增加了内存以及CPU资源以及GC的开销。典型的以
空间换时间的策略
,提供了某些经常做删除或者插入操作的优势。而想要删除就要找到元素,对应的查找方式如下:
- ArrayList/Vector: 底层实现为对数组的操作,有扩容机制,对查询相比链表要高效,对删除或者插入操作则需要移动该元素后面的数组元素,所以性能相对会比较低,还有如果是删除的后面元素则相对前面的元素复制的元素相对少.复制采用
public E remove(int index) { //给定下标进行删除
checkElementIndex(index);
return unlink(node(index));
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//给定对象进行删除操作
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
-
ForEache遍历性能低于普通Iterator迭代器,而For循环遍历通过随机访问遍历列表时,ArraylList表现很好,对LinkedList却无法接受,这是因为对LinkedList进行随机访问时,总会进行一次列表的遍历操作。ArrayList实现了RandomAccess接口,而LinkedList没有实现。集合遍历
注意
: 对ArrayList这些基于数组的实现来说,随机访问的速度是很快的,在遍历这些List对象时,可以有限考虑随机访问。但是对于LinkedList等基于链表的实现,随机访问性能非常差,避免使用。 - Map接口:主要的实现类Hashtable,HashMap,LinkedHashMap,TreeMap,在Hashtable的子类中还有Properties类的实现.
- HashMap,线程不安全,可以存在一个为Null的key
- HashTable,线程安全,不能存在为Null的key,对key的Hash算法到内存索引的映射算法不同。
-
HashMap实现原理: HashMap就是将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应得数据。在HashMap中,底层数据结构数组+单链表,为了解决单链表查询效率低的问题后续版本JDK引入了数组+红黑数(JKDK1.8单链表>8个时转化),所谓的内存地址即数组得下标索引。HashMap的高性能保证了hash算法的高效,hash值到内存地址的算法快速,根据内存地址可以直接取得对应的值。移位操作以及native方法保证了性能的高效。估算容器容量,避免多次rehash以及扩容(不同版本的JDK扩容机制不同,比如高版本的有限扩容底层数组到64,然后再把单链表转红黑树结构等等,最根本的原因就是为了高性能)设置合理的负载因子,默认HashMap大小16,负载因子0.75,当HashMap超过负载因子就会进行扩容操作,维护了一个threshold变量,被定义为当前数组总容量和负载因子的乘机,HashMap的阀值。负载因子越大,需要的内存空间越小但也越容易发生hash冲突,否则反之。因此需要一个可靠的hashCode()方法,HashMap的性能一定程度上取决于hashCode()的实现.还有一些注意点比如低版本计算size,采用连续计算2次,如果相等则正确,如果不相等则产生误差。高版本JDK采用volitail变量声明的变量,size值大小更加精准。
- LinkedHashMap-有序的HashMap,在其内部增加了一个链表,用以存放元素的顺序,因此可以理解为一个维护了元素次序表的HashMap,提供了两种类型的顺序,一是元素插入时顺序,二是最近访问的顺序。
- 注意:不要在迭代器模式中修改被迭代的集合。如果这么做,会抛出
ConcurrentModificationException
异常,这个特性适用于所有集合类,包括HashMap,Vector,ArrayList等 - TreeMap:排序方式与LinkedHashMap不同,LinkedHashMap是基于元素进入集合的顺序或者被访问的先后顺序排序的,而TreeMap是基于元素的固有顺序(由Comparator或者Comparable确定)
- Set接口:
- 不重复,内部实现为HashMap的一种封装。
- 优化集合访问代码
- 分离循环中被重复调用的代码.如下举例:
for (int i = 0; i < collection.size(); i++) {
//todo
}
//修改为如下
int size = collection.size();
for (int i = 0; i < size; i++) {
//todo
}
- 省略相同的操作
- 减少方法调用,改为直接访问元素会更高效。因为函数调用是需要消耗大量的系统资源的。
- RandomAccess接口:一个标志接口,表示支持快速随机访问的对象。主要目的是标识可以支持快速随机访问的List的实现,JDK中,任何基于数组的List实现都实现了RandomAccess接口,而对链表的的实现则没有,因为数组能够进行快速随机访问,而链表的随机访问需要进行链表的遍历。如下:
if (list instanceof RandomAccess) {
for (int i = 0, size = list.size(); i < size; i++) {
Object obj = list.get(i);//通过下标随机高效访问,底层为数组实现RandomAccess接口
//todo
}
} else {
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
//todo
}
}
在Java语言中使用NIO提高I/O性能摆脱最大堆束缚
- NIO是JDK1.4被纳入,特征:
- 为所有的原始类型提供Buffer缓存支持
- 使用Java.nio.charset.Charset作为字节集编码解决方案
- 增加通道Channel对象,作为新的原始I/O抽象
- 支持锁和内存映射文件的文件访问接口
- 提供基于Selector的异步网络I/O
- Buffer 3个重要参数:位置position, 容量capactiy,上限limit,分别有读写两种模式,读/写时position/limit会发生变化
- 使用MaxDirectMemorySize可以指定DirectBuffer的最大可用空间,DirectBuffer的缓存空间不再堆上分配,因此可以使应用程序突破最大堆的内存限制,堆DirectBuffer的读写操作比普通Buffer快,但是对它的创建和销毁却比普通Buffer慢。
在Java中的应用类型和使用方法
- 4个级别的引用:强引用,软引用,弱引用和虚引用
- 强引用:可以直接访问对象,不会被系统回收JVM宁愿抛出OOM异常,可能导致内存泄漏。
- 软引用:JVM会根据当前堆的使用情况来判断何时回收软引用对象,当堆使用情况临近阀值,才会回收软引用对象,若内存足够,软引用可能在内存中存活相当长一段时间,因此软引用可以用于实现对内存敏感的Cache
- 弱引用:只要系统GC时发现弱引用就会进行回收,无论堆空间是否足够。
- 虚引用:持有虚引用的对象和没有引用几乎一样,随时都可能被GC回收,当试图通过虚引用的get()取得强引用时,总是失败
一些有助于提高系统性能的技巧
- 慎用异常,比如循环体内使用try{}catch(Exception e){}
- 使用局部变量,局部变量都是保存在栈中,速度快,而静态变量,实例变量,都在堆中创建,速度慢。局部变量访问速度一般高于类的成员变量。
- 位运算代替乘除法
- 替换switch语句,比如数组可能可以是一种思路
- 一维数组代替二维数组
- 提取表达式,即重复工作抽出来做一次就可以
- 展开循环,例子如下:
int[] array = new int[99999];
for (int i = 0; i < 99999; i++) {
array[i] = i;
}
//展开循环
for (inti i = 0; i < 99999; i+=3) {
array[i] = i;
array[i+1] = i+1;
array[i+2] = i+2;
}
- 布尔运算代替位运算
- 数组复制使用
System.arraycopy(....)
- 使用Buffer进行I/O操作
使用clone()代替new
,clone()默认浅层拷贝- 静态方法替代实例方法
第4章 并行程序开发及优化
4.1 并行程序设计模式
Future
模式Master-Worker
模式是常用的并行模式之一,核心思想是,系统由两类进行协作,Master进行负责接收和分配任务,Worker进行负责处理子任务,当各个Worker进行将子任务处理完成后,将结果返回给Master进行,由Master进程做归纳和汇总,从而得到系统的最终结果。将一个大任务分解成若干个小任务,并行执行,提高系统吞吐量,对client来说异步处理,不会出现等待现象Guarded Suspension模式
意为保护暂停,其核心思想是仅当服务进程准好时,才提供服务。确保系统仅在有能力处理某个任务时,才处理该任务,当系统没有能力处理任务时,它将暂存任务信息比如队列或MQ中,等待系统空闲。起到消峰平谷的作用。不变模式
在并行软件开发过程中,同步操作似乎必不可少,当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步,而同步操作对系统性能有相当的损耗。为了能尽可能去除这些同步操作,提高并行程序性能,可以使用一种不变对象
,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性。核心思想就是,一个对象一旦被创建,则它的内部状态将永远不会发生改变,所以没有一个线程可以修改其内部状态和数据,基于这个特性,不变性对象在多线程环境中不需要进行同步控制。注意,不变模式比只读具有更强的一致性和不变性。对只读属性的对象而言,对象本身不能被其他线程修改,但是对象的自身状态却可能被自身自行修改。注意如下可以做到不变模式:- 去除setter方法及其所有修改自身属性的方法
- 将所有属性设置为私有并用final标记,确保不可修改
- 确保没有子类可以重载修改它的行为
- 有一个可以创建完整对象的构造函数
- 综上保证对象创建后其内部状态和数据不再发生变化,可以被共享被多线程频繁访问。比如JDK中的
java.lang.String/Boolean/Byte/Character/Double/Float/Integer/Long/Short
等保证多线程下的性能和吞吐量
生产者-消费者模式
:通过内存缓冲在多线程间共享数据,还可以缓解生产者和消费者间的性能差,同时也要注意是否差的太多。还可以进行解耦
优化系统整体结构,一定程度地缓解性能差对系统性能的影响。- JDK多任务执行框架:
Executor
框架,自定以线程池如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
//函数参数含义如下:
corePoolSize: 线程池核心线程数量
maximumPoolSize: 线程池最大线程数量
keepAliveTime: 当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程在多长时间内会被销毁
unit: keepAliveTime的单位
workQueue: 任务队列,被提交但尚未执行的任务
threadFactory: 线程工厂,用于创建线程,一般默认即可
handler: 拒绝策略,当任务太多,如何进行拒绝任务
- 一些注意点针对有界队列而言:
- 如果线程池的实际线程数小于corePoolSize则优先创建新的线程
- 若大于corePoolSize则会将新任务加入等待队列
- 若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的进程执行任务。
- 若大于maximumPoolSize则执行拒绝策略
- 可见有界队列仅当任务队列满时才可能将线程数量提升到corePoolSize以上,换言之除非系统非常繁忙,否则确保核心线程数维持在corePoolSize,队列未满先放队列,也更好利用线程达到线程的目的,如果先创建线程则会多GC,多线程间的切换,性能影响。如果是无界队列,则会存在内存耗尽情况,危险。以及还有优先级的队列
PriorityBlockingQueue
主要还是根据业务需要。多次测试,调corePoolSize,以及maximumPoolSize才能达到最优。
- 可见有界队列仅当任务队列满时才可能将线程数量提升到corePoolSize以上,换言之除非系统非常繁忙,否则确保核心线程数维持在corePoolSize,队列未满先放队列,也更好利用线程达到线程的目的,如果先创建线程则会多GC,多线程间的切换,性能影响。如果是无界队列,则会存在内存耗尽情况,危险。以及还有优先级的队列
- JDK内置拒绝策略:
- AbortPolicy:直接抛出异常,阻止系统正常工作
- CallerRunPolicy: 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务
- DiscardOledestPlicy: 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
- DiscardPolicy: 默默丢系无法处理的任务不予任何处理
- 可以自定义实现
RejectedExecutionHandler
接口
- 优化线程大小经验,一般与CPU数量,内存大小,JDBC连接等因素,如下:
- Ncpu=CPU数量; Ucpu=目标CPU使用率,0<=Ucpu<=1; W/C=等待时间与计算时间的比率
- 为保持处理器达到期望使用率,最优线程池大小等于:
Nthreads=Ncpu*Ucpu*(1+W/C)
,Ncpu=Runtime.getRuntime().availableProcessors()
获取CPU数量
- 扩展
ThreadPoolExecutor
JDK并发数据结构
- 并发List,
CopyOnWriteArrayList/Vector/Collections.synchronizedList(List list)
,在读多写少的高并发环境中优先使用CopyOnWriteArrayList
可提高系统性能吞吐量,在写多读少CopyOnWriteArrayList
的性能可能不如Vector
- 并发Set,
CopyOnWriteArraySet
场景同上 - 并发Map,
ConcurrentHashMap
分段锁 - 并发Queue,
ConcurrentLinkedQueue
- 并发Deque,
LinkedBlockingDeque
并发控制方法
- 内存模型与
volatile
- 工作内存执行操作:
use,assign,load,store
- 主内存执行的操作:
read,write,lock,unlock
都是原子操作 volatile
变量可以做以下保证:- 编译器保证其有序性,不保证原子性
- 强迫所有线程读写主内存,比如当前线程修改volatile变量后,首先写回主内存然后使其他线程的工作内存的该变量失效去重新读工作内存中最新的数据。
- volatile使用原因简单不会造成像锁一样造成阻塞,某些情形下性能优于锁,其使用规则:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中,只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。
- 比如,布尔值的状态标志,比如安全发布对象双重检查锁单例模式….等
synchronized
一般用法为锁对象或者同步块,为了有效地控制线程间的协作,需要配合使用synchronized
以及notify()
和wait(), notifyAll()
等方法ReentrantLock
重入锁,可中断可定时,分为公平和非公平两种锁,公平锁先进先出,非公平锁不保证可能插队,公平锁的实现代价比非公平锁大,因此性能上吞吐量上非公平锁要好很多,注意使用ReentrantLock
在finally
释放锁。重要方法:lock(), lockInterruptibly(), tryLock(), tryLock(long time, TimeUnit unit), unlock()....
在高并发锁竞争激烈的情况下,这些灵活的控制功能有助于应用程序在应用层根据合理的任务分配 来避免锁竞争,以提高应用程序的性能。ReadWriteLock
读写锁,有效减少锁竞争提升系统性能,使用场景是读多写少
因为写写操作和读写操作依然是需要相互等待和持有锁的。读操作越耗时读写锁优势越明显,读锁可以绝对并行。Condition
对象,Condition对象用于协调对线程间的复杂协作,核心方法如下:await(), awitUninterruptibly(), awaitNanos(lonog nanosTimeout), await(long time, TimeUnit unit), awaitUntil(Date deadline), signal(), signalAll()
可以对比notify(),notifyAll()
,更灵活,有助于提高吞吐量性能。Semaphore
信号量,是对锁的扩展,无论是内部锁synchronized
还是重入锁ReentrantLock
一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问某一个资源,信号量有以下核心方法:Semaphore(int permits, boolean fair), acquire(), acquireUninterruptibly(), tryAcquire(), tryAcquire(long timeout, TimeUnit unit), release()
信号量对锁概念进行了扩展,它可以限定对某一个具体资源的最大可访问线程数。ThreadLocal
线程局部变量,一种多线程间并发访问变量的解决方案,于synchronized
等加锁的方式不同,ThreadLocal
完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是一种数据共享的解决方案。从性能上说,ThreadLocal并不具有绝对的优势,在并发量不是很高时,也许加锁的性能更好,但作为一套与锁完全无关的线程安全解决方案,在高并发量或者锁竞争激烈的场合,使用ThreadLocal可以在一定程度上减少锁竞争,核心接口set(T value), get(), remove()
。ThreadLocal还可以作为过多调用链的传递参数,或者多个调用链的日志输出。核心代码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
“锁”的性能和优化
- 线程的开销,多线程除了处理功能需求外,还需要额外维护多线程环境的特有信息,比如线程本身的元数据,线程的调度,线程上下文的切换,单核CPU上,并行算法效率一般低于串行算法。而在多核时代,合理的并发才能将多核CPU的性能发挥到极致
- 避免死锁,发生死锁的条件如下:
- 互斥条件:一个资源每次只能被一个进行使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相连接的循环等待资源关系
- 综上只要破坏4个必要条件中的任何一个,就解决了死锁问题。
- 减小锁持有时间,有助于降低锁冲突的可能性,进而提升体统的并发能力。
- 减小锁粒度,是一种削弱多线程锁竞争的一种有效手段,典型
ConcurrentHashMap
锁分段技术即拆分锁对象技术,默认拥有16个线程的并行,大大增加吞吐量,减少锁粒度会引入一个新的问题,即当系统需要取得全局锁时,其消耗的资源会比较多,比如put()方法做到了很好的分离锁,但是试图访问ConcurrentHashMap全局信息时,就需要同时取得所有段的锁方能顺利实施,比如size()方法就需要同时取得所有段的锁方能顺利实施,size()部分代码如下:注意JDK版本最新版本是Node节点
sum = 0;
for (int i = 0; i < segments.length; ++i)
segments[i].lock(); //对所有段加锁
for (int i = 0; i < segments.length; ++i)
sum += segments[i].count; // 统计总数
for (int i = 0; i < segments.length; ++i)
segments[i].unlock(); //释放所有锁
- 读写分离锁来替换独占锁,在读多写少的场合,使用读写锁可以有效提升系统的并发能力
— | 读锁 | 写锁 |
---|---|---|
读锁 | 可访问 | 不可访问 |
写锁 | 不可访问 | 不可访问 |
- 锁分离,根据读写操作功能上的不同,进行了有效的锁分离,可以达到增加吞吐量提高性能的目的,提高并发量。
- 重入锁
ReentrantLock
和内部锁synchronized
:synchronized使用简单,可以自动释放锁。而ReentrantLock需要主动显示在finally中进行释放,在高并发的情况,synchronized性能略低于ReentrantLock,随着JVM对内部锁synchronized的更多的优化性能会越来越好,重入锁ReentrantLock功能更强大也更复杂比如提供锁等待时间tryLock(long time, TimeUnit unit),支持锁中断处理lockInterruptibly()和快速锁轮训tryLock()等,这些技术能有助于避免死锁的产生,从而提高系统的吞吐量性能以及稳定性,当死锁发生时,可以在有限的时间内释放资源,从而避免系统资源耗尽甚至发生宕机。同时重入锁还有一套Condition
机制,await(), awaitNanos(…),signal(),signalAll()对比Object的wait(),notify()的实现。在并发不是很大的情况下,应优先内部锁实现. - 锁粗化(Lock Coarsenging):指对一连串连续的对同一个锁不断进行资源请求和释放的操作时,应该把所有对锁的请求整合成对锁的一次请求,从而减少对锁的请求同步次数,这种操作叫锁的粗化。尤其比如循环的时候,如下:粗化与细化思想和减少锁持有时间相反,不同的场景效果不同,需要根据实际情况自行权衡。
for (int i = 0; i < 10; i++) {
synchronized(lockObject) {
//todo
}
}
//粗化
synchronized(lockObject) {
for (int i = 0; i < 10; i++) {
//todo
}
}
- 自旋锁(Spinning Lock): 为了解决多线程的间状态和上下文切换消耗系统资源,比如当访问共享资源仅需花费很小一段CPU时间片时,可能比花费的系统线程切换状态转化时间还短,这样做线程间的切换就显得重量级了是不值得的。为此JVM引入自旋锁,
自旋锁
可以使线程在没有取得锁时,不被挂起,而时做空循环所谓自旋锁,在若干个空循环后,如果获得锁则继续执行,若线程依然不能获得锁,才会被挂起,这样线程挂起几率相对少,线程执行的连贯性相对加强,因此对于锁竞争不是很激烈,锁占用时间很短的并发线程,具有积极意义。但对于锁竞争激烈,锁占用时间长的并发程序,自旋锁往往无法获得锁白白浪费CPU时间片,最终还是被挂起,浪费系统资源,JVM提供了-XX:+UseSpinning
开启自旋锁,-XX:PreBlockSpin
参数设置自旋锁的等待次数。 - 锁消除
Lock Elimination
,指JVM在即时编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种JVM优化可以节省毫无意义的请求锁时间。举例:
protected String createStringBuffer(String a, String b) {
StringBuffer sb = new StringBuffer(); //局部变量没有逃逸出这个方法,但StringBuffer是线程安全的有加锁,此处JVM就进行锁消除优化
sb.append(a); //append()有锁申请在当前环境中是不必要的
sp.append(b);
return sb.toString();
}
- 逃逸分析和锁消除分别使用JVM参数
-XX:+DoEscapeAnalysis
-XX:+EliminateLocks
开启(注意锁消除必须工作在-server模式下) - 偏向锁(Biased Lock):核心思想如果程序没有竞争,则取消之前已经取得锁的线程同步操作,也就是若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省操作时间,如果在此之间有其他线程进行了锁请求,则锁退出偏向模式,在JVM中使用
-XX:+UseBiasedLocking
启用偏向锁,这种模式在锁竞争激烈的场合没有优化效果,因为大量的锁竞争导致持有锁的线程不停切换,锁也很难一直保持在偏向模式,此时使用偏向锁不仅得不到性能优化,反而有损系统性能,反而在锁竞争激烈的场合下使用-XX:-UseBiasedLocking
参数禁用偏向锁能提升系统吞吐量
无锁的并行计算
- 非阻塞的同步/无锁:比如
ThreadLocal
就是一种简单的非阻塞同步,每个线程拥有各自独立的变量副本,因此在并行计算时无需相互等待。再比如CAS(Compare And Swap)
的无锁并发控制算法,无锁算法设计实现相对阻塞复杂的多,但无锁具有对死锁天生免疫,并且线程间相互影响也远远比基于锁的方式要小,无锁方式不存在锁竞争的系统开销,也没有线程间频繁调度切换带来的开销,因此比基于锁方式拥有更优越的性能。
/**
* Atomically updates Java variable to {@code x} if it is currently
* holding {@code expected}.
*
* <p>This operation has memory semantics of a {@code volatile} read
* and write. Corresponds to C11 atomic_compare_exchange_strong.
*
* @return {@code true} if successful
*/
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
- CAS(V,E,N):V,要更新的值,E预期值,N新值,仅当V等于E值才会将V值设为N,如果V和E值不同则说明已经有其他线程做了更新,则当前线程什么也不做。最后CAS返回当前V的真实值,CAS操作是抱着乐观的态度进行的,总认为自己可以成功完成操作,当多个线程并发时只有一个会成功并成功更新值,失败的继续尝试竞争,基于这样的原理,CAS没有锁也可发现其他线程对当前线程的影响并进行恰当的处理,如上代码所示,JDK已从硬件层面支持原子化的CAS指令。
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
//JDK1.8之后
@HotSpotIntrinsicCandidate
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, newValue));
return v;
}
Amino
框架介绍,简介使用Amino进行软件开发的优势:- 对死锁问题免疫
- 确保系统整体进度
- 减少高并发无锁竞争带来性能开销
- 可以轻易使用一些成熟的无锁结构,无需自研
协程
Kilim
框架,使用协程可以让系统以更低的成本,支撑更高的并行度
总结
第5章 JVM调优
Java虚拟机内存模型
- JVM虚拟机将内存数据分为程序计数器,虚拟机栈,本地方法栈,Java堆,方法区等部分,程序计数器用于存放下一条运行的指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息,Java堆存放Java程序运行时所需的对象等,方法区存放程序类元数据信息。
- 程序计数器: 线程私有,是一块很小内存空间记录当前线程下一条要运行的指令,各个线程之间的计数器相互不影响独立工作。若执行的Java方法则记录正在执行的Java字节码地址,如果当前线程执行的是Native方法,则程序计数器为空。
- 虚拟机栈:线程私有内存空间,和线程一同创建保存方法的局部变量,部分结果,并参与方法的调用和返回。
-Xss
设置栈的大小其决定了函数可达深度。如果线程在计算过程中,请求的栈深度大于最大可用的栈深度抛出StackOverflowError
; 如果Java栈可动态扩展在扩展栈过程中,没有足够的内存空间来支持栈的扩展,则抛出OutOfMemoryError
; 虚拟机栈运行时使用一种叫栈帧数据结构保存上下文数据。
栈帧结构: 局部变量表; 操作数栈; 动态连接方法; 返回地址;
- 函数嵌套调用的次数由栈的大小决定。栈越大,函数嵌套调用次数越多,对一个函数而言,参数越多,内部局部变量越多,其嵌套调用次数越少,对GC也有一定的影响,局部变量表中的字可能会影响GC回收如果这个字没有被后续代码复用那么它所引用的对象不会被GC释放。分析工具
jclasslib
- 本地方法栈: 与虚拟机栈类似用于管理本地方法的调用,只是采用的是C语言实现的。
- Java堆:线程共享; 分为: eden, survivor(from to), tenured;
- 方法区:线程共享; 主要保存的信息是类的元数据比如,类的类型信息,常量池,域信息,方法信息。
- 类型信息包括类的完整名称,父类完整名称类型修饰符(public/protected/private)和类型的直接接口类表;
- 常量池:类方法,域等信息所引用的常量信息;
- 域信息:域名称,域类型,域修饰符;
- 方法信息: 方法名称,返回类型,方法参数,方法修饰符,方法字节码,操作数栈和方法帧栈的局部变量区大小以及异常表。方法区保持的信息,大部分来自class文件,是Java运行必不可少的数据。也叫永久区,是独立于Java堆的内存空间,可能也被GC回收,通常从两方面分析1 GC对永久区常量池的回收 2 永久区对类元数据的回收。
JVM内存分配参数
JVM参数 | 名称 |
---|---|
-Xmx | 最大堆内存(新生代+老年代) |
-Xms | 最小堆内存,JVM启动占据内存大小,如果过小则增加MinorGC和FullGC次数,对系统性能产生影响 |
-Xmn | 新生代大小,由于堆内存=新生代+老年代,所以新生代大老年代则小,对系统GC影响较大,一般新生代大小为堆的1/4到1/3左右,-XX:NewSize :设置新生代初始化大小;-XX:MaxNewSize :新生代最大值;设置了-Xmn 等于设置了相同的-XX:NewSize =-XX:MaxNewSize ,如果-XX:NewSize!=-XX:MaxNewSize可能导致内存震荡从而产生不必要的系统开销 |
-XX:MaxPermSize | 持久代(指方法区不是堆,决定了系统可以支持多少个类定义和常量)最大值 |
-XX:PermSize | 持久代初始化大小 |
-Xss | 线程栈大小,若过大则消耗内存系统支持的线程总数下降,若过小则可能没有足够的空间分配局部变量或者达不到足够的函数调用深度,导致程序异常退出 |
-XX:SurvivorRation=eden/s0=eden/s1 | 设置堆比例分配 |
-XX:NewRatio | =老年代/新生代 |
-XX:PrintGCDetails | 打印堆的实际大小 |
垃圾收集器基础
- 那些对象需要回收?
- 何时收回这些对象?
- 如何收回这些对象?
- 垃圾回收算法与思想:
- 引用计数法: 有引用➕1,引用消失减1,为0则可回收.无法处理循环引用问题。优点高效。
- 可达性分析算法:GC Roots对象为起点向下搜索所走过的路径称为引用链,当一个对象不存在这样一条链则可回收。可作为GC Roots对象包括但不限:虚拟机栈(栈帧的本地变量表)中引用的对象;方法区中类静态属性应用的一项或者常量引用的对象;本地方法栈中JNI引用的对象…
- 标记-清除算法:优点,高效简单,缺点,造成不连续的内存碎片。
- 复制算法: 内存分为两块,每次只用其中一块,将存活的对象复制到另一块完成垃圾回收。优点高效,不存在内存碎片。缺点内存折半,若存活对象数量太大则复制量也会很大需要,不适用老年代。此为新生代算法,新生代对象朝生夕死,对象创建在eden,from,to来回复制
- 标记-压缩:老年代回收算法,对象特点存活多复制成本高,优点,不用两块相同内存空间压缩又避免了产生内存碎片因此性价比较高。
- 增量算法:垃圾回收过程中系统处于
Stop the World
状态, 此算法指将一次处理的垃圾分成多次执行,减少一次执行系统长时间的耗时停顿,从而减轻对用户体验的影响。但多次执行GC,则线程上下文切换消耗系统资源,会使得GC总体成本上升,造成系统总吞吐量下降。 - 分代采用适合各自的GC算法,新生代-复制; 老年代-标记压缩;
垃圾收集器的类型
分类 | 垃圾收集器类型 |
---|---|
线程数 | 串行 1个线程; 并行 >=2个线程多核可缩短GC停顿时间 |
工作模式 | 并发垃圾收集器 ,与应用程序交替执行,尽可能减少应用程序的停顿时间; 独占式 (Stop the World) |
碎片处理 | 压缩GC ,非压缩GC |
分代 | 新生代GC , 老年代GC |
- 评价GC策略指标
指标 | 含义 |
---|---|
吞吐量 | 指应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值(系统总运行时间=应用程序耗时+GC耗时)。如果系统运行了100min,GC耗时1min,那么吞吐量就是(100-1)/100=99% |
垃圾收集器负载 | 和吞吐量相反,指垃圾回收器耗时与系统运行总时间的比值 |
停顿时间 | 指GC运行时应用程序的停顿时间。独占式STW时间较长;并发则每次停顿时间短,总耗时不一定,但由于会做多次涉及线程上下文切换等会消耗系统资源降低系统吞吐量。 |
垃圾回收频率 | 指多久会进行一次GC,一般来说频率越低越好,通常增加堆可以有效降低GC频率,但会增加GC停顿耗时以及内存空间 |
反应时间 | 指一个对象成为垃圾后多久它所占用的空间会被释放 |
堆分配 | 良好的垃圾收集器应该有合理的堆内存区间划分 |
- 新生代串行收集器,特点: 单线程发生STW独占式;优点:高效不存在线程间的切换开销。
-XX:+UseSerialGC
Client模式下是默认的垃圾收集器 - 新生代并行回收器关注系统吞吐量,可以通过
-XX:MaxGCPauseMillis
和-XX:GCTimeRatio
设置期望停顿时间和吞吐大小 - CMS收集器关注系统停顿时间是标记-清除算法使用多线程并行回收的垃圾收集器。会和应用程序并发执行线程间相互抢占CPU对吞吐量产生一定影响,
-XX:ParallelCMSThreads
设置收集器线程数量 - G1收集器: 标记-压缩算法
-XX:+UseG1GC; -XX:MaxGCPauseMillis=30; -XX:GCPauseIntervalMillis=200
常用的调优案例和方法
- 将对象预留在新生代,FullGC成本远远高于MinorGC,预留年轻代有助于整体GC效率
- 大对象进入老年代,开发中应尽可能避免短命的大对象.
-XX:PretenureSizeThreshold
设置大对象进入老年代的阀值,当对象的大小查过这个值将直接在老年代分配,注意只对串行和新生代并行收集器有效,并行收集器不识别 - 设置对象进入老年代的年龄
-XX:MaxTenuringThreshold
默认15; - 稳定与震荡的堆大小: 设置
-Xms=-Xmx
相同可减少GC次数。小堆可加快单次GC速度 - 吞吐量优先,应减少系统GC停顿总时间,考虑关注系统吞吐量的并行回收器;
实用JVM参数
- JVM的JIT(Just-In-Time)编译器可以运行时将字节码编译成本地代码从而提高函数的执行效率
-XX:CompileThreshold
为JIT编译的阀值. - 堆快照
-XX:+HeapDumpOnOutOfMemoryError
发生OOM时可导出堆快照-XX:HeapDumpPath
指定堆快照保存位置 -XX:+TraceClassLoading
跟踪类加载情况-XX:-UseSplitVerifier
关闭类校验器-XX:UseLargePages
启用大页-XX:LargePageSizeInBytes
指定页大小-XX:+UseCompressedOops
指针压缩,可以压缩如下指针:- Class的属性指针(静态成员变量)
- 对象属性指针
- 普通对象数组的每个元素指针
- 注意:虽然压缩指针可以节省内存但压缩和解压缩指针也会对JVM造成一定的性能损失
实战JVM调优
Tomcat
- 在catalina.bat写入看日志
set CATALINA_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails
set CATALINA_OPTS=%CATALINA_OPTS% -Xmx32M -Xms32M
第6章 Java性能调优工具
Linux命令行工具
top
从宏观看各个进程对CPU的占用情况以及内存使用情况sar
性能监测工具之一,周期性地对I/O,内存和CPU使用情况进行采集vmstat
看内存,交互分区,I/O,上下文切换,时钟中断以及CPU的使用情况iostat
详尽I/O信息pidstat
查看进程,线程,I/O信息,CPU,内存资源
Windows工具
- 任务管理器
- Process Explorer
- pslist命令
JDK命令行工具
jps
只列Java进程jstat
可查看运行时信息,堆详尽信息GC信息等jinfo
查看正在运行的Java应用程序的扩展参数,甚至运行时修改部分的参数.jmap
生成java应用程序的堆快照和对象的统计信息
jmap -dump:format=b,file=path\heap.hprof pid
jhat
分析堆快照内容jstack
导出应用程序的线程堆栈jstack [-l] <pid>
jstack -l pid > path\filename
jstatd
RMI服务端程序,相当于代理服务器建立本地计算机与远程监控工具的通信hprof
监控各个函数的运行时间,导出堆内容
JConsole工具
Visual VM多合一工具
MAT 内存分析工具 (Memory Analyzer) 堆内存分析器
JProfile
-
QQL
-
20180220051428