这是一本实践过的人写的书,在书中能学到作者的实践成果,测试结果都以数字来说明参数的作用。 在原来你认为已经掌握的基础上,提出一些新的问题和挑战。 从设计,到JVM,以及怎么实现性能的监控。 同时能学习到一些全新的知识点。每节后的注意点会再次提出需要特别关注的重点。 其实记录下来,即是对当前学习的一个总结,也是以后温习的好资料。在看到Linux性能监控的Top命令的使用特别有这种感觉。
Java性能调优概述
性能
- 客户端性能: 用户体验。界面停顿,抖动,响应迟钝
服务端性能: 响应时间,吞吐量
程序的性能表现:
- 执行速度: 程序的反应是否迅速,响应时间是否足够短
- 内存分配: 内存分配是否合理,是否过多地消耗内存或存在泄露
- 启动时间: 程序从运行到可以正常处理业务花费多长时间
- 负载承受能力: 当系统压力上升时,系统的执行速度,响应时间的上升曲线是否平缓
定量评测性能指标:
- 执行时间: 一段代码从开始到运行结束,所使用的时间
- CPU时间: 函数或者线程占用CPU的时间
- 内存分配: 程序在运行时占用的内存空间
- 磁盘吞吐量: 秒速I/O的使用情况
- 网络吞吐量: 秒速网络的使用情况
- 响应时间: 系统对某用户性能或时间做出响应的时间。响应时间越短,性能越好。
木桶原理(短板理论,取决于最差的组件): 对系统中表现最差的组件进行优化。
- 磁盘I/O
- 网络操作
- CPU
- 异常,对Java应用来说,异常的捕获和处理是非常消耗资源的。如果程序搞频率地进行异常处理,则整体性能便会有明显下降。
- 数据库
- 锁竞争,对高并发程序来说,如果存在激烈的锁竞争,无疑是对性能极大地打击。锁竞争将会明显增加线程上下文切换的开销。而且,这些开销是与应用需求无关的系统开销,白白占用宝贵的CPU资源,却不带来任何好处。
- 内存
Amdahl定律
仅提高CPU数量而不降低程序的串行化比重,也无法提高系统性能。
串行系统并行化后加速比的计算公式:
1 2 3 4 5 6 |
|
当N趋于无穷大,有speedup=1/F
,当任务(5个步骤)中有2个可以并行,即F=0.6
,故有speedup=1.67
,由此可见,为了提供系统的速度,仅增加CPU处理器的数量并不一定能起到有效的作用,需要从根本上个修改程序的串行行为,提供系统内可并行化的模块比重,在此基础上,合理增加并行处理器数量,才能以最小的投入,得到最大的加速比。
性能调优层次(整体上提升系统的性能)
设计调优(宏观层面的质)
- 在软件开发之初,软件架构师师就应该评估系统存在的各种潜在问题,并给出合理的设计方案。设计人员必须熟悉常用的软件设计方法,设计模式,基本性能组件和常用优化思想。设计优化的一大显著特点是,可以规避某一个组件的性能问题,从而改良该组件的实现。比如,系统组件A需要等待某事件E才能触发一个行为。如果组件A通过循环监控不断监测事件E是否发生,其监测行为必然会占用部分系统资源,因此,开发人员必然在监测频率和资源消耗间取得平衡。如果监测频率太低,虽然减少了资源消耗,但是系统实时反应性就会降低。如果能进行代码层的调优,就需要优化监测方法的实现以及求得一个最为恰当的监测频率。事件通知的方式将系统行为进行倒置(观察者模式)。从某种程度上说,设计优化直接决定了系统的整体品质。开发人员必须在软件设计之初,认真仔细考虑软件系统的性能问题。进行设计优化时,设计人员必须熟悉常用的软件设计方法、设计模式、基本性能组件和常用优化思想,并将其有机地集成在软件系统中。尽可能多花些时间在系统设计上,是创建高性能程序的关键。
- 选用合理的软件结果和性能组件。
代码调优(微观层面上的量,但却是对系统性能产生最直接影响的优化/改进方法)
- 涉及诸多编码技巧,需要开发人员熟悉相关语言的API,并在合适的场景中正确使用相关API或类库。同时对算法、数据结构的灵活使用,也是代码优化的重要内容。
- 提高代码的执行效率。一个好的实现和一个坏的实现对系统的影响也是非常大的。如ArrayList和LinkedList随机访问上的性能;同样文件读写,使用Stream方式与JavaNIO的方式性能可能又会相差一个数量级别。
JVM调优(可以在软件开发后期进行)
- JVM的堆大小,垃圾回收策略,堆内存结构,GC的种类
- 合理设置JVM虚拟机参数。需要开发人员对JVM的运行原理和基本内存结构有一定了解。如堆内存、GC的种类等。
数据库优化
- 在应用层对SQL语句进行优化 涉及大量的编程技巧。如PreparedStatement代替Statement,Select语句中显示指定要查询的列名避免使用星号*,
- 对数据库进行优化 建立一个具有良好表结构的数据库。为了提高多表级联查询效率,可以合理使用冗余字段;对于大表,可以使用行的水平切割或者类似Oracle的分区表技术;建立有效且合理的索引。
- 对数据库软件进行优化 设置合理的共享池,缓存缓冲区或者PGA,对Oracle的运行性能都有很大的影响。
操作系统优化 对于主流Unix系统,共享内存段、信号量、共享内存最大值(shmmax)、共享内存最小值(shmmin)等都是可以进行优化的系统资源。此外,如最大文件句柄数、虚拟内存,磁盘块大小等参数。
基本调优策略和手段(找到性能瓶颈,对症下药)
- 确认对性能优化的性能目标、方法进行统筹安排。首先需要有明确的性能目标,清楚地指出优化的对象和最终目标。其次,需要在目标平台上对软件进行测试,通过各种性能监控和统计工具,观测和确认当前系统是否已经达到相关目标,若已经达到,则没有必要再进行优化;若当前系统性能尚未达到优化目标,则需要查找当前的性能瓶颈(磁盘IO,网络IO和CPU)。查到性能瓶颈后,首先需要定位相关代码,确认是否在软件实现上存在问题或者优化空间。若有,则进行代码优化,若已经没有代码优化空间,则需要考虑进行JVM层,数据库或者操作系统的优化。甚至,可以考虑修改原有设计、或者提升硬件性能。以此反复。
- 调优必须明确已经问题和性能目标。有明确的目标,不要为了调优而调优,如果没有明显的性能问题,盲目地进行调整,其风险可能远远大于收益。任何优化都是为了解决具体的软件问题。在性能问题暴露之前,只是凭主观臆断对某些模块进行性能改进,从软件规范化开发的角度上来说,是非常冒险的。因为修改后的新代码没有经过完整的测试,软件质量就没有保障。而且,优化后的性能提升幅度可能也不足以让开发者如此费尽心机。因此,在进行软件优化后,必须要进行慎重的评估。
- 可维护性与维护性取得平衡。
设计优化
设计模式是前人工作的总结和提炼。对某一特定问题的成熟的解决方案。 如能合理的使用设计模式,不仅能是系统更容易被他人理解,同时也能是系统拥有更合理的结构。
单例模式
确保系统中一个类只产生一个实例。 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是非常可观的一笔系统消耗。 由于new操作的次数减少,因而对系统内存的使用屏幕也会降低,这将降低GC压力,缩短GC停顿的时间。 对于系统的关键组件和被频繁使用的对象,使用单例可以有效地改善系统的性能。
- 4个特点 final class, private construtor, private static instance, public static method
- 还有就是lazy load。
- synchronized(静态方法同步)
- 两次判断if(null) -> synchronized -> if(null)
- 内部类静态变量(JVM保证类加载对多线程友好)
由于instance成员变量时static定义的,因此在JVM加载单例类时,单例对象将就会被创建。如果此时,这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方就会初始化这个单例变量,而不管是否被用到。所以职责单一也是开发过程中经常被提起的!
1 2 3 4 5 6 7 8 9 10 11 |
|
在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会被初始化,故可以确保当StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好,getInstance()方法也不需要使用同步关键字。因此,这种实现方式同时兼容以上两种实现的优点。
使用内部类的方式实现单例,既可以做到延迟加载,也不必使用同步关键字,是一种比较完善的实现。
**通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然有例外情况,可能导致系统生成多个实例,比如,在代码中,通过反射机制,强行调用单例类的私有构造函数,生成多个单例。 (这也是我很欣赏这本书的地方,对一个问题进行全面的剖析!)
单例序列化反序列化要注意(情况比较少,但是如果有一定要特别注意),这要自定义一个readReslove!!在实现了readResolve方法后,readObject已经形同虚设,它直接使用readResolve替换了原来的返回值。(如果功能职责单一的话,就不存在这个问题了!)P14
1 2 3 4 5 6 7 |
|
代理模式
使用代理对象完成用户请求,屏蔽用户对真是对象的访问。代理人被授权执行当事人的一些事宜,而无需当事人出面。从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上,代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。
考虑安全因素的安全代理,远程调用网络代理,延迟加载来提升系统性能和反应速度,改善用户体验。灵活性。
- 主题接口。定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法
- 真实主题。真正实现业务逻辑的类。
- 代理类。用来代理和封装真实主题。
- Main。客户端,使用代理类和主题接口完成一些工作。
在系统启动时,将消耗资源最多的方法都使用代理模式分离,就可以加快系统的启动速度,减少用户的等待时间。而在用户真正做查询操作时,再由代理类,单独去加载真实的处理类,完成用户的请求。
延迟加载只是代理模式的一种应用场景。用于远程调用的网络代理,考虑安全因素的安全代理。 延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象代替他的原有的位置,只要在真正需要使用的时刻,才对他进行加载。 可以在时间轴上分散系统压力,尤其系统启动时,不必完成所有的初始化工作,从而加速启动时间;其次,对很多真实主题而言,在软件启动直到关闭的整个过程中,可能根本不会被调用,初始化这些数据无疑是一种资源浪费。启动时只需要初始化一个轻量级的对象。
- 动态代理
运行时动态生成代理类。即代理类的字节码将在运行时生成并载入当前的ClassLoader。不需要为真实主题写一个形式上完全一致的封装类。使用动态单例的生成方法甚至可以在运行时指定代理类的执行逻辑,从而大大提升系统的灵活性。
* JDK自带的动态代理: Proxy.newProxyInstance(classloader, interfaces, invocationhandler)
* *CGLIB: Enhancer#setCallback(MethodInterceptor),#setInterfaces(interfaces), #setSuperClass(superClass), #create()
* DefaultGeneratorStrategy.Generate(), ReflectUtils.defineClass()/newInstance()
* *Javaassist: ProxyFactory#setInterfaces(interfaces), #createClass(), #newInstance(), #setHandler(MethodHandler)
* 代理工厂MethodHandler; ProxyFactory#setInterfaces,#createClass,#newInstance,#setHandler
* 动态代码创建 ClassPool, CtClass, CtField, CtNewMethod。P20
* ASM 低级字节码生成工具,近乎使用Java bytecode编程,当然也是性能最好的一种动态代理生成工具。使用ASM过于繁琐,而且性能也没有数量级的提升。维护性差,对开发人员要求高
与静态代理相比,动态代理可以很大幅度地减少代码行数,并提升系统的灵活性。甚至可以在运行时生成业务逻辑。 在Java中,动态代理的生成主要涉及对ClassLoader的使用。根据指定的回调类生成Class字节码,通过defineClass()将字节码定义为类,使用反射机制生成该类的实例。
就动态代理的方法调用性能而言,CGLIB和Javassist的基于动态代码的代理都优于JDK自带的动态代理。JDK的动态代理要求代理类和真实主题都实现同一个接口,而CGLIB和Javassist没有强制要求。
Hibernate中代理的使用。P22 * 属性的延迟加载 * 关联表的延时加载
享元模式
核心思想是:如果在一个系统中存在多个相同的对象,那么只需共享一份对象的拷贝,而不必为每一个使用都创建新的对象。在享元模式中,由于需要构造和维护这些可以共享的对象,因此,常常会出现一个工厂类,用于维护和创建对象。
- 节省重复创建对象的开销。因为被享元模式维护的对象只会被创建一次,当创建对象比较耗时,便可以节省大量的时间。
创建对象的数量减少,所以对系统内存的需要也较少了,使GC的压力也相应地降低,进而使得系统拥有一个健康的内存结构和更快的反应速度。
享元工厂: 用以创建具体享元类,维护相同的享元对象。它保证相同的享元对象可以被系统共享。即期内部使用了类似单例模式的算法,当去请求独享已经存在时,直接返回对象,不存在时,在创建对象。
- 抽象享元:定义需要共享的对象的业务接口。享元类被创建出来总是为了实现某些特定的业务逻辑,而抽象享元便定义了这些逻辑的语义行为
- 具体享元类:实现抽象享元类的接口,完成某一具体逻辑。
- Main:使用享元模式的组件,通过享元工厂取得享元对象。
享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象,一般情况下,享元工厂会维护一个对象列表,当任何组件尝试获取享元时,如果请求的享元类已经被创建,则直接返回已有的享元类;若没有,则创建一个新的享元对象,并将它加入到维护队列中。
主要作用就是复用大对象(重复级对象),以节省内存空间和对象创建时间。 在一个对象池中,所有的对象都是等价的,任何两个对象在任何使用场景中都可以被对象池中国的其他对象代替。 而在享元模式中,享元工厂所维护的所有对象都是不同的,任何两个对象不能相互代替。
1
|
|
装饰者模式
动态*添加*对象功能。合成/聚合复用原则,代码复用应该尽可能使用委托,而不是继承。因为继承是一种紧密耦合,任何父类的改变都会影响起子类,不利于维护。而委托则是松散耦合,只要接口不变,委托类的改变并不会影响其上层对象。
将功能组件*叠加*,从而构造一个“超级对象”,使其拥有所有这些组件的功能。而各子功能模块,被很好地维护在各个组件的相关类中,拥有整洁的系统结构。
这种结构可以很好的讲功能组件和性能组件进行分离,彼此互不影响,并在需要的时刻,有机地结合起来。从而提升模块的可维护性并增加模块的复用性。
装饰者和被装饰者拥有相同的接口,被装饰者是系统的核心组件,完成特定的功能目标。而被装饰者则可以坐在被装饰者的方法前后,加上特定的前置处理和后置处理,增强被装饰者的功能。
无需将所有的逻辑,即,核心内容构建,HTML文件构造和HTTP头生成等3个功能模块粘合在一起实现。
通过装饰者的构造函数把被装饰者对象传入。
Component,Decorator(#component)
Buffer I/O 将性能模块和功能模块分离的一种典型实现。
问题:装饰者的前后顺序有什么影响呢?DataOutputStream(BufferedOutputStream(FileOutputStream)与BufferedOutputStream(DataOutputStream(FileOutputStream)?
观察者模式
当一个对象的行为依赖于另一个对象的状态!可以在单线程中,使某一个对象,及时得知自身所依赖的状态的变化。
ISubject,IObserer
用于事件监听,通知发布等场合,可以确保观察者在不使用轮询监控的情况下,及时收到相关消息和事件。
这个不细讲了,只要写过Java客户端图形界面的都知道这个,不管是awt,swing还是SWT!按钮的监听器等等。
ValueObject模式(一次性返回一个整体的对象)
提倡将一个对象的各个属性进行封装,将封装后的对象在网络中传递,从而是系统拥有更好的交互模型,并且减少网络通信数据,从而提高系统性能。
也使系统接口具有更好的可维护性。
RMI:
public class OrderMangerServer {
try{
LocateRegistry.createRegistry(1099);
IOrderManager userManager = new OrderManager();
Naming.rebind("OrderManager", userManager);
System.out.println("OrderManager is ready.");
}catch(Exception e){}
}
client:
IOrderManager userManager = (IOrderManager)Naming.lookup("OrderManager");
业务代理模式Delegate
将*一组*有远程方法调用构成的业务流程,*封装*在一个位于展示层的代理类中。
基于远程调用封装业务逻辑,提供缓冲功能。将一些业务流程封装在前台系统,为系统性能优化提供了基础平台。在业务代理中,不仅可以复用业务流程,还可以视情况为展示层组件提供缓存等功能,从而减少远程方法调用次数,减低系统压力。
缓冲Buffer
缓解应用程序上下层之间的性能差异,提高系统的性能。上层系统完成工作,可以去处理其他业务逻辑。而此时,水并未完全进入瓶中,而大部分被积累在漏斗中。这就可以由下层慢慢处理,直到水完全进入瓶中,漏斗(缓冲区)被清空。
缓冲可以协调上层组件和下层组件的性能差。当上层组件性能优于下层组件,可以有效减少上层组件对下层组件的等待时间。上层应用组件不需要等待下层组件真实地接受全部数据,即可返回操作,加快了上层组件的处理速度,从而提升系统整体性能。
I/O, 动画显示效果, 生产者消费者(上下层组件的通信工具)
缓存Cache
为提升系统性能而开辟的内存空间。暂存数据处理结果或者来自不易的数据,并提供下次访问使用。
比较好的实现使用WeakHashMap。
EHCache,OSCache, JBossCache
频繁使用且重负载的函数中实现加入缓存。直接编码加入缓存紧密耦合依赖性强。->动态代理的缓存解决方案!
浏览器本地缓存。
池
如果一个类被频繁请求使用,那么不必每次都生成一个实例,可以将这个类的一些实例保存在一个“池”中,待需要使用的时刻直接从池中获取。
对于那些经常使用,并且创建很费时的大型对象来说,使用对象池维护,不仅可以节省获得对象实例的成本,还可以减轻GC频繁回收这些对象产生的系统压力。
但对于生成对象开销很小的对象进行池化,反而可能得不偿失,维护对象池的成本可能会大于对象池带来的好处。
在JDK中,new操作的效率是相当高的,不需要担心频率的new操作对象系统有性能影响。但是new操作时所调用的类构造函数可能是非常费时的,对于这些对象,可以考虑池化。
线程池,数据库连接池(C3P0和Proxool) <- Proxy
com.mchange.v2.c3p0.impl.NewProxyConnection
Jakarta Common Pool
ObjectPool, PoolableObjectFactory
并行代替串行
Thread, Runnable, java.util.concurrent, 各种同步工具
负载均衡 (Terracotta服务器)
Tomcat集群,使用Apache服务器作为负载分配器,将请求转向Tomcat服务器,从而实现负载均衡。黏性Session模式和复制Session模式。
跨JVM虚拟机,专门用于分布式缓存的框架--Terracotta。
一款企业级的,开源的,JVM层的集群解决方案。可以实现诸如分布式对象共享,分布式缓存,分布式Session等功能。可以作为负载均衡、高可用性的解决方案。
使用Terracotta也可以实现Tomcat的Session共享。同时Terracotta也是一个成熟的高可用性系统解决方案。
并不会进行全复制,而仅仅传输变化的部分,网络负载也相当对较低。
memcached
时间与空间交换
缓存
CPU换取内存空间
第三章 Java程序优化
3.1 字符串优化
不变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式。
substring内存泄露 目的是为了高效且快速的共享String内的char数组对象。空间换时间的手段。 对于大字符串的截取小子字符串可以使用new String(string.substring(a,b)),失去所有的强引用从而使的原来的大字符串可以被回收。
字符串分隔和查找 split, StringTokenizer, 自己实现indexof(int) & subString(start, end) & charAt(int index)
StringTokenizer#hasMoreTokens对象被不断创建并销毁!
charAt效率远远高于startsWith(String)和endsWith(String)
StringBuffer & StringBuilder 同一行的+号处理,不同行代码使用builder! String的concat()方法效率远远高于+和+=运算符,但是又远远低于StringBuilder类。
容量参数
3.2 核心数据结构
List, ArrayList, Vector, LinkedList(循环双向链表) LinkedList对堆内存和GC的要求更高。
P82中关于LinkedList的最前端的插入测试,这是那ArrayList插入的最坏情况和LinkedList的最好情况对比嘛!
LinkedList任意位置插入还是需要查找的过程的!entry(index),这要看移动元素和查找元素的消耗!
如果在系统应用中,List对象需要经常在任意位置插入元素,则可以考虑用LinkedList代替ArrayList。
对于ArrayList从尾部删除元素时效率很高,从头部删除元素时则相当费时;而LinkedList从头部删除元素时效率相差无几级,但从LinkedList中间删除元素是性能*非常*糟糕,比ArrayList从头部删除还差!!
容量参数
遍历列表
foreach(63ms), foriterator(47ms), for(int)(31ms)
LinkedList最好不要使用for(int)因此每次获得对象都要几乎遍历一遍list!
对ArrayList这些基于数组的实现来说,随机访问的速度是最快的,在遍历这些List对象时,可以优先考虑随机访问。但对于LinkedList等基于链表的实现,随机访问性能是非常差的,应避免使用。
Map Hashtable,Properties, HashMap, TreeMap, LinkedHashMap(有序的HashMap) Collections.synchronizedMap()
HashMap不是线程安全的。但Hashtable不允许key或者value使用null值。在内部算法上,它们对key的hash算法和hash值到内存索引的映射算法不同。
重写equals时最好重写hashcode!
容量参数
扩容操作会遍历整个HashMap,应该尽量避免改操作发生,设置合理的初始大小和负载因子,可以有效减少HashMap扩容的次数。
LinkedHashMap在内部增加了一个链表,用以*存放元素的顺序*。因此,LinkedHashMap可以简单地理解为一个维护了元素次序的HashMap。
TreeMap
实现了SortMap可以对元素进行*排序*!性能上有所不足!
TreeMap排序方式和LinkedHashMap是不同的。LinkedHashMap是基于元素进入集合的顺序或被访问的先后顺序排序;而TreeMap则是基于元素的固有顺序(由Comparator或者Comparable确定)
set HashSet,LinkedHashSet,TreeSet
优化集合访问代码
分离循环中被重复访问的代码
len = collections.size();
for(int i = 0; i < len; i++){..
省略相同的操作
String s = null;
for(..;..;..){
s = collections.get(i);
...
}
减少方法调用
方法调用要消耗系统堆栈的,虽然面向对象的设计模式和模块化的软件设计方法鼓励程序员使用若干个小方法替代一个大方法。
可以的话,应该尽量避免调用原生接口,转为直接访问对象的属性。
如果可以,则尽量直接访问内部元素,而不要调用对应的接口。函数调用时需要消耗系统资源的,直接访问元素会更高效。
RandomAccess标志接口 通过RandomAccess可以知道List是否支持快速随机访问。 快速随机访问的对象,任何一个机遇数组的List都是实现了RandomAccess接口。而基于链表的实现则没有。 对LinkedList进行随机访问时list.get(i),总是要进行一次遍历查找,虽然通过双向链表的特性,将平均查找次数减半,但是其遍历过程依然消耗了大量的CPU时间。而ArrayList的get方法则可以简单的用一条语句描述elementData[index];
3.3 使用NIO提升性能 基于流的I/O实现,以字节为单位处理数据。 新的Java I/O具有以下特性: 为所有的原始类型提供(Buffer)缓存支持; 使用java.nio.charset.Charset作为字符串编码解码解决方案; 增加通道Channel对象,作为新的原始I/O抽象; 支持锁和内存映射文件的文件访问接口; 提供了基于Selector的异步网络I/O.
NIO基于块Block的,以块为基本单位处理数据。在NIO中最为重要的两个组件是缓冲Buffer和通道Channel。
缓冲是一块连续的内存块,是NIO读写数据的中转地。通道表示缓冲数据的源头或目的地,它用于向缓冲读取或写入数据,是访问缓冲的接口。
通道Channel只和缓冲Buffer打交道!数据的大写都得通过Buffer!
FileChannel fc = fileInputStream.getChannel()
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
fc.read(byteBuffer);
fc.close();
byteBuffer.flip();
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();
writeChannel.write(buffer);
}
readChannel.close();
writeChannel.close();
* position
* capacity
* limit
* flip()操作,该操作会重置position,通常,将buffer从写模式转换为读模式时需要执行此方法,读写转换时使用。
* rewind()将position置零,并清除标志位(mark)。它的作用在于为提取Buffer的有效数据做准备。
* clear()将position置零,同时将limit设置为capacity的大小,并清除了标志mark,为重写buffer做准备。
* 标志缓冲区
mark(), reset()
* 复制缓冲区
dulicate() 新生成的缓冲区和原缓冲区共享相同的内存数据。并且,对任意一方的数据改动都是相互可见的,但两者又独立维护了各自的position,limit和mark。
* 缓冲区分片
slice()方式,它将现有的缓冲区中,创建新的子缓冲区,子缓冲区和父缓冲区共享数据,有助于将系统模块化。
* 只读缓冲区
可使用缓冲对象的asReadOnlyBuffer()方法得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区。
对于数据安全非常有用。保证数据不被修改。同时,因为只读缓冲区和原始缓冲区是共享内存块的,因此,对原始缓冲区的修改,只读缓冲区也是可见的。
* 文件映射到内存(高性能)
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
RandomAccessFile raf = new RandomAccessFile("c:/mapfile.txt", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapNode.READ_WRITE, 0, raf.length());
while(mbb.hasRemaining()){
System.out.print((char)mbb.get());
}
mbb.put(0, (byte)98);
raf.close();
* 处理结构化数据
散射(Scattering)和聚集(Gathering)。散射是指将数据读入一组Buffer中,而不仅仅是一个。而聚集指将数据写入一组Buffer中。
散射和聚集的基本使用方法和对单个Buffer操作时的使用方法相当类似。在JDK中,通过ScatteringByteChannel和GatheringByteChannel接口提供相关操作。
在散射读取中,通过依次填充每个缓冲区。填充一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。
散射/聚集I/O对于处理*结构化*的数据非常有用。例如,对于一个有固定格式的文件的读写。在已知文件具体结果的情况下,可以构造若干个符合文件结构的Buffer,使得各个Buffer的大小恰好符合文件各段结构的大小。此时,通过散射读的方式可以一次将内容装配到各个对应的Buffer中,从而简化操作。
如果需要创建指定格式的文件,只要先构造好大小合适的Buffer对象,使用聚集写的方式,便可以很快的创建出文件。在JDK提供的各种通道中,DatagramChannel,FileChannel,和SocketChannel都实现了这个两个接口。
有利于实现程序的模块化。
ByteBuffer bookbuf = ByteBuffer.wrap("java性能优化技巧".getBytes("utf-8"));
ByteBuffer authorbuf = ByteBuffer.wrap("xxx".getBytes("utf-8");
booklen = bookbuf.limit();
authlen = authorbuf.limit();
ByteBuffer bufs[] = new ByteBuffer[]{bookBuf, authorBuf};
File file = new File(TPATH);
if(!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
FileChannel fc = fos.getChannel();
fc.write(bufs);
fos.close();
ByteBuffer b1 = ByteBuffer.allocate(booklen);
ByteBuffer b2 = ByteBuffer.allocate(authlen);
ByteBuffer[] bufs = new ByteBuffer[]{b1, b2};
File file = new File(TPATH);
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
fc.read(bufs);
String bookname = new String(bufs[0].array(), "utf-8");
String authname = new String(bufs[1].array(), "utf-8");
* 直接内存访问
DirectBuffer = ByteBuffer.allocateDirect();
-XX:MaxDirectMemorySize=10M -Xmx10M
频繁创建和销毁DirectBuffer的代价远远大于在堆上分配内存空间。但如果能将DirectBuffer进行复用,那么,在读写频繁的情况下,它完全可以大幅改善系统性能。
DirectBuffer的缓冲空间不再堆上分配,因此可以使应用程序突破最大堆的内存限制。对DirectBuffer的读写操作比普通Buffer快,但是对它的创建和销毁且比普通Buffer慢。
3.4 引用类型
FinalReference强引用
强引用可以直接访问目标对象
强引用所指向的对象的任何时刻都不会被系统回收。JVM宁愿抛出OOM异常,也不回收强引用所指向的对象
强引用可导致内存遗漏
SoftReference软引用
一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。
当堆使用率接进阈值时,才会去回收软引用的对象。
只要有足够的内存,软引用便可能在内存中存活相当长一段时间。因此软引用可以用于实现对内存敏感的Cache。
obj = null;
System.gc();
软引用可以使用一个应用队列(构造函数的参数),当对象被回收时,就会被加入到这个队列中。
WeakReference弱引用
在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。但,由于垃圾回收器的线程通常优先级很低,因此并不一定能很快的发现持有弱引用的对象。在这种情况下,弱引用可以存在较长的时间。一旦一个弱引用的对象被垃圾回收器回收,并会加入一个注册引用队列中。
只要进行垃圾回收,弱引用对象一旦被发现,便会以及被回收,并加入注册引用队列中。再试图通过weakRef.get()方法取得强引用就会失败。
适合保存那些可有可无的缓存数据。
Object#finalize()
VirutalReference虚引用 PhantomReference
虚引用的对象和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用取得强引用,总是失败。
并且,虚引用必须和引用队列一起使用,它的作用在于*跟踪垃圾回收*过程。
P123
在第一次GC时,系统找到了垃圾对象,并调用finalize()方法回收内存,但没有立即加入回收队列。第二次GC时,该对象真正被GC清除。
finalize()只会被调用一次,因此,在第二次回收时,对象就没有机会再度复活了。
* WeakHashMap
#expunageStaleEntries()清理持有弱引用的key的表项。
如果WeahHashMap的key都在系统内持有强引用,那么WeakHashMap就退化为普通的HashMap,因为所有的表项都无法被自动清理。
3.5 有助于改善性能的技巧 慎用异常,不要在for循环内使用try-catch。 使用局部变量,局部变量保存在栈中,速度快。 位运算代替乘除发>>1 >>>1 <<1 替换switch,对于固定结果的形式,使用结果数组代替 一维数组代替二维数组 尽可能介绍方法的调用,可以使用变量不需要使用getset方法。 提取表达式,提取这些重复劳动相当有意义,尤其要关注在循环体内的代码,从循环体内提取重复的代码可以有效地提升系统性能。但有时可需要注意数值的有效性! 展开循环(这个不到万不得已不要弄) 布尔运算代替为运算 使用arraycopy(), 比使用for循环快很多 使用Buffer(缓冲)进行I/O操作,Reader/Writer由于InputStream/OutputStream 使用clone替换new,在构造函数的执行可能会比较长 需要注意的默认情况下,clone方法生成的实例只是原生对象的浅拷贝。如果需要深拷贝,则需要重新实现clone方法 静态方法替换实例方法 实例方法需要维护一张类似虚函数表的结构,以实现对多态的支持。与静态方法相比,实例方法的调用需要更多的资源。 对于一些常用的工具类方法,没有对其进行重载的必要,那么将它们声明为static,便可以加速方法的调用。 如果是private方法呢?!
第四章 并行程序开发及优化
4.1 并行程序设计模式 * future模式 类似商品订单。在网上购物后,当看中某一件商品提交订单。当订单处理完毕后,便可在家里等待商品送货上门。 当某一段程序提交一个请求,期望得到一个答复。但服务程序处理可能很慢! FutureTask, Callable, Sync,
FutureTask<String> future = new FutureTask<String>(new RealData("a")); // callable
ExecutorService executor = Executors.newFixedThreadPool();
executor.submit(future);
Thread.sleep(1000);
System.out.println(future.get());
* Master-Worker模式
系统由两类进程协作工作,Master进程和Worker进程。Master负责接收和分配任务,Worker负责处理子任务。
当各个Worker进程将子任务(workquene)处理完成后,将结果返回给Master进程,由Master进程做归纳和汇总,从而得到系统的最终结构。
将一个大任务分解成若干个小任务,并行执行,从而提高系统额吞吐量。其处理过程是异步的,因此Client不会出现等待现象。
P150
一种将串行任务并行化的方法,被分解的子任务在系统中可以被并行处理,同时,如果有需要,Master进程不需要等待子任务都完成计算,就可以根据已有的部分结果集计算最终结果。
* Guarded Suspension模式
保护暂停,仅当服务进程准备好时,才提供服务。进行排队!
RequestQuene充当中间缓冲,存放未处理的请求,保证了客户请求不丢失,同时也保护了服务线程不会受到大量并发的请求,而导致计算机资源不足。
wait,notifyAll
在一定程度上缓解系统的压力,可以将系统的负载在时间轴上均匀地分配,使用该模式后,可以有效降低系统的瞬时负载,对提高系统的抗压力和稳定性有一定帮助。
* 不变模式
为了能尽可能地去除这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象额不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性。
不变模式天生就是多线程友好的。一个对象一旦被创建,则它的内部状态将永远不会发生改变。所以,没有一个线程可以修改其内部状态和数据,同时期内部状态也决不会自行发生改变。
基于这些特性,对不变对象的多线程操作不需要进行同步控制。
不变模式和只读属性是有一定的区别的。不变模式比只读属性更有更强的一致性和不变形。
对只读属性的对象而言,对象本身不能被其他线程修改 ,但是对象的自身状态却可能自行修改。
不变模式则要求,无论出于什么原因,对象自创建后,其内部状态和数据保持绝对的稳定。
当对象创建后,其内部状态和数据不再发生任何变化。
对象需要被共享,被多线程频繁访问。
主要以下4个方面:
去除setter方法及其所有修改自身属性的方法。
将所有属性设置为私有,并用final标记,确保不可修改。
确保没有子类可以重载修改它的行为。final类。
有一个可以创建完整对象的构造函数。
在JDK中,不变模式的应用非常广泛。
java.lang.String/Boolean/Byte/Character/Double/Float/Integer/Long/Short
不变模式通过回避问题而不是解决问题的态度来处理多线程并发访问控制。不变模式是不需要进行同步操作的。
由于并发同步会对性能产生不良的影响,因此,在需要允许的情况下,不变模式可以提高系统的并发性能和并发量。
* 生产者-消费者模式
多线程建的协作提供了良好的解决方案。生产者和消费者之间通过共享内存缓冲区进行通信。
生产者-消费者模式中的*内存缓冲区*的主要功能是数据在多线程的共享,此外,通过该缓冲区,可以缓解生产者和消费者间的性能差。
共享内存缓冲区,生产者和消费者间的通信桥梁,避免了生产者和消费者的直接通信,从而将生产者和消费者进行解耦。生产者不需要知道消费者的存在,消费者也不需要知道生产者的存在。
允许生产者和消费者在执行速度上存在时间差,可以通过共享内存缓冲区得到缓冲,确保系统正常运行。
BlockingQuene
volatile
AtomicInteger
生产者和消费者模式能够很好地对生产者线程和消费者线程进行解耦,优化了系统整体结构。同时,由于缓冲区的作用,允许生产者线程和消费者线程存在执行上的性能差异,从一定程度上缓解了性能瓶颈对系统性能的影响。
4.2 JDK对任务执行框架 在有限的范围内,增加线程的数量可以明显提高系统的吞吐量,但一旦超过了这个范围,大量的线程只会拖垮应用系统。 因此在生产环境中使用线程,必须对加以控制和管理。 盲目地大量创建线程对系统性能是有伤害的。
* 线程池
进行线程的复用。
在线程频繁调用的场合,可以节约不少系统开销(指创建和销毁线程的开销)
wait, notifyAll,
使用线程池后,线程的创建和关闭通常有线程池维护。线程通常不会因为执行完一次任务而被关闭,线程池中的线程会被多个任务重复利用。
线程池可以减少线程频繁调度的开销。对改善性能也有明显的效果。
* Executor框架
ExecutorService exe = Executors.newCachedThreadPool();
for(int i = 0; i<1000; i++){
exe.execute(new MyThread("testJDKThreadPool" + Integer.toString(i)));
}
newFixedThreadPool 数量始终不变,无界队列
newSingleThreadExecutor 只有一个线程的线程池。任务队列。
newCachedThreadPool (不推荐)根据实际情况调整线程数量的线程池,线程数量不确定,若有空闲的线程可以复用,则会优先使用可复用的线程。如果不够则会创建新的线程处理任务。
newSingleThreadScheduledExecutor 返回一个ScheduledExecutorService对象,线程池大小为1。在给定时间执行某任务的功能。如在某个固定的延时之后执行,或者周期性执行某个任务。
newScheduledThreadPool 可以指定线程数量。
* 自定义线程池
ThreadPoolExecutor
P173
SynchronousQueue 直接提交队列,总会迫使线程池增加新的线程执行任务。
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
RejectedExecutionHandler
使用自定义线程池可以提供更为灵活的任务处理和调度方式。如果内置的线程池无法满足应用需求,则可以考虑使用自定义线程池。
* 优先线程池大小
过大或者过小的线程数量都无法发挥最优的系统优化,但是线程池大小的确定也不需要做的非常精确,因为只要避免极大和极小两种情况,线程池的大小对系统的性能并不会影响太大。一般来说,确定线程池的大小需要考虑CPU数量,内存大小,JDBC连接因素。
Ncpu=CPU的数量
Ucpu=目标CPU的使用,0<=Ucpu<=1
W/C=等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的线程池的大小等于:
Nthreads=Ncpu*Ucpu*(1+W/C)
在JAVA中,可以通过Runtime.getRuntime().availableProcessors()取得可用的CPU数量。
* 扩展ThreadPoolExecutor
#beforeExecute()
#afterExecute()
可以获取线程池调度的内部细节,对并行程序故障排查很有帮助。
4.3 JDK并发数据结构
* 并发List
Vector, CopyOnWriteArrayList, Collections.synchronizedList(list)
CopyOnWriteArrayList很好的利用了对象的不变性,在没有对对象进行写操作前,由于对象为发生改变,因此不需要加锁。
而在试图改变对象时,总是先获取对象的一个副本,然后对副本进行修改,最后将副本写回。
读性能优越,但是写操作性能切不尽如人意!此时应该优先使用Vector.
ReentrantLock
在读多写少的高并发情况下,使用CopyOnWriteArrayList可以提高系统的性能。
但是,在写多读少的场合,CopyOnWriteArrayList的性能不如Vector。
* 并发Set
CopyOnWriteArraySet
synchronizedSet(set)
* 并发Map
synchronizedMap(map) 高并发的情况,这个Map的性能表现不是最优的。
ConcurrentHashMap 专用于高并发的Map,锁分离。get操作也是无锁的,它的put操作的锁粒度又小于同步的HashMap,因此它的整体性能优于同步的HashMap
* 并发Queue
ConcurrentLinkedQueue 适用于高并发场景下的队列。通过无锁的方式,实现了高并发状态下的高性能。
LinkedBlockingQueue
BlockingQueue的主要功能并不是在于提升高并发时的队列性能,而在于简化多线程建的数据共享。读写堵塞等待的机制。
多线程间的数据共享。
生产者-消费者模式。
#offer #poll
#put #take
#drainTo
ArrayBlockingQueue
LinkedBlockingQueue
* 并发Deque(Double-Ended Queue)
双端队列
LinkedList,ArrayDeque, LinkedBlockingDeque
LinkedBlockingDeque没有进行读写锁的分离,因此同一时间只能有一个线程对其进行操作。
在高并发的应用中,它的性能表现要远远低于LinkedBlockingQueue,更要低于ConcurrentLinkedQueue。
4.4 并发控制方法 多线程,多任务间的协作和数据共享 内部锁,重入锁,读写锁,信号量
* volatile
在Java中,每一个线程有一块工作内存区,其中存放着被所有线程共享的主内存中的变量的值得拷贝。当线程执行时,它在自己的工作内存中操作这些变量。
为了存取一个共享的变量,一个线程通常先获取锁定并且清楚它的工作内存区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
read & load
store & write
由于主存储和工作内存间传送数据需要一定的时间,而且每次消耗的时间可能是不同的。因此从另一个线程的角度看,一个线程对变量的操作顺序可能是不同的。比如,某一线程内的代码是先给变量a赋值,再给变量b赋值,在另一个线程中,可能先在内存中看到变量b的更新,再看见变量a的更新。当然,在一个线程中对同一个变量的操作次序,一定和该线程中的实际次序相吻合。
由于每个线程都有自己的工作内存区,因此当一个线程改变自己的工作内存中的数据时,对其他线程来说,可能是不可见的!
为此,可以使用volatile关键字迫使所有线程均读写主内存中的对应变量,从而使得volatile变量在多线程间可见。
volatile的变量可以做一下保证:
其他线程对变量的修改,可以即时反应在当前线程中。
确保当前线程中对volatile变量的修改,能即可写回共享主内存中,并被其他线程所见。
使用volatile声明的变量,编译器会保证其有序性。
使用volatile标识变量,将迫使所有线程均读写主内存中的对应变量,从而使得volatile变量在多线程间可见。
volatile boolean isExit;
public void tryExit(){
if(isExit == !isExit){
System.exit(0);
}
}
public void swapValue(){
isExit = !isExit;
}
如果在单线程或者isExit变量未声明为volatile,则isExit==!isExit很难成立。因为等号左边的取值和等式右边的取值都首先尝试从线程工作区中获得,虽然swap线程总是在不停地切换这个数值,但对tryExit线程并不是立即可见。因此两次取值几乎每次都相等,故程序通常可以运行很长时间。
如果定义为volatile类型后,swap线程对isExit的修改可以很快被tryExit()函数发现,也就是说isExit==!isExit很有可能成立。因此此时,tryExit总是设法去主内存获取数据,当它获得等式左边的数据后,获取等式右边的数据前,swap线程极有可能已经修改了isExit值,故该变但是很有可能成立,则程序退出。
* synchronized
在JDK6中,synchronized和非公平锁的差距已经缩小。且其性能还将会进一步优化。更重要的是,与其他的同步方式相比,synchronized更为简洁,代码可读性和维护性较好。
当同步method()方法被调用后,调用线程首先必须获得当前对象的锁,若当前对象锁被其他线程持有,则调用线程会等待,方法结束后,对象锁会被释放。等价于:
public void method(){
synchronized(this){
//....
}
}
同步块可以更为精确的控制同步代码范围,缩小同步块。一个小的同步代码非常有利于锁的快进快出,从而使系统拥有更高的吞吐量。
将无需同步的代码块有效地剥离,仅同步必要的代码,有利于减少锁的竞争。
当synchronized用于static函数时,相当于锁加在当前Class对象上,因此所有对该方法的调用,都必须获得class对象的锁。
为了实现多线程间的交互,还需要使用Object对象的wait和notify方法。函数wait()可以让线程等待当前对象上的通知(notify()调用),在wait()过程中,线程会释放对象锁。
为了有效地控制线程间的协作,需要配合使用synchronized以及notify和wait等方法。
* ReentrantLock重入锁
比内部锁synchronized拥有更加强大的功能,它可中断,可定时。
还拥有公平和非公平两种锁。
使用ReentrantLock时,还要时刻牢记,一定要在程序最后*释放锁*。一般释放锁的代码写在finally里。而JVM虚拟机总会在最后自动释放synchronized锁。
公平锁的实现代价比非公平锁大,因此从性能上分析,非公平锁的性能要好得多。
在ReentrantLock使用完毕后,务必释放ReentrantLock锁。
lock(): 获得锁,如果锁应被占用,则等待。同时在等待锁的过程中,线程不会响应中断。
lockInterruptibly(): 获得锁,但优先响应中断。在锁等待的过程中可以响应中断事件。
tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,以及返回。
tryLock(long time, TimeUnit unit): 在给定时间内尝试获得锁。在等待时间内可以进行中断。
unlock(): 释放锁。
在锁竞争激烈的情况下,这些灵活的控制功能有助于应用程序在应用层根据合理的任务分配来避免锁竞争,以提高应用程序的性能。
* ReadWriteLock读写锁
读写锁可以有效地帮助减少锁竞争,以提升系统性能。如果使用重入锁或者内部锁,在理论上说所有读之间,读写之间,写写之间都是串行操作。
读写锁允许多个线程同时读,使用读线程间真正并行。但,考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有错的。如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。
private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private static Lock readLock = rwLock.readLock();
private static Lock writeLock = rwLock.writeLock();
读写锁的高性能的原因是读的绝对并行,如果读操作占有绝对多数,那么读操作本身消耗的时间越多,读写锁与重入锁的性能差距也就越大。
在读多写少的场合,使用读写锁可以分离读操作和写操作,使所有读操作间真正并行,因此,能够有效提高系统的并发能力。
* Condition对象
用于协调多线程间的复杂协作。
Condition与锁相关联的。通过Lock接口的Condition newCondition()方法可以生成一个与锁绑定的Condition实例
Condition对象和锁的关系,就如同Object.wait(),Object.notify()两个函数以及synchronized关键字一样,他们都可以*配合*使用以完成多线程协作的控制。
await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。
awaitUniterruptibly()方法和await()方法基本相同,但是它并不会在等待过程中响应中断。
signle()方法用于唤醒一个在等待中的线程,相对的signalAll()方法会唤醒所有在等待中的线程。
如:BlockingQueue的使用方法和场景。
* Semaphore信号量
为多线程协作提供了更为强大的控制方法。广义上说,信号量是多锁的扩展,无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量去可以指定多个线程同时访问某个资源。
在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多个线程可以访问某一个资源。
acquire() 尝试获得一个准入的许可,若无法获得,则线程等待,直到有线程释放一个许可或者当前线程被中断。
acquireUninterruptibly() 不响应中断
tryAcquire() 尝试获得一个许可,如果成功返回true,失败返回false,它不会进行等待,立即返回。
tryAcquire(long timeout, TimeUnit unit)
release() 用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。
对象池!可用资源数!
对于共享资源/变量还是需要同步的synchronized
信号量对锁的概念进行了扩展,它可以限定对某一个具体资源的最大可访问线程数。
* ThreadLocal线程局部变量
ThreadLocal完全不提供锁,而是用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是一种共享数据的解决方案。
从性能方面讲,在并发量不是很高时,也许加锁的性能会更好。但在高并发量或者锁竞争激烈的场合,使用ThreadLocal可以在一定程度上减少锁竞争。
不同线程间的对象副本,并不是有ThreadLocal创建的,而*必须在线程内创建*,并保证不同线程间实例均不相同。若不同线程间使用了同一个Date实例,即使把它放到ThreadLocal中保护起来,也无法保证其线程安全性。
应该避免将同一个实例设置到不同线程的ThreadLocal中,否则其线程安全性无法保证。
4.5 “锁”的性能和优化
在高并发的环境下,激烈的锁竞争会导致程序的性能下降。
避免死锁,减少锁的粒度,锁分离
在明显提高系统性能,同时,多线程的方式会额外增加系统的开销。线程本身的元数据,线程的调度,线程上下文的切换等。
合理的进行任务调度,合理的开发,才能将多核CPU的性能发挥到极致。
* 死锁问题需要满足以下四条件:
互斥条件: 一个资源每次只能被一个进程使用
请求与保持条件: 一个进程因获得的资源而阻塞时,对已获得资源保持不放
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
循环条件等待: 若干进程之间性能一种头尾相接的循环等待资源关系。
只要打破死锁必要条件中的任意一个,就能够解决死锁问题。
通过线程dump检查到死锁的存在。
* 减少锁持有时间
单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。
只在必要是进行同步,这样就能明显减少线程持有锁的时间,提高系统的吞吐量。
初始化的两次判断!
if(!compiled){
synchronized(this){
if(!compiled){
compile();
}
}
}
* 减少锁粒度
这种技术典型的使用场景就是ConcurrentHashMap类的实现。
很好的使用了拆分锁对象的方式提高ConcurrentHashMap的吞吐量。ConcurrentHashMap将整个HashMap分成若干个段Segment,每段都是一个子HashMap。
如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到那个段中,然后对该段加锁,并完成put()操作。
默认情况下,ConcurrentHashMap拥有16个段,如果幸运的话,可以同时接受16个线程同时插入(如果都插入不同的段中),从而大大提高其吞吐量。
减少锁粒度会引入一个新问题,当系统需要取得全局锁时,其消耗的资源会比较多。
类似于size()获取全局信息的方法调用并不频繁是,这种减少锁粒度的方法才能真正意义上提高系统吞吐量。
减少锁粒度,指缩小锁定对象的范围,从而减少锁冲突的可能性。进而提高系统的并发能力。
* 读写分离锁来替换独占锁
ReadWriteLock
使用读写分离锁来替代独占锁时减少锁粒度的一种特殊情况。
如果说上小节中提高的较少锁粒度是通过分隔数据结构实现的,那么,读写锁则是对系统功能点的分割。
在*读多写少*的场合,读写锁对系统性能是很有好处的。
* 锁分离
读写锁思想的延伸就是锁分离。
在LinkedBlockingQueue的实现中,take函数和put函数分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但由于其基于链表的,因此,两个操作分别作用于队列的前端和尾端,才理论上说,两者并不冲突。
使用两把不同的锁分离了take和put操作。
* 重入锁ReentrantLock和内部锁synchronized
* 锁粗化Lock Coarsening
凡是有个度,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
在软件开发过程中,开发人员也应该有意识地在合理的场合进行锁的粗化,尤其当在循环内请求锁时。
for循环的例子!
for(){
synchronized(this){
//
}
}
synchronized(this){
for(){
//
}
}
* 自旋锁Spinning Lock
可以使线程在没有取得锁时,不被挂起(因为线程挂起和恢复需要时间),而转而去执行一个空循环(即所谓的自旋)。
若干个空循环后,线程如果获得了锁,则继续执行。若线程依然不能获得锁,才会被挂起。
对于那些锁竞争不是很激烈、锁占用时间很短的并发线程,是有一定的积极意义,但对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅仅白白浪费了CPU的时间,最终还是免不了执行被挂起的操作,反而浪费了系统资源。
-XX:+UseSpinning参数开启自旋锁
-XX:PreBlockSpin参数来设定自旋锁的等待次数。
* 锁消除Lock Elimination
是JVM的即时编译,通过对运行时上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
逃逸分析和锁消除分别可以使用-XX:+DoEscapeAnalysis和-XX:+EliminateLocks开启(锁消除必须工作在-server模式上)
而使用参数-server -XX:-DoEscapeAnalysis -XX:-EliminateLocks关闭逃逸分析和锁消除。
对锁的请求和释放是要消除系统资源的。使用锁消除技术可以去掉那些不可能存在多线程访问的锁请求,从而提高系统性能。
* 锁偏向Biased Lock
是 JDK6提出的一种锁优化方式。
如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就说,若某一锁被线程获取后,便进入偏向模式。
当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省了操作时间。如果在此之间有其他线程进行了锁请求,则锁退出偏向模式。
在JVM中使用-XX:+UseBiasedLocking可以设置启用偏向锁。
偏向锁在锁竞争激烈的场合没有优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持在偏向模式,
此时,使用锁偏向不仅得不到性能的优化,反而有损系统性能。因此,在激烈竞争的场合,使用-XX:-UseBiasedLocking参数禁用锁偏向反而能提升系统吞吐量。
4.6 无锁的并行计算 非阻塞同步的方法。不需要使用“锁”(无锁),但是依然能确保数据和程序在高并发环境下保持多线程间的一致性。
最简单的一种非阻塞同步以ThreadLocal为代表,每个线程拥有各自独立的变量剧本,因此在并行计算时,无需相互等待。
基于比较并交换(Compare And Swap)CAS算法的无锁并发控制方法。
与锁的实现相比,无锁算法的设计和实现都要复杂的多,但由于其非阻塞行,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。
更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
CAS(V,E,N) V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。
CAS操作时抱有乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,
其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
* 在JDK的java.util.concurrent.atomic包下,有一组使用无锁算法实现的原子操作类,主要有AtomicInteger,AtomiIntegerArray,AtomicLong,AtomicArrayLong,AtomicReference等。
getAndSet()方法,实现了CAS算法。
public final int getAndSet(int newValue){
for(;;){
int current=get();
if(compareAndSet(current, newValue))
return current;
}
}
java.util.concurrent.atomic包中的原子类是基于无锁算法实现的,它们的性能远远优于普通的有锁操作。因此推荐直接用这些工具。
* Amino框架(apache)
LockFreeList
LockFreeVector
LockFreeSet
LockFreeBSTree
Graph
提供了Master-Worker模式:
一种是静态的,不允许在任务开始时添加新的子任务
另一种是动态的,允许在任务执行过程中,有Master或者Worker添加新的子任务。
Animo框架中已经内置实现了动态和静态两种Master-Worker模式,开发人员可以直接使用。
4.7 协程 协程,轻量级线程。 协程便是对线程的进一步分隔。无论是进程、线程和协程,在逻辑层,它们都可以对应一个任务,以执行一段逻辑代码,达到一个目标。。 当使用协程实现一个任务时,协程并不完全占据一个线程,当一个协程处于等待状态时,它便会把CPU交给该线程内的其他协程。 与线程相比,协程间的切换更为轻便,因此,具有更低的操作系统成本和更高的任务并发性。
Kilim框架
可以很快地建立一个基于协程的多任务并发系统,并通过一种称为MailBox邮箱的对象进行协程间通信。
协程间也没有锁,同步块等概念。因此它不会使原有的多线程程序更加复杂。同时,一个纯粹的协程系统,其逻辑复杂性也要低于一个需要严格处理线程间并发关系的多线程系统。
在开发后期,需要通过Kilim框架提供的织入weaver工具,将协程的控制代码库织入原始代码,以支持协程的正常工作。
Task是协程的任务载体。
Fiber对象用于保存和管理任务执行堆栈,以实现任务可以正常暂停和继续。Mailbox对象为协程间的通信载体,用于数据共享和信息的交流。
使用协程,可以让系统以更低的成本,支持更高的并行度。
第五章 JVM调优
5.1 Java虚拟机内存模型
程序计数器,栈(虚拟机栈,本地方法栈),Java堆和方法区
各线程之间的计数器互不影响,独立工作;是一块线程私有的内存空间。 如果当前线程正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,则程序计数器为空。
Java虚拟机栈也是线程私有的内存空间,它和Java线程在同一时间创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。 栈空间增加后,程序支持的函数的调用深度明显上升。
虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态链接方法和返回地址等信息。
函数嵌套调用的次数由栈的大小决定。栈越大,函数嵌套调用次数越多。对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其嵌套调用次数就会减少。
随着调用函数参数的增加和局部变量的增加,单次函数调用对栈空间的需求也会增加。
-Xss1M
局部变量表用于存放方法的参数和方法内部的局部变量。局部变量表以“字”为单位进行内存的划分,一个字为32位长度。 在方法执行时,虚拟机使用局部变量表完成方法的传递,对于非static方法,虚拟机还会将当前对象this作为参数通过局部变量表传递给当前方法。
使用jclasslib工具可以查看class文件中每个方法所分配的最大局部变量表的容量。 jclasslib可以查看class文件的结构,包括常量池、接口、属性、方法,还可以用于查看方法的字节码,可以帮助读者对class文件做较为深入的研究。
局部变量表张的字空间是可以重用的。因为在一个方法体内,局部变量的作用范围并不一定是整个方法体。
当方法一结束,该方法的栈帧就会被销毁,即栈帧中的局部变量表也被销毁,变量b就会被自然回收。
Java堆分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存地足够长,老年对象就会被移入老年代。 新生代又可以分为eden,survivor space0(s0 或者 from space)和survivor space1(s1或者to space). eden伊甸园 survivor幸存者。存放其中的对象至少经历了一次垃圾回收,并得以幸存。 如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。
-XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M
方法区主要保存的信息是类的元数据。 类的类型信息、常量池、域信息、方法信息。大部分来就class文件,是Java应用程序必不可少的重要数据。 在Hot Spot虚拟机中,方法区也成为永久区,是一块独立于Java堆的内存空间。 在永久区中的对象,同样也是可以被GC回收的。 对永久区的回收,通常主要从两个方面分析:一是GC对永久区常量池的回收,二是永久区对类元数据的回收。
-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails
–如果虚拟机确认该类的所有实例已经被回收,并且加载该类的ClassLoader已经被回收,GC就可能回收该类型。 只要ClassLoader被回收,在FullGC时,永久区中的类的元数据是完全有可能被回收的。这种方法,可以很好地与一些动态字节码生成库结合使用,以确保永久区的稳定。
5.2 JVM内存分配参数 -Xmx5M 最大堆内存
Runtime.getRuntime().maxMemory()取得系统可用的最大堆内存。
-Xms5M 最小堆内存 Java应用程序在运行时,首先会被分配-Xms指定的内存的大小,并尽可能尝试在这个空间段内运行程序。 如果-Xms的数值较小,那么JVM为了保证系统尽可能地的指定内存范围内运行,就会更加频繁的进行GC操作,以释放失效的内存空间。 从而,会增加MinorGC和FullGC的次数,对系统性能产生一定的影响。 为了减少GC次数,增加-Xms的值。
JVM会试图将系统内存尽可能限制在-Xms中,因此,当内存实际使用量触及-Xms知道那个的大小时,会触发FullGC。 因此把-Xms值设置为-Xmx时,可以在系统运行初期减少GC的次数和耗时。
-Xmn设置新生代 设置一个较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响。 新生代的大小一般设置为整个堆空间的1/4到1/3左右。
在Hot Spot虚拟机中,-XX:NewSize用于设置新生代的初始大小。 -XX:MaxNewSize用于设置新生代的最大值。 通常情况下,只设置-Xmn已经满足绝大部分应用的需要。设置—Xmn的效果等同于设置-XX:NewSize和-XX:MaxNewSize。
持久代(方法区)不属于堆的一部分,在HotSpot虚拟机中,使用—XX:PermSize设置持久带的初始大小。 -XX:MaxPermSize设置持久代的最大值。 持久代的大小直接决定了系统可以支持对少个类定义和多少常量。 设置合理的持久代大小有助于维持系统稳定。
线程栈是线程的一块私有空间。 -Xss参数设置线程栈的大小,每个线程的栈空间。 由于Java堆也是向操作系统申请内存空间的,因此,如果堆空间过大,就会导致操作系统可用于线程栈的内存减少,从而间接减少程序所能支持的线程数量。
-Xss1M 设置每个线程拥有1M的栈空间
如果系统确实需要大量线程并发执行,那么设置一个较小的堆和较少的栈,有助于提高系统所能承受的最大线程数。
堆的比例分配 -XX:SurvivorRatio用于设置新生代中,eden空间和s0空间的比例关系。s0和s1空间又分别成为from空间和to空间。它们的大小事相同的,职能也是一样的,并在Minor GC后,会互换角色。 -XX:SurvivorRatio = eden/s0 = eden/s1
-XX:+PrintGCDetails -Xmn10M -XX:SurvivorRatio=2
-XX:NewRatio=老年代/新生代 设置新生代和老年代的比例。
-XX:+PringGCDetails -XX:NewRatio=2 -Xmx20M -Xms20M
-XX:TargetSurvivorRatio设置survivor区的可使用率。当survivor区的空间使用率达到这个数值时,会将对象送入老年代。
5.3 垃圾收集基础
引用计数法Reference Counting 无法处理循环引用的情况,AB互相包含。 由于无法处理循环引用的问题,引用计数器不合适用于JVM的垃圾回收。
标记-清除算法Mark-Sweep 标记阶段和清除阶段 最大的问题是空间碎片 标记-清除算法先通过根节点标记所有可达对象,然后清除所有不可达对象,完成垃圾回收。
复制算法Copying 将原有的的内存空间分成两块,每次只用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的颜色,完成垃圾回收。 如果系统中的垃圾对象比较多,复制算法需要复制的存活对象数量并不会太多。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。 并且没有碎片。但是,复制算法的代价缺点是系统内存折半。
在Java的新生代串行垃圾回收器中,使用了复制算法的思想。新生代分为eden空间,from空间和to空间3部分。
其中from和to空间可以视为用于复制的两块大小相同,地位相等,且可进行角色互换的空间块。from和to空间也称为survivor空间,即幸存者空间,用于存放未被回收的对象。
复制算法比较适用于新生代。因为在新生代,垃圾对象通常会多于存活对象,复制算法的效果会比较好。
标记-压缩算法Mark-Compact 复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。 标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。 这种方法即避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比较高。
增量算法Incremental Collecting 在大部分垃圾回收算法而言,在垃圾回收的过程中,应用软件将处于一种Stop the World的状态。 让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。 以此反复,直到垃圾收集完成。 由于在垃圾回收的过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文切换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分代Generational Collecting 各种回收算法都有各自的优势和特点。根据垃圾回收对象的特性,使用合适的算法回收,才是明智的选择。 将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。
分代的思想被现有的HotSpot虚拟机广泛使用,几乎所有的垃圾回收器都区分年轻代和老年代。
-> 评价GC策略: 吞吐量 垃圾回收器负载 停顿时间 垃圾回收频率 反应时间 堆分配
新生新生代串行处理器使用复制算法,实现相当简单,逻辑处理特别高效,且没有线程切换的开销。 -XX:UseSerialGC参数可以指定使用新生代串行收集器和老年代串行收集器。当JVM在Client模式下运行时,它是默认的垃圾收集器。 串行垃圾回收器虽然古老,但是久经考验,在大多数情况下,其性能表现是相当不错的。
老年代串行收集器使用标记-压缩算法,和新生代串行收集器一样,它也是一个串行的,独占式的垃圾回收器。通常会使用比新生代垃圾回收更长的时间。 老年代串行收集器可以和多种新生代回收器配合使用,同时它也可以作为CMS回收器的备用回收器。 若要启用老年代串行回收器,可以尝试使用以下参数: -XX:+UseSerialGC 新生代、老年代都使用串行回收器 -XX:+UseParNewGC 新生代使用并行收集器,老年代使用串行收集器 -XX:+UseParallelGC 新生代使用并行回收收集器,老年代使用串行收集器
并行收集器工作在新生代的垃圾收集器,它只是简单的串行回收器多线程化。它的回收策略、算法以及参数和串行回收器一样。 并行回收器也是独占式的回收器,在收集过程中,应用程序会全部停止暂停。 开启并行回收器可以使用以下参数: -XX:+UsePerNewGC 新生代使用并行收集器,老年代使用串行回收器。 -XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS。
并行收集器工作是的线程数量可以使用-XX:ParallelGCThreads参数指定。一般,最好与CPU数量相当。
在默认情况下,当CPU数量小于8个时,ParallelGCThreads的值等于CPU数量;当CPU数量大于8个时,值等于3+[(5*CPU_COUNT)/8]
新生代并行回收Parallel Scavenge收集器 使用复制算法的收集器,非常关注系统的吞吐量。 新生代并行回收收集器可以使用一下参数启用: -XX:UseParallelGC 新生代使用并行回收收集器,老年代使用串行收集器 并行回收器提供了两个重要的参数来控制系统的吞吐量: -XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间,在收集器工作时,会调整Java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis内。 如果希望减少停顿时间,而把这个值设得很小,为了达到预期的停顿时间,JVM可能会使用一个较小的堆,而这将导致垃圾回收变得很频繁,从而增加垃圾回收总时间,降低了吞吐量。 -XX:GCTimeRatio: 设置吞吐量大小,它的值时一个0~100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。默认情况下它的取值是99,即不超过1/(1+99)=1%的时间用于垃圾收集。 除此之外,并行回收收集器与并行收集器另一个不同之处在于,它还支持一种自适应的GC调节策略,使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。 在这种模式下,新生代的大小、eden和servivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点。 仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
老年代并行回收收集器 一种多线程并发的收集器。也是一种关注吞吐量的收集器。使用标记-压缩算法,它在JDK6中才可以使用。 使用-XX:+UseParallelOldGC可以在新生代和老年代都使用并行回收收集器,这是一对非常关注吞吐量的垃圾收集器组合,在堆吞吐量敏感的系统中,可以考虑使用。 参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。
CMS收集器ConcurrentMark Sweep 主要关注于系统停顿时间。并发标记清除,使用多线程并发回收标记-清除算法垃圾收集器。不是独占式的回收器,在CMS回收过程中,应用程序仍然在不停地工作。 初始标记(独占系统资源)、并发标记、重新标记(独占系统资源)、并发清除和并发重置。 CMS默认启动的线程数(ParallelGCThreads+3)/4,ParallelGCThreads是新生代并行收集器的线程数,也可以通过-XX:ParallelCMSThreads参数手动设定CMS的线程数量。 当CPU资源比较紧张时,受到CMS收集器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。 回收阀值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认值是68.即当老年代的空间使用率达到68%时,会执行一次CMS回收。 标记-清除算法的回收器会导致大量内存碎片。CMS还提供了几个用于内存压缩整理的参数。 -XX:+UseCMSCompactAtFullCollection开关可以使用CMS在垃圾收集完成后,进行一次内存碎片整理内存碎片的整理不是并发进行的。 -XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少CMS回收后,进行一次内存压缩。
G1收集器Garbage First 服务端的垃圾收集器,它在吞吐量和停顿控制上,预期要优于CMS收集器。 与CMS收集器相比,G1收集器是基于标记-压缩算法。因此它不会产生空间碎片,也没有必要再收集完成后,进行一次独占式的碎片整理工作。 G1收集器还可以进行非常精确的停顿控制。它可以让开发人员指定在长度为M的时间段中,垃圾回收时间不超过M。 -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:GCPauseIntervalMillis=200 指定在200ms内,停顿时间不超过50ms。这是G1回收器的目标,并不保证能执行它们。
Stop the World。
-Xmx512M -Xms512M -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails
-Xmx512M -Xms512M -XX:+UseParNewGC -Xmx512M -Xms512M -XX:+UseParallelOldGC -XX:ParallelGCThreads=8 -Xmx512M -Xms512M -XX:+UseSerialGC -Xmx512M -Xms512M -XX:+UseConcmarkSweepGC
进可能把新的对象保留在新生代,eden。
-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn6M -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivoRatio=8 -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=90 -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2
由于新生代垃圾回收的速度高于老年代回收。因此,将年轻对象预留在新生代有利于提高整体的GC效率。
大对象出现在新生代可能扰乱新生代GC,并破坏新生代原有的对象结构。 可以将大对象直接分配到老年代,保持新对象结构的完整性,以提高GC的效率。 还要尽可能地避免使用短命的大对象!短命的大对象对垃圾回收是一场灾难。 使用参数-XX:PretenureSizeThreshold设置大对象直接进入老年代的阀值。这个参数只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。 -XX:+PrintGCDetails -Xmx20M -Xms20M -XX:PretenureSizeThresold=1000000参数设置1M的字节数据直接进入老年代。
-XX:MaxTenuringThreshold设置进入老年代的年龄(阀值),它的默认值是15。 但这不意味着对象非要达到这个年龄才进入老年代。事实上,对象实际进入老年代的年龄与虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阀值年龄的最大值。 即,实际晋升老年代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的那个。 -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=1 将晋升到老年代的对象年龄阀值设置为1。
当-Xms和-Xmx相等时,-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio这两个参数是无效的。(堆空间空闲比例) 相等时,可以减少GC的次数,但是每次GC的压力必然比不相等时大! -Xms10M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
Solaris系统可以使用大页,增强CPU的内存寻址能力 java -Xmx2506m -Xms2506m -Xmn1536m -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:LargePageSizeInBytes=256m
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:SurvivorRatio=8 -XX:TargetSurvivorRation=90 -XX:MaxTenuringThreshold=31
5.5 使用JVM参数 JIT参数:(just in time) -XX:CompileThreshold(默认1500-client,1000-server) -XX:+PrintCompilation -XX:CompileThreshold=1500 -XX:+PrintCompilation -XX:+CITime
堆快照(dump) -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\m.hprof 然后可以通过visual vm来分析。 获取和分析堆快照对于Java应用程序性能优化和故障排查都是相当重要的。
在系统发生OOM错误时,虚拟机在错误发生时运行一段第三方脚本。
-XX:OnOutOfMemoryError=C:\reset.bat
获取GC信息:
-verbose:gc或者-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDestribution
-XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=18
-XX:+PrintHeapAtGC
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime
-Xloggc:c:\gc.log
类和对象的跟踪
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-verbose:class
-XX:+PrintClassHistogram
-XX:+DisableExplicitGC
-Xnoclassgc
-Xincgc
-XX:-UseSplitVerifier
-XX:-FailOverToOldVerifier
-XX:+UseBoundThreads
-XX:+UseLWPSynchronization
-XX:+UseVMInterruptibleIO
-XX:+UseLargePages
-XX:LargePageSizeInBytes
64为虚拟机可以使用-XX:+UseCompressedOops
5.6 实战JVM调优 tomcat启动调优
1 在catalina.bat中加入set CATALINA_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails
2 从gc.log中发现39次GC,2次Full GC。去掉无疑会变快!
set CATALINA_OPTS=%CATALINA_OPTS% -Xmx32M -Xms32m
3 禁用显示GC
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DisableExplicitGC
4 进一步减少Minor GC
set CATALINA_OPTS=%CATALINA_OPTS% -XX:NewRatio=2
5 加快Minor GC
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+UseParallelGC
6 取消校验
set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none
JMeter 再调优,下降GC次数,提升吞吐量
rem 分配适当的堆,可以明显提升系统的性能
set CATALINA_OPTS=%CATALINA_OPTS% -Xmx512M -Xms512m
set CATALINA_OPTS=%CATALINA_OPTS% -XX:PermSize=32M
set CATALINA_OPTS=%CATALINA_OPTS% -XX:MaxPermSize=32M
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DisableExplicitGC
set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none
再优化,使用并行回收器
set CATALINE_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails
set CATALINA_OPTS=%CATALINA_OPTS% -Xmx512M -Xms512m
set CATALINA_OPTS=%CATALINA_OPTS% -XX:PermSize=32M
set CATALINA_OPTS=%CATALINA_OPTS% -XX:MaxPermSize=32M
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DisableExplicitGC
set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8
还可以尝试使用CMS收集器,设置survivor去的大小(-XX:SurvivorRatio),努力将对象预留在新生代-XX:CMSInitiatingOccupancyFraction.
-Xmx, -Xms, -XX:NewRatio, -Xmn, -XX:SurvivorRatio, -XX:PermSize, -XX:MaxPermSize, 选择垃圾回收器, -XX:+DisableExplicitGC, -Xnoclassgc, -Xverify:none
Java性能调优工具
top命令
从宏观上观察各个进程对CPU的占用情况以及内存使用情况
sar命令 sar -u/-r/-b 1 3
-A 所有报告总和
-u cpu利用率
-d 硬盘使用报告
-b I/O的情况
-q 查看队列长度
-r 内存使用统计使用
-n 网络信息统计
-o 采样结果输出到文件
查看I/O信息,内存以及CPU使用情况
vmstat 1 3
可以查看内存,交互分区,I/O操作,上下文切换,时钟中断以及CPU的使用情况。
iostat 1 2
iostat -d 1 2
iostat -x 1 2
磁盘I/O很容易成为系统性能瓶颈。通过iostat可以快速定位系统是否产生了大量的I/O操作
pidstat
http://www.icewalkers.com/Linux/Software/59040/Sysstat.html
/configure
make
make install
jps
pidstat -p 1187 -u 1 3
-p指定进程ID,-u表示对CPU使用率监控,-t将系统性能的监控细化到线程级别。
pidstat -p 1187 1 3 -u -t (TID为1204)
使用pidstat工具不仅可以定位到进程,还可以进一步定位到线程
jstack -l 1187 > /tmp/t.txt,然后找到nid(native id)为0x4b4(即1204),即可对应到具体的线程类!开发人员可以使用pidstat很容易地捕获到在Java线程中大量占用CPU的线程 。
-d表明监控对象为磁盘的I/O
pidstat -p 22796 -d -t 1 3
使用pidstat命令可以查看进程和线程的I/O信息。
pidstat -r -p 27233 1 5
任务管理器 perfmon性能监控工具
Process Explorer: http://technet.microsoft.com/en-us/sysinternals/bb896653
jps
java -classpath %JAVA_HOME%/lib/tools.jar sun.tools.jps.Jps
jps -q 只输出进程Id
jps -m 输出传递给Java进程(主函数)的参数
jps -m -l 输出主函数的完整路径
jps -m -l -v 显示传递给JVM的参数
jstat 观察Java应用程序运行时信息的工具,可以通过它查看对信息的详细情况
jstat -class -t 2972 1000 2
-class输出中,Loaded表示载入了类的数量,第一个Bytes表示载入类的合计大小,Unloaded表示卸载类的数量。
jstat -compiler -t 2972 JIT编译信息
jstat -GC 2972
jstat -gccapacity 2972
jstat -gccause 2972
jstat -gcnew 2972
jstat -gcnewcapacity 2972
jstat -gcold 2972
jstat -gcoldcapacity 2972
jstat -gcpermcapacity 2972
jstat -gcutil 2972
jinfo 查看运行时某一个JVM参数的实际取值,甚至可以在运行时修改部分参数,并使之以及生效 jinfo -flag MaxTenuringThreshold 2972 -XX:MaxTenuringTHreshold=15 jinfo -flag PrintGCDetails 2972 -XX:PrintGCDetails
jmap 导出Java应用程序的堆快照和对象的统计信息 jmap -histo 2972 > c:\s.txt jmap -dump:format=b,file=c:\heap.hprof 2972
jhat 分析Java应用程序的堆快照内容 jhat c:\heap.hprof 分析完成后,使用http服务器展示其分析结构,http://localhost:7000 可以使用OQL查看出当前Java程序中对象的路径 select file.path.value.toString() from java.io.File file
jstack 导出Java应用程序线程堆栈,还能自动进行死锁检查,输出找到的死锁信息。
jstack [-l] <PID>
jstatd 启动远程控制,是一个RMI服务端程序,建立本地计算与远程监控工具的通信。 为jstatd分配最大的权限jstatd.all.policy
grant codebase "file:E:/tools/jdk1.6/lib/tools.jar" {
permission java.security.AllPerission
}
jstatd -J -Djava.security.policy=c:\jstatd.all.policy
netstat -ano | findstr 1099
jps localhost:1099
jstat -gcutil 460@localhost:1099
hprof工具
java -agentlib:hprof=help
java -agentlib:hprof=cpu=times,interval=10
java -agentlib:hprof=heap=dump,format=b,file=C:\core.hprof
可以使用MAT和Visual VM等工具分析这些堆文件
java -agentlib:hprof=heap=sites可以输出Java应用程序中各个类所占的内存百分比。
JConsole工具 可以查看Java应用程序的运行概述,监控堆信息,永久区使用情况,类加载情况等。
如果需要使用JConsole连接远程进程,则需要在远程Java应用程序启动时,加上如下参数:
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.managerment.jmxremote
-Dcom.sun.managerment.jmxremote.port=8888
-Dcom.sun.managerment.jmxremote.authenticate=false
-Dcom.sun.managerment.jmxremote.ssl=false
在线程选项卡中,使用“检测到死锁”按钮,还可以自动检测多线程应用程序的死锁情况。
VM摘要显示了当前Java应用程序的基本信息,如虚拟机类型,虚拟机版本,系统线程信息,操作系统内存信息,堆信息,垃圾回收器类型,JVM参数以及类路径等。
MBean,查看或者设置MBean属性、运行MBean的方法等。可以对Java应用程序中的MBean进行统一管理。
jconsole -pluginpath %JAVA_HOME%/demo/management/JTOP/JTOP.jar
Visual VM也支持远程JMX连接。Java应用程序可以通过以下JConsole的参数打开端口。
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.managerment.jmxremote
-Dcom.sun.managerment.jmxremote.port=8888
-Dcom.sun.managerment.jmxremote.authenticate=false
-Dcom.sun.managerment.jmxremote.ssl=false
Visual VM也支持添加远程主机。远程主机可以通过jstatd工具建立,如使用以下命令开启jstatd服务器: jstatd -J -Djava.security.policy=c:\jstatd.all.policy
Visual VM的Threads页面提供了详细的线程信息。在该页面,还会进行自动的死锁检测,一旦发现存在死锁现象,便会提示用户。
TDA(Thread Dump Analyzer)一款线程快照分析工具 https://java.net/projects/tda TDA实际上是一个文本分析工具,它将线程快照的文本信息进行整理和统计,以图形化的方式展现,方便开发人员分析数据。
BTree 可以在不停机的情况下,通过字节码注入,冬天监控系统的运行情况。 可以跟踪指定的方法调用、构造函数调用和系统内存等信息。 “Tree application”
监控指定函数耗时: import static com.sun.btrace.BTraceUtils.; import com.sun.btrace.annotation.;
@BTrace
public class PrintTimes{
@TLS
private static long startTime=0;
@OnMethod(clazz="/.+/", method="/writeFile/")
// 监控 任意类,监控writeFile()方法
public static void startMethod(){
startTime=timeMillis();
}
@OnMethod(class="/.+/", method="/writeFile", location=@Location(Kind.RETURN)) // 方法返回时触发
public static void endMethod(){
print(strcat(name(probeClass()),"."), probeMethod()));
print(" [");
print(strcat("Time token: ", str(timeMillis()-startTime)));
println("]");
}
}
通过BTrace脚本,可以监控指定的某一个函数的运行耗时。
取得任意行代码信息: import com.sun.btrace.annotation.; import static com.sun.btrace.BTraceUtils.;
@BTrace
public class AllLines{
@OnMethod(clazz="/.*BTraceTest/", location=@Location(value=King.LINE, line=26) // 监控以BTraceTest结尾的类,指定26行触发
public static void online(@ProbeClassName String pcn, @ProbeClassName String pmn, int line){
print(Strings.strcat(pcn, "."));
print(Strings.strcat(pmn, ":"));
println(line);
}
}
定时触发:
@BTrace
public class Timers{
@OnTimer(1000)
public static void getUpTime(){
println(Strings.strcat("1000 msec : ", Strings,str(Sys.VM.vmUptime())));
}
@OnTimer(3000)
public static void getStack(){
jstackAll();
}
}
通过BTrace可以在当前应用程序中,定时获得一些运行时的系统信息。
监控函数参数:
@BTrace
public class FunArg{
@OnMethod(clazz="/.*BTraceTest/", method="/writeFile/")
public static void anyWriteFile(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args){
print(pcn);
print(".");
print(pmn);
printArray(args);
}
}
BTrace可以跟踪指定方法的传入参数,获取方法的参数值,对于现场问题排查会很有帮助。
监控文件
@BTrace
public class FileTracker{
@TLS
private static String name;
@OnMethod(clazz="java.io.FileInputStream", method="<init>")
public static void onNewFileInputStream(@Self FileInputStream self, File f){
name = Strings.str(f);
}
@OnMethod(clazz="java.io.FileInputStream", method="<init>", type="void (java.io.File)", location=@Location(King.RETURN))
public static void onNewFileInputStreamReturn(@Self FileInputStream self, File f){
if(name!=null){
println(Strings.strcat("opened for read", name)); // 打印文件信息
name=null;
}
}
@OnMethod(clazz="java.io.FileOutputStream", method="<init>")
public static void onNewFileOutputStreamReturn(@Self FileOutputStream self, File f, boolean b){
name = str(f);
}
@OnMethod(clazz="java.io.FileOutputStream", method="<init>", type="void (java.io.File, boolean)", location=@Location(King.RETURN))
public static void onNewFileOutputStreamReturn(@Self FileOutputStream self, File f){
if(name!=null){
println(Strings.strcat("opened for write", name)); // 打印文件信息
name=null;
}
}
}
BTrace通过截获FileOutputStream和FileInputStream构造函数的方式监控文件I/O操作。如果文件I/O没有通过这个两个构造函数发起,那么就无法通过这个脚本获得相关的文件信息。
6.6 Visual VM对OQL的支持
select s from java.lang.String s where s.count >= 100
select a from int[] a where a.length >= 256
select {instance:s, content:s.toString()} from java.lang.String s where /^\d{2}$/(s.toString())
select {content:file.path.toString(), instance:file} from java.io.File file
select cl from instanceof java.lang.ClassLoader cl
select s from 0x37A014D8 s
select heap.findClass("java.util.Vector")
select heap.findClass("java.util.Vector").superclasses()
select filter(heap.classes(), "/java.io./(it.name)")
select heap.livepaths(s) from java.lang.String s where s.toString()=='56'
select heap.roots()
select heap.objects("java.io.File", true)
select classof(v) from instanceof java.util.Vector v
select objectid(v) from java.util.Vector v
select reachables(s) from java.lang.String s where s.toString()=='56'
select reachables(s, "java.lang.String.value") from java.lang.String s where s.toString()=='56'
select referrers(s) from java.lang.String s where s.toString()=='56'
select s.toString() from java.lang.String s where (s.count==2 && count(referrers(s)) >=2 )
select referees(heap.findClass("java.io.File"))
select referees(s) from java.lang.String s where (s.count==2 && count(referrers(s)) >=2 )
select {size:sizeof(o), Object:o} from int[] o
select {size:sizeof(o), Object:o} from java.util.Vector o
select {size:sizeof(o), rsize:rsizeof(o)} from java.util.Vector o
.....
–END