vip影院自助建站系统百度会员
news/
2025/10/7 9:40:32/
文章来源:
vip影院自助建站系统,百度会员,网店美工招聘,互联网产品设计graph LR
A--B性能概述
程序性能表现形式
执行速度#xff1a;程序响应速度#xff0c;总耗时是否足够短内存分配#xff1a;内存分配是否合理#xff0c;是否过多消耗内存或者存在泄漏启动时间#xff1a;程序运行到可以正常处理业务需要的时间负载承受能力
性能测…graph LR
A--B性能概述
程序性能表现形式
执行速度程序响应速度总耗时是否足够短内存分配内存分配是否合理是否过多消耗内存或者存在泄漏启动时间程序运行到可以正常处理业务需要的时间负载承受能力
性能测评指标
执行时间CPU时间函数或者线程占用CPU时间内存分配程序在运行时占用内存空间磁盘吞吐量描述I/O使用情况网络吞吐量描述网络使用情况响应时间系统对用户行为或者时间做出回应的时间。
系统瓶颈相关计算机资源
磁盘I/O网络操作CPU异常对java来说异常捕获和处理是非常消耗资源的锁竞争高并发程序中锁的竞争对性能影响尤其重要增加线程上下文切换开销。而且这部分开销与应用需求五官占用CPU资源内存一般只要应用设计合理内存读写速度不会太低出发程序进行高频率内存交换扫描。
Amdahl定律
Amdahl定义了串行系统并行化后加速比的计算公式和理论上线 加速比定义加速比 优化前系统耗时/优化后系统耗时 Amdahl定律给出了加速比与系统并行°和处理器数量的关系加速比Speedup串行化程序比重FCPU数量为N Speedup1/(f(1-F)/N) 总结根据Amdahl定律使用多核CPU对系统进行优化优化效果取决于CPU数量以及系统中串行程序比重CPU数越多串行比重越低优化效果更好。
性能调优层次
设计调优代码调优JVM调优数据库调优操作系统优化
设计优化
善用设计模式
单例模式经典问题
用于长沙一个多小的具体实例可以确保系统中一个类只产生一个实例能带来两大好处 对的频繁使用的对象可以省略创建对象花费的时间这对于那些重量级的对象而言是非常可观的一笔系统开销由于new操作次数减少因而对系统内存的使用频率也会降低这将减轻GC压力缩短GC停顿时间。
public class Singleton{private Singleton(){System.out.println(Singleton is create);//创建单例可能比较慢应为是大对象。}private static Singleton instance new Singleton();public Singleton getInstance(){return instance;}
}单例模式必须有一个私有构造只有构造方法是私有的才能保证他不会被实例化其次instance和getInstance必须是static修饰的这样JVM加载单例类时候对象就会被穿件才能全局使用。以上这种单例实现方式简单可靠但是缺点是我们无法做到延迟加载因为static修饰比如JVM加载类时候被建立如果此时这个单例类特别大或者在系统中海油其他角色你们任何使用这个类的地方都会初始化这个单例变量而不管是否被用到比如单例类作为String工厂如下
public class SingletonStr {private SingletonStr(){System.out.println(SingletonStr is create);}private static SingletonStr instance new SingletonStr();public static SingletonStr getInstance(){return instance;}public static void createString(){System.out.println(createString in singleton);}public static void main(String[] args) {SingletonStr.createString();}
}
/***
*输出如下
* SingletonStr is create
* createString in singleton
***/如上可见没有使用instance但是还是会新建因此我们需要的是延迟加载机制使用时候才创建如下实现
public class LazySingleton {private LazySingleton(){System.out.println(LazySingleton is create);}private static LazySingleton instance null;public static LazySingleton getInstance(){if(instance null){synchronized (LazySingleton.class){if(instance null){instance new LazySingleton();}}}return instance;}
}如上instance初始值null在jvm加载时候没有额外的负担需要调用getinstance()方法之后才会判断是否已经存在不存在则加上锁来初始化。此处不加锁在并发情况会有线程安全问题。以上案例用synchronized必然存在性能问题存在锁竞争问题如下改造
public class StaticSingleton {private StaticSingleton(){System.out.println(StaticSingleton is create);}private static class SingletionHolder{private static StaticSingleton instance new StaticSingleton();}public static StaticSingleton getInstance(){return SingletionHolder.instance;}
}使用内部类来维护单例的实例当StaticSingleton被加载时内部类并不会被初始化只有getInstance被调用才会加载SingletionHolder从而初始化instance因为实例的建立是类加载完成所以天生就对多线程友好。
代理模式 代理模式的一个作用可作为延迟加载例如当前并没有使用某个组件则不需要初始化他使用一个代理对象替代它的原有位置只要在真正需要使用的时候才对他进行加载。在实践周上分散系统压力尤其在启动的过程。
动态代理介绍
生成动态代理的方法有多个JDK自带的动态代理CGLIBJavassist或者ASMjdk的无需引入第三方jar功能比较弱CGLIB和Javassist都是高级字节码生成库总体性能不jdk的优秀而且功能强大ASM是低级字节码生成工具使用ASM已近乎使用java bytecode编程对开发人员要求高。首先jdk的简单如下
public class JdkDBQueryHandler implements InvocationHandler {IDBQuery real null;Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if(real null){real new DBQueryImpl();}return real;}
}首先使用JDK动态代理生成代理对象JDK动态代理需要实现一个处理方法调用的Handler用于实现代理方法的内部逻辑接着需要用这个Handler生成代理对象。如下 public static IDBQuery createJdkproxy(){IDBQuery jdkProxy (IDBQuery) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{IDBQuery.class},new JdkDBQueryHandler());return jdkProxy;}
}以上代码生成了一个实现IDBQuery接口的代理类代理类的内部逻辑有JdkDBQueryHandler决定生成代理类后由newProxyInstance方法放回该代理类的一个实例。
CGLIB和Javassist
public class CglibDBQueryInsterceptor implements MethodInterceptor {IDBQuery real null;Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {if(real null){real new DBQueryImpl();}return real;}
}
public static IDBQuery createCGlibProxy(){Enhancer enhancer new Enhancer();enhancer.setCallback(new CglibDBQueryInsterceptor());enhancer.setInterfaces(new Class[]{enhancer.createClass()});IDBQuery cglibProxy (IDBQuery) enhancer.create();return cglibProxy;
}
CGLIB的动态代理使用和JDK的类似如上代码。Javassist的动态java代码创建代理过程和上面的有一些不同javassist内部可以通过动态java代码生成字节码这种方式创建的动态代理可以非常灵活甚至可以在运行时候生成业务逻辑。很奇特的一种用法如下代码
public static IDBQuery createJavassistBytecodeDynamicProxy() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {ClassPool mpool new ClassPool(true);CtClass mCtc mpool.makeClass(IDBQuery.class.getName() JavassistBytecodeProxy);mCtc.addInterface(mpool.get(IDBQuery.class.getName()));mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));mCtc.addField(CtField.make(public IDBQuery.class.getName() real;, mCtc));String dbqueryname DBQueryImpl.class.getName();mCtc.addMethod(CtNewMethod.make(public String request(){if(real null) real new dbqueryname (); return real.request();},mCtc));Class pc mCtc.toClass();IDBQuery bytecodeProxy (IDBQuery) pc.newInstance();return bytecodeProxy;}以上代码使用CtField.make方法和CtNewMethod.make方法在运行时候生成了代理类的字段和方法这些逻辑由于javassist的CtClass对象处理将java代码转换为对应的字节码并生成动态代理的实例。
享元模式
享元模式核心思想在于如果一个系统存在多个相同对象你们只需要共享一份拷贝不必每次都实例化对象。享元模式对性能的提升有如下两点 节省重复创建对象的开销。因为被享元模式维护的对象只被创建一次当被对象创建消耗比较大实话可以节省大量时间。由于创建对象的数量减少所以对系统内存的需求也减少这将GC的压力也降低进而使得系统拥有一个健康的内存结构和更快的响应速度。 享元模式主要角色由享元工厂抽象享元具体享元类主函数几部分组成各功能如下 享元工厂用来创建具体享元类维护相同享元对象他保证相同的享元对象可以被系统共享内部使用类似单例模式的算法当请求对象已经存在时候直接返回对象不存在时候在创建对象抽象享元定义需共享的对象的业务接口享元类被创建出来总是为了实现某些特定的业务逻辑。而抽象享元就是定义这些逻辑的语义行为。具体享元类实现抽象享元类的接口完成某一具体逻辑。 如上类图所示通过FlyWeightFactory工厂方法来生成ConcreteFlyWeight这一类大对象其中ConcreteFlyWeight可以代表多个不同的类但是都有相同的属性通过继承IFlyWeight接口这样都可以通过工厂方法来生成如下案例
public interface IReportManager {public String createReport();
}public class EmployeeReportManager implements IReportManager {private String tenantId;public EmployeeReportManager(String tenantId){this.tenantId tenantId;}Overridepublic String createReport() {return is employee;}
}public class FinancialReportManager implements IReportManager {private String tenantId;public FinancialReportManager(String tenantId){this.tenantId tenantId;}Overridepublic String createReport() {return is financial;}
}
//工厂方法
public class ReportManagerFactory {MapString, IReportManager financialMap new HashMap();MapString, IReportManager empolyeeMap new HashMap();public IReportManager getFinancialReportManager(String tenantId){if(!financialMap.containsKey(tenantId)){IReportManager financialReportManager new FinancialReportManager(tenantId);financialMap.put(tenantId, financialReportManager);return financialReportManager;}return financialMap.get(tenantId);}public IReportManager getEmployeeReportReportManager(String tenantId){if(!empolyeeMap.containsKey(tenantId)){IReportManager employeeReportManager new EmployeeReportManager(tenantId);empolyeeMap.put(tenantId, employeeReportManager);return employeeReportManager;}return empolyeeMap.get(tenantId);}
}//使用
public class MainTest {public static void main(String[] args) {ReportManagerFactory reportManagerFactory new ReportManagerFactory();IReportManager reportManager reportManagerFactory.getFinancialReportManager(A);System.out.println(reportManager.createReport());}
}装饰者模式 装饰者模式基本设计准则 合成/聚合复用原则的思想代码复用应该尽可能的使用委托而不是使用继承。因为继承是一种紧密耦合任何父类的改动都会影响子类不利于系统维护而委托则是松耦合只要接口不变委托类的改动并不会影响其上层对象。 装饰者模式通过委托机制复用系统中各个组件在运行时可以将这些功能进行叠加从而构造一个超级对象使其拥有各个组件的功能。基本结构如下 装饰者Decorator在被装饰者ConcreteComponent的基础上拥有相同接口Component添加自己的新功能被装饰者拥有的是系统核心组件完成特定功能模板。如下案例
//接口
public interface IPacketCreator {public String handleContent();
}
//被装饰者
public class ConcreteComponent implements IPacketCreator {Overridepublic String handleContent() {return Content of packet;}
}
//装饰器
public abstract class PacketDecorator implements IPacketCreator {IPacketCreator component;public PacketDecorator(IPacketCreator c){component c;}
}
//具体的装饰器一
public class PacketHTMLHeaderCreator extends PacketDecorator {public PacketHTMLHeaderCreator(IPacketCreator c) {super(c);}Overridepublic String handleContent() {StringBuilder sb new StringBuilder();sb.append(html);sb.append(body);sb.append(component.handleContent());sb.append(/body);sb.append(/html);return sb.toString();}
}
//具体的装饰器二
public class PacketHTTPHeaderCreator extends PacketDecorator {public PacketHTTPHeaderCreator(IPacketCreator c) {super(c);}Overridepublic String handleContent() {StringBuilder sb new StringBuilder();sb.append(Cache-Control:no-cache\n);sb.append(component.handleContent());return sb.toString();}
} JDK的实现中也有不少组件用的装饰器模式其中典型的就是IO模型OutputStreamInputStream类族的实现如下OutputStream为核心的装饰者模型的实现 其中FileOutputStream为系统核心类实现了文件写入数据使用DataOutputStream可以在FileOutputStream的基础上增加多种数据类型的写操作支持。BufferOutputStream装饰器可以多FileOutputStream增加缓冲功能优化IO性能以BufferedOutputStream为代表的功能组件是将性能模块和功能模块分离的一种典型实现 public static void testIOStream() throws IOException {DataOutputStream dataOutputStream new DataOutputStream(new BufferedOutputStream(new FileOutputStream(C:\\Users\\Administrator\\Desktop\\重要信息.txt)));
// 没有缓冲功能的流对象
// DataOutputStream dataOutputStream
// new DataOutputStream(
// new FileOutputStream(C:\\Users\\Administrator\\Desktop\\重要信息.txt));long begin System.currentTimeMillis();for (int i 0; i 10000; i) {dataOutputStream.write(i);}System.out.println(speed: (System.currentTimeMillis() - begin));}以上FileOutputStream第一种加入了BufferedOutputStream第二种没有第一种IO性能明显更高。
观察者模式
观察者模式在软件系统中非常常用当一个对象的行为依赖另外一个对象的状态的时候观察者模式就相当有用。在JDK内部就已经为开发人员准备了一套观察者模式的实现在java.util包中包括java.util.Observable类和java.util.Observer如图 如下按以上UML图实现观察者模式 //观察者的接口
public interface Observer {void update(Subject s);
}
//Subject顶级接口
public interface Subject {void registerObserver(Observer o);void removeObserver(Observer o);void notivfyAllObserver();
}
//某视频网站,实现了Subject接口
public class VideoSite implements Subject {private ArrayListObserver userList;private ArrayListString videos;public VideoSite() {userList new ArrayList();videos new ArrayList();}Overridepublic void registerObserver(Observer o) {userList.add(o);}Overridepublic void removeObserver(Observer o) {userList.remove(o);}Overridepublic void notivfyAllObserver() {for (Observer observer : userList) {observer.update(this);}}public void addVideos(String video) {this.videos.add(video);//通知所有用户notivfyAllObserver();}public ArrayListString getVideos() {return videos;}Overridepublic String toString() {return videos.toString();}
}
//实现观察者,即看视频的美剧迷们
public class VideoFans implements Observer {private String name;public String getName() {return name;}public void setName(String name) {this.name name;}public VideoFans(String name){this.name name;}Overridepublic void update(Subject s) {System.out.println(this.name , new Video are available!);System.out.println(s);}
}//test
public class MainTest {public static void main(String[] args) {VideoSite vs new VideoSite();vs.registerObserver(new VideoFans(one));vs.registerObserver(new VideoFans(two));vs.registerObserver(new VideoFans(three));vs.addVideos(生活大爆炸大更新!!!);}
}常用优化组件和方法
缓冲Buffer
缓冲可以协调上层组件和下层组件的性能差异当上层组件性能优于下层组件时可以有效减少上层组件的处理速度从而提升系统整体性能。缓冲最好的案例便是I/O中的读取速度。还是上面那个例子 public static void testIOStream() throws IOException {DataOutputStream dataOutputStream new DataOutputStream(new BufferedOutputStream(new FileOutputStream(C:\\Users\\Administrator\\Desktop\\重要信息.txt)));
// 没有缓冲功能的流对象
// DataOutputStream dataOutputStream
// new DataOutputStream(
// new FileOutputStream(C:\\Users\\Administrator\\Desktop\\重要信息.txt));long begin System.currentTimeMillis();for (int i 0; i 10000; i) {dataOutputStream.write(i);}System.out.println(speed: (System.currentTimeMillis() - begin));}使用BufferedOutputStream他构造方法有两个和和BufferedWriter类似
public BufferedOutputStream(OutputStream out);
public BufferedOutputStream(OutputStream out, int size)其中第二个构造可以指定缓冲区大小默认情况和BuffereWriter一样缓冲区大小8K缓冲大小不宜过小这样起不到缓冲作用也不宜过大浪费内存增加GC负担我们使用默认缓冲大小测试一个2毫秒一个34毫秒性能差一个数量级。除了性能优化缓冲区还可以作为上下层组件的通讯工具从而做到上下层组件解耦优化设计结构。
缓存cache
缓存也是一个提升系统性能而开辟的内存空间。缓存主要作用暂存数据处理结果并提供下次使用减少数据库访问减少计算从而减少CPU的占用从而提升系统性能。目前有很多java缓存框架EHCache OSCAcheJBossCacheEhCache缓存出自Hibernate还有缓存数据库Redismemcache这两个都是常用的缓存中间件。
对象复用池
对象池是目前常见的系统优化技术核心思想是一个类被频繁请求使用不必每次生成一个实例可以将这个类的一些实例保存在一个池中需要使用直接获取类似享元模式。对象池的使用最多的就是线程池和数据库连接池线程池中保存可以被重用的线程对象当有任务提交到线程池时候系统不需要新建线程而从池中获取一个可用线程执行即可。任务结束后也不用关闭线程而将他返回到池中下次继续用因为线程创建销毁过程是费时的工作因为线程池改善了系统性能。数据库连接池也是一个特殊对象池他维护数据库连接的集合。当系统需要访问数据库直接从池中获取无需重新创建并连接使用完后也不会关闭重新回到连接池中在这个过程节省了创建和销毁的重量级操作因此大大节约了事件减少资源消耗。目前应用多的数据库连接池C3P0 和Hibernate一起发布。处理线程池数据库连接池对普通Java对象必须要时候也可以池化对于大对象来说池化不仅节省获取对象实例的成本还可以减轻GC频繁回收这些对象产生的系统压力。实际开发中对象池Apache中有现成的Jakarta Commons Pool对象池。
public interface ObjectPoolT extends Closeable {
......T borrowObject() throws Exception, NoSuchElementException, IllegalStateException;......void returnObject(T var1) throws Exception;
}其中borrowObject方法从对象池获取一个对象returnObject方法在使用完后将对象返回对象池另外一个重要接口是PooledObjectFactory
public interface PooledObjectFactoryT {
//定义如何创建新对象实例PooledObjectT makeObject() throws Exception;
//对象从对象池中被销毁时会执行这个方法。void destroyObject(PooledObjectT p) throws Exception;
//判断对象是否可用boolean validateObject(PooledObjectT p);
//在对象从对象池取出前会激活这对象void activateObject(PooledObjectT p) throws Exception;
//在对象返回对象池时候被调用void passivateObject(PooledObjectT p) throws Exception;
}
Jakarta Commons Pool中内置了三个对象池StackObjectPoolGenericObjectPoolSoftReferenceObjectPool StackObjectPool利用java.util.Stack来保存对象可以为StackObjectPool指定一个初始化大小并且空间不够时候StackObjectPool自增长当无法从对象池得到可用对象他会自动创建新对象。GenericObjectPool一个通用的对象池可以设定容量也可以指定无对象可用时候的对象池行为等待或者创新对象还可以设置是否进行对象有效性检查。SoftReferenceObjectPool使用ArrayList保存对象但是并不是直接保存对象的强引用二手保存对象的软引用对对象数量没有限制没有可用对象时候新建对象内存紧张时候JVM可以自动回收具有软引用的对象。
并行替代串行
多CPU是的并行充分发挥CPU的潜能
负载均衡
大型应用系统一般用多台服务器来同时提供服务以此将请求尽可能均匀的分配到各个计算技上。案例TOmcat集群通过Apache服务器实现用Apache服务器作为负载均衡分配器将请求转向各个tomcat类似nginxtomcat集群有两种基本Session共享模式 黏性Session所有Session信息平均到各个tomcat上实现负载均衡好处在于不同各个节点同步信息每个节点固定几个用户信息节省内存缺点是宕机后用户信息丢失无法做到高可用。复制Session每台Tomcat存储全量用户Session数据当一个节点上Session修改广播到所有节点同时做更新操作这样即使挂掉也能提供服务坏处是更新操作占用网络资源影响效率 解决方案分布式缓存框架Terracotta在公线内存时候并不会进行全复制仅仅传输变化的部分网络复制也比较低因此效率远高于普通session复制
时间换空间
通常用于嵌入式设备或者内存硬盘空间不足的情况通过牺牲CPU的方式获得原本需要更多内存或者硬盘才能完成的工作。如下哪里教皇ab两个变量值一般是引入第三变量方法一而方法二通过计算能避免第三变量引入节省资源消耗CPU
//方法一
temp a;
a b;
b temp;
//方法二
a ab;
b a-b;
a a-b;空间换时间
使用更多的内存或者磁盘空间换取CPU资源或者网络资源等通过增加系统内存消耗来加快程序的运行速度。比如使用缓存技术来缓存一部分计算后的结果信息避免重复计算。
java程序优化
字符串优化处理
java中String类的基本实现主要包含三部分char数组偏移量String的长度。char数组表示String的内容他是String对象锁标识字符串的超集。String的真实内容还需要由偏移量和长度在这个Char数组中进行定位和截取。如下图 java设计对String对象进行了一部分优化主要体现在以下三个方面同时也是String对象的3个特点 不变性针对常亮池的优化类的final定义
不变性
String对象一旦生成则不能在对他进行修改String的这个特性可以泛化成不变模式机一个对象的状态在对象被创建后不发送变化主要作用在一个对象被多线程共享并发范问时候可以沈略同步和锁等待的时间因为不会被修改因此不存在线程安全问题。
针对常亮池优化
当两个String对象拥有相同的值时候他们只引用常量池中同一个拷贝。这样可以大幅节省内存空间如下案例
public static void main(String[] args) {String str1 abc;String str2 abc;String str3 new String(abc);System.out.println(str1 str2);System.out.println(str1 str3);System.out.println(str1 str3.intern());}以上案例str1 和str2 引用了相同的地址但是str3重新开辟了一块内存但是str3 最终在常量池的位置和str1 是一样的虽然str3 重新分配了堆空间但是在常量池中的实体和str1 相同intern方法返回Stromg对象在常量池中的引用。如下图说明 类的final定义
public final class Stringimplements java.io.Serializable, ComparableString, CharSequence {.....}如上java中对String类的定义final类型的定义是一个重要特点说明String在系统中不存在子类这是对系统安全性的保护同时对JDK1.5 版本之前环境中使用final定义有助于虚拟机寻找机会内敛所有final方法从而提升系统效率。
subString方法内存泄露风险
内存泄露leak of memory指为一个对象分配内存后在对象已经不再使用时候未及时释放导致一直占用内存单元是实际可用内存减少就好像内存漏了。内存溢出out of memory通俗说就是内存不够比如在一个无线循环中不断建大对象很快就会引发内存溢出。
substring方法内存泄漏JDK1.6
public String substring(int beginIndex, int endIndex)如上是String类的一个方法这个方法在JDK6 和JDK7 是实现不同的一下我们分别讨论subString作用是返回一个子字符串从父字符串的beginindex开始结束语endindex1如下
String x abcdef;
x str.substring(1,3);
System.out.println(x);
//输出ab看JDK6 中的substring的实现如下
public String substring(int beginIndex, int endIndex) {if (beginIndex 0) {throw new StringIndexOutOfBoundsException(beginIndex);}if (endIndex count) {throw new StringIndexOutOfBoundsException(endIndex);}if (beginIndex endIndex) {throw new StringIndexOutOfBoundsException(endIndex - beginIndex);}return ((beginIndex 0) (endIndex count)) ? this :new String(offset beginIndex, endIndex - beginIndex, value); //使用的是和父字符串同一个char数组value
}
//构造方法
String(int offset, int count, char value[]) {this.value value;this.offset offset;this.count count;
}//案例
String str abcdefghijklmnopqrst;
String sub str.substring(1, 3);
str null;如上简单案例两个字符串变量strsub。sub字符串是有父字符串str截取得到如果在jdk6 中运行因为数组在内存空间分配是在堆上那么sub和str的内部char数组value是公用的同一个也就是上述a~t这个char数组str和sub唯一的差别就是在数组中的beginindex和字符串长度count不同我们吧str引用为空实际上是想释放str的空间这时候GC并不会回收因为sub引用的还是那个char因此不会被回收虽然sub只截取一部分但是str特别大时候那就会造成资源浪费。
subString方法JDK1.7
JDK1.7中改进了subString的实现他实际上是为截取的字符创在堆中重新申请内存用来保存字符串的字符如下源码
public String substring(int beginIndex, int endIndex) {if (beginIndex 0) {throw new StringIndexOutOfBoundsException(beginIndex);}if (endIndex value.length) {throw new StringIndexOutOfBoundsException(endIndex);}int subLen endIndex - beginIndex;if (subLen 0) {throw new StringIndexOutOfBoundsException(subLen);}return ((beginIndex 0) (endIndex value.length)) ? this: new String(value, beginIndex, subLen);}
//构造函数
public String(char value[], int offset, int count) {if (offset 0) {throw new StringIndexOutOfBoundsException(offset);}if (count 0) {if (count 0) {throw new StringIndexOutOfBoundsException(count);}if (offset value.length) {this.value .value;return;}}// Note: offset or count might be near -11.if (offset value.length - count) {throw new StringIndexOutOfBoundsException(offset count);}this.value Arrays.copyOfRange(value, offset, offsetcount);}public static char[] copyOfRange(char[] original, int from, int to) {int newLength to - from;if (newLength 0)throw new IllegalArgumentException(from to);char[] copy new char[newLength];System.arraycopy(original, from, copy, 0,Math.min(original.length - from, newLength));return copy;}可以发现copyOfRange中为子字符串创建了一个新的char数组去存储子字符串中的字符。这样子字符串和父字符串也就没有什么必然联系父字符串引用失效时候GC就会适当时候回收。substring1.6 这种方式采用了空间换时间的手段他是一个包内的私有构造不被外界调用因此时间使用不用太担心带来的麻烦但是仍然需要关注这些问题。
字符串分割和查找
public String[] split(String regex)最原始的字符串分割split函数原型提供强大字符串分割功能参数可传正则普通字符串分割可以选择性能更好的函数StringTokenizer
StringTokenizer类分割 public static void main(String[] args) {String orgStr null;StringBuilder stringBuilder new StringBuilder();for (int i 0; i 10000; i) {stringBuilder.append(i);stringBuilder.append(;);}orgStr stringBuilder.toString();Long timeBegin System.currentTimeMillis();for (int i 0; i 10000; i) {orgStr.split(;);}Long timeEnd System.currentTimeMillis();System.out.println(speed : (timeEnd - timeBegin));Long timeBegin1 System.currentTimeMillis();StringTokenizer st new StringTokenizer(orgStr, ;);for (int i 0; i 10000; i) {while (st.hasMoreTokens()){st.nextToken();}st new StringTokenizer(orgStr, ;);}Long timeEnd2 System.currentTimeMillis();System.out.println(speed2 : (timeEnd2 - timeBegin1));}
//输出
speed :3162
speed2 :2688第二段即使StringTokenizer对象不断被销毁创建性能还是优于split
更优化的分割方式
可以用两个组合方法比如IndexOf和subString之前提到subString采用空间换时间速度比较快我们有如下实现 Long timeBegin2 System.currentTimeMillis();String tmp orgStr;while (true){String splitStr null;int j tmp.indexOf(;);if(j0) break;splitStr tmp.substring(0,j);tmp tmp.substring(j1);}Long timeEnd2 System.currentTimeMillis();
System.out.println(speed2 : (timeEnd2 - timeBegin2));
//输出
speed2 :94由此可以看出indexOf和subString方法性能远比split和StringTokenizer高很适合高频函数的使用。
高效的charAt方法
public char charAt(int index);和indexOf相反返回位置在index的字符效率和indexOf一样高同样是java内置函数的startWith和endWith效率远低于charAt方法。如下性能示意图 StringBuffer和StringBuilder
由于String对象是不可变的如果我们在做字符串修改操作时候总会生成新对象这样性能差所以有这两个用来修改字符串工具方法
String常量的累加操作
//方法一
String result String and String append;
//方法二
StringBuilder result new StringBuilder();
result.append(String);
result.append(and);
result.append(String);
result.append(append);如上代码一会生成String and appendStringand StringandStringStringandStringappend六个字符串理论上说效率低下两段代码分表指向5万次方法一小盒0ms方法二消耗15ms与预期相反反编译第一段代码
String s StringandStringappend;以上结果看出对于常量字符串累加java的编译时候就做了优化对编译时候就能确定取值的字符串操作在编译时候就进行了计算所以运行时候并没有生成大量String实例而使用StringBuffer的代码反编译后的结果和源代码王完全一致。可见运行StringBuffer对象和append方法都被如实调用所以第一段代码效率才会这么快。如果我们写如下代码避开编译优化
String str1 String;
String str2 and;
String str3 String;
String str4 append;
String result str1 str2 str3 str4;执行5万次平均耗时16ms性能与StringBuilder几乎一样我们反编译一次得到如下
String str1 String;
String str2 and;
String str3 String;
String str4 append;
String result new StringBuilder(String.valueOf(str1)).append(str2).append(str3).append(str4);可以看到对应字符串的累加java也做了相应的优化操作。实际上就是用的Stringbuilder对象来实现的字符串累加所以性能几乎一致。
构建超大的String对象
有如下字符串修改方式对比
//耗时1062ms
for (int i 0; i 10000; i) {str str i;
}
//耗时360ms
for (int i 0; i 10000; i) {str str.concat(String.valueOf(i));
}
//耗时0ms
StringBuilder sb new StringBuilder();
for (int i 0; i 10000; i) {sb.append(i);
}如上代码以及耗时与我们预期的方法一和三本相同的不一样因为一与三本质都是使用的StringBuilder为什么差距这么大我们反编译一得到的如下
for (int i 0; i 10000; i) {new StringBuilder(String.valueOf(str)).append(i).append(str3).toString();
}如上可看出每次循环都重新生成StringBuilder实例所以降低系统性能。这个也表明了String的加法操作虽然被优化但是编译器并不是万能的因此少用为妙也可以得出StringBuilder concat ,这个字符串编辑效率的方法排名。
StringBuilder和StringBuffer的选择
两个方法都实现了AbstractStringBuilder抽象类有相同对外接口最大不同是StringBuffer几乎所有方法都做了同步处理SynchronizedStringBuilder没有做同步StringBuilder效率优于StringBUffer但是多线程下StringBuilder无法保证线程安全。
容量参数
StringBuilder和StringBuffer初始化都有一个容量参数如下构造不知道的情况默认16字节
public StringBuilder(int capacity)
public StringBuffer(int capacity)追加append方法时候如果超过容量则进行扩容在AbstractStringBuilder类中如下
private int newCapacity(int minCapacity) {// overflow-conscious codeint newCapacity (value.length 1) 2;if (newCapacity - minCapacity 0) {newCapacity minCapacity;}return (newCapacity 0 || MAX_ARRAY_SIZE - newCapacity 0)? hugeCapacity(minCapacity): newCapacity;}如上扩容方法策略将原有容量大小翻倍 1 移位运算以新容量申请内存空间建新char数组然后复制原有数据到新数组。因此大对象的处理会涉及到N多次的内存复制扩容如果能评估StringBuilder的大小能避免中间的扩容复制操作提高性能。
核心数据结构
List接口 三种list的实现ArrayList VectorLinkedList类图如下 都来自AbstraList实现AbstractList直接实现了List接口 ArrayList和Vector都使用数组但是Vector增加了对多线程的支持是线程安全的Vector绝大部分方法做了线程同步理论上说ArrayList性能要好于Vector LinkedList使用双向链表实现每个元素都包含三个部分元素内容前驱表项后驱表项 增加元素分析
ArrayList源码如下
public boolean add(E e) {ensureCapacityInternal(size 1); // Increments modCount!!elementData[size] e;return true;}//扩容private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity elementData.length;int newCapacity oldCapacity (oldCapacity 1);if (newCapacity - minCapacity 0)newCapacity minCapacity;if (newCapacity - MAX_ARRAY_SIZE 0)newCapacity hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData Arrays.copyOf(elementData, newCapacity);}
add方法的性能取决于ensureExplicitCapacity 方法中的grow扩容当ArrayList足够大add效率是很高的无需扩容时候只需要执行以下两个步骤
modCount;elementData[size] e;需要扩容时候会将数组扩大到原来的1.5倍之后在进行数据的复制复制的方法利用Arrays.copyOf方法。LinkedList源码如下 /*** Pointer to last node.* Invariant: (first null last null) ||* (last.next null last.item ! null)*/
transient NodeE last; public boolean add(E e) {linkLast(e);return true;
}void linkLast(E e) {final NodeE l last; //最后一个节点复制给lfinal NodeE newNode new Node(l, e, null); //新节点前驱是llast newNode;if (l null)first newNode;//l为空标识之前没有节点那么新节点就是首个节点elsel.next newNode;//l不为空将l此时L是之前最后一个节点 后驱指针指向新节点。size;modCount;}如下last节点就是我们理解的指针用来指向最后一个节点元素每次添加节点都添加到最后LinkedList使用链表结构不需要维护容量大小无需扩容这个是对比ArrayList的优势但是每次需要新建Entry并且赋值会有一定性能影响如下实验设置jvm参数-Xmx512M -Xms512M
Object obj new Object();
for(int i0; i500000;i){last.add(obj);
}使用jvm参数是为了屏蔽GC对程序性能的干扰ArrayList耗时16msLinkedList耗时31ms不间断的生产新对象还是有一定资源损耗若不用jvm参数区别更大因为LinkedList会产生很多对象占用堆内存触发GC因此LinkedList对对内存和GC要求高。
增加元素到任意位置
List接口中有一个插入元素的方法
public void add(int index, E element)ArrayList和LinkedList在这个方法上存在非常大性能差异ArrayList数组实现需要对数据重新排列如下 public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size 1); // Increments modCount!!System.arraycopy(elementData, index, elementData, index 1,size - index);//数据移动拷贝elementData[index] element;size;}可见每次插入都需要将index后面的数据整体后移非常损耗性能并且index越靠前需要拷贝的数据越多性能损耗越大。LinkedList优势比较大如下源码
public void add(int index, E element) {checkPositionIndex(index);if (index size)linkLast(element);elselinkBefore(element, node(index));}如上代码LinkedList插入随意位置与插入队尾区别在于需要遍历链表到指定index位置无需数据复制而是对元素中前驱指针与后驱指针作修改。
删除任意位置元素
删除与添加的逻辑基本一致两个的性能取决于需要操作的数据是在List的前中后ArrayList需要复制数据在头部时候效率低LinkedList需要遍历List在中间的时候效率更低我们做如下测试得出以下结论
while(list.size() 0){list.remove(0);
}
while(list.size() 0){list.remove(list.size() 1);
}
while(list.size() 0){list.remove(list.size() - 1);
}List类型/删除位置头部中部尾部ArrayList6203312516LinkedList15878116
遍历列表
遍历有三种方式forEachIteratori.for三种方式在100万数据量下性能如下表
List类型ForEach操作迭代器for循环ArrayList63ms47ms31msLinkedList63ms47ms~~无穷大
最简单的ForEach性能不如迭代器而用for循环的情况下ArrayList性能最优LinkedList的在随机访问每次都需要一次列表的遍历操作性能会非常差无法等到运行结束。ForEach 与 迭代器比较反编译代码得到一些结果
//ForEach for (Iterator iterator list.iterator(); iterator.hasNext()){String s (String) iterator.next();String s1 s;
}
//forfor (Iterator iterator list.iterator(); iterator.hasNext()){String s (String) iterator.next();
}由上看出仅仅是多了一次赋值操作导致ForEach循环的性能更差一点。
Map接口
Map接口最主要的实现类是HashTable,HashMapLinkedHashMapTreeMap在HashTable的子类中还有Preperties类的实现。HashMap,HashTable 异同 Hashtable大部分方法做了同步处理HashMap没有因此HashMap不是线程安全HashTable不允许key或者value使用nullHashMap是允许的HashMapHashTable 的hash算法和hash值到内存索引的映射算法不同。
HashMap实现原理
将key做hash算法将hash值映射到内存地址直接取得key对应的数据。HashMap的高性能保证以下几点 hash算法必须高效的hash值到内存地址数组索引的算法是快速的更具内存地址数组索引可以直接取得对应值 首先第一点hash算法的高效性我们用get方法中hash算法代码来解释
public V get(Object key) {NodeK,V e;return (e getNode(hash(key), key)) null ? null : e.value;
}
//hash值获取
static final int hash(Object key) {int h;//调用的Object中的hashCode方法接着对取得的Hash做移位运算与原hash值异或^运算return (key null) ? 0 : (h key.hashCode()) ^ (h 16);
}先获取key的hash值,调用的Object中的HashCode方法这个方法是native的实现比一般方法都要快其他的操作都是基于位运算也是高效的注意native方法快的原因是他直接调用操作系统本地连接库的API第二点取得key的hash后续通过hash获取内存地址还是通过get方法中代码
final NodeK,V getNode(int hash, Object key) {NodeK,V[] tab; NodeK,V first, e; int n; K k;if ((tab table) ! null (n tab.length) 0 (first tab[(n - 1) hash]) ! null) {//按位与操作......}return null;
}以上get方法中table是数组将hash值和数组长度n1按位与直接得到数组索引等效于取余但是位操作性能更高这样得到数组下标取对应值第三点更具数组下标取值直接内存访问速度也快因此获取内存地址也是快速的
Hash冲突
HashMap解决Hash冲突首先结构上如下图结构HashMap内部维护一个数组在1.7 中是Entry在1.8 中是Node只是节点元素不同结构一样此处按1.8 来讲如下Node源码
static class NodeK,V implements Map.EntryK,V {final int hash;final K key;V value;NodeK,V next;Node(int hash, K key, V value, NodeK,V next) {this.hash hash;this.key key;this.value value;this.next next;}
....public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue value;value newValue;return oldValue;}public final boolean equals(Object o) {....}}如上每个Node中包含四部分hash值keyvaluenext指针当put操作时候如如上图新的Node n1会被放在对应的索引下标内并且替换原有值同时为保证旧值不丢失将Node n1 的next 指向旧的Node n2 这样就实现了一个数组索引空间内存放多个值。HashMap实际上就是一个链表的数组。并且在java1.8 时候还做了优化在链表长度达到7 的时候将链表转红黑数提高读写效率。
容量参数
容量参数对性能影响类似ArrayList和Vector技术数组结构不可避免有空间不足进行扩展扩容的过程也就会对性能消耗。HashMap 有如下构造
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()其中initialCapacity指定HashMap初始容量loadFactor指定负载因子 初始容量大小大于等于initialCapacity并且是2的指数次幂的最小整数作为内置数组的大小。负载因此叫填充比介于0~1 之间浮点数决定了HashMap在扩容前其内部数组的填充度。默认情况初始大小16负载因子0,75负载因子 元素个数/内部数组总大小 负载因子越大说明HashMap中的hash冲突就越多次数另外一个参数threshold 变量他被定义为氮气数组总容量和负载因子的乘积 threshold 内部数组总容量*负载因子 确定元素总量一个数字 依照上面的公式threshold 相当于HashMap的一个元素的阀值更具各个参数来决定超过这个阀值会开始扩容扩容代码如下
final NodeK,V[] resize() {NodeK,V[] oldTab table;int oldCap (oldTab null) ? 0 : oldTab.length;int oldThr threshold;int newCap, newThr 0;if (oldCap 0) {//超过最大值不扩容if (oldCap MAXIMUM_CAPACITY) {threshold Integer.MAX_VALUE;return oldTab;}//没超过扩容为原来2倍并且扩容后小于最大值大于默认值16else if ((newCap oldCap 1) MAXIMUM_CAPACITY oldCap DEFAULT_INITIAL_CAPACITY)newThr oldThr 1; // double threshold}else if (oldThr 0) // initial capacity was placed in thresholdnewCap oldThr;else { // zero initial threshold signifies using defaultsnewCap DEFAULT_INITIAL_CAPACITY;newThr (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//计算新的resize上线if (newThr 0) {float ft (float)newCap * loadFactor;newThr (newCap MAXIMUM_CAPACITY ft (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold newThr;SuppressWarnings({rawtypes,unchecked})NodeK,V[] newTab (NodeK,V[])new Node[newCap];table newTab;if (oldTab ! null) {//此处开始数据迁移吧每个bucket都移动到新的buckets中for (int j 0; j oldCap; j) {NodeK,V e;if ((e oldTab[j]) ! null) {oldTab[j] null;if (e.next null)newTab[e.hash (newCap - 1)] e;else if (e instanceof TreeNode)((TreeNodeK,V)e).split(this, newTab, j, oldCap);else { // preserve orderNodeK,V loHead null, loTail null;NodeK,V hiHead null, hiTail null;NodeK,V next;do {next e.next;//原索引if ((e.hash oldCap) 0) {if (loTail null)loHead e;elseloTail.next e;loTail e;}//原索引 oldCapelse {if (hiTail null)hiHead e;elsehiTail.next e;hiTail e;}} while ((e next) ! null);//将原索引放到bucket里面if (loTail ! null) {loTail.next null;newTab[j] loHead;}//原索引 oldCap放到bucket里面if (hiTail ! null) {hiTail.next null;newTab[j oldCap] hiHead;}}}}}return newTab;}LinkedHashMap有序的HashMap
因为HashMap是无序的因此有一个替代品LinkedHashMap继承自HashMap因此也具备HashMap的高性能在HashMap基础上增加一个链表存放元素顺序相当于一个维护了元素顺序的HashMap通过如下构造
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder accessOrder;}当accessOrder 为true时候按元素最后范问时间排序 当accessOrder 为false时按插入顺序排序默认false。LinkedHashMap 中的元素用的Entry继承了HashMap的Node并且在此基础扩展了beforeafter指针如下
static class EntryK,V extends HashMap.NodeK,V {EntryK,V before, after;Entry(int hash, K key, V value, NodeK,V next) {super(hash, key, value, next);}}每个Entry 2通过after指向后继元素Entry n而Entry n的before指向前驱元素Entry 2构成一个循环链表。如下图所示
TreeMap----另一种Map实现 public TreeMap(Comparator? super K comparator) {this.comparator comparator;}TreeMap功能上比HashMap更强大他实现了SortedMap的功能如上构造函数注入了一个Comparator使用一个实现了Comparable接口的key对于TreeMap而言排序是必须进行的一个过程TreeMap的性能相比HashMap更差TreeMap的排序不同于LinkedHashMapLinkedHashMap 是更具元素增加或者访问的先后顺序来排序TreeMap是根据key实现的算法进行排序。
Set接口
Set接口并没有在Collection接口上增加额外的操作Set结婚中的元素是不能重复的而且Set是比较特殊的一个他的三个实现HashSetLinkedHashSetTreeSet的实现分表是对HashMapLinkedHashMapTreeMap的一个封装以HashSet为例内部维护一个HashMap所有有关Set’的实现都委托HashMap对象完成如下构造
public HashSet() {map new HashMap();
}
public HashSet(Collection? extends E c) {map new HashMap(Math.max((int) (c.size()/.75f) 1, 16));addAll(c);
}优化集合访问代码
减少循环中相同的操作比如循环中的size等
减少方法调用
RandomAccess接口
使用NIO提升性能
虽然java提供了基于流的IO实现InputStreamOutputStream这种实现以字节为单位处理数据并且非常容易建立各种过滤器但是还是基于传统的IO凡是是系统性能的瓶颈。NIO是New IO的简称有以下特点 为所有原始类型提供Buffer缓存支持使用java.nio.charset.Charset作为字符集编码解码解决方案增加通道Channel对象作为新的原始IO抽象支持锁和内存映射文件的文件访问接口提供了基于Selector的异步网络IO 与IO不同的是NIO基于Block块的以块为基本单位处理数据NIO中最重要两个组件BufferChannel缓冲是一块连续的内存块是NIO读写数据的中转地通过标识缓存数据的源头或者目的地用于向缓冲读写。如下图 NIO的Buffer和Channel
Buffer是一个抽象类他为JDK中每种原生数据类型创建了一个BufferIntBufferByteBufferCharbufferDubboBufferFloatBufferLongBufferShortBuffer除了ByteBuffer其他每种都有完全一样的操作唯一去吧是对应的数据类型NIO中和Buffer配合使用的是Channel相当于一个双向通道读与写如下一个读文件案例 public static void nioCopyFileTest(String source, String destination) throws IOException {FileInputStream fis new FileInputStream(source);FileOutputStream fos new FileOutputStream(destination);FileChannel readChannel fis.getChannel();FileChannel writeChannel fos.getChannel();ByteBuffer byteBuffer ByteBuffer.allocate(1024);//申请堆内存while (true){byteBuffer.clear();int len readChannel.read(byteBuffer);if(len -1){break;}byteBuffer.flip();writeChannel.write(byteBuffer);}readChannel.close();writeChannel.close();}Buffer的基本原理
Buffer三个重要参数位置position容量capacity上限limit 位置position写的时候当前缓冲区的位置从position的下一个位置开始写入读取的时候当前缓冲区读取的位置从此位置后读取数据容量capacit读写的时候缓冲区总容量上线。上限limit写入时候缓冲区实际上线一般小于等于总容量通常和总容量相等读取的时候代表可读取的总容量和上次写入的数据量相等。 一下案例解析三个参数
public static void main(String[] args) {//分配15个字节ByteBuffer byteBuffer ByteBuffer.allocate(15);System.out.println(limit: byteBuffer.limit() capacity: byteBuffer.capacity() position: byteBuffer.position());for (int i 0; i 10; i) {byteBuffer.put((byte) i);}System.out.println(limit: byteBuffer.limit() capacity: byteBuffer.capacity() position: byteBuffer.position());byteBuffer.flip();//重置positionSystem.out.println(limit: byteBuffer.limit() capacity: byteBuffer.capacity() position: byteBuffer.position());for (int i 0; i 5; i) {System.out.println(byteBuffer.get());}System.out.println(limit: byteBuffer.limit() capacity: byteBuffer.capacity() position: byteBuffer.position());byteBuffer.flip();System.out.println(limit: byteBuffer.limit() capacity: byteBuffer.capacity() position: byteBuffer.position());}
/**
limit: 15capacity: 15position: 0
limit: 15capacity: 15position: 10
limit: 10capacity: 15position: 0
0
1
2
3
4
limit: 10capacity: 15position: 5
limit: 5capacity: 15position: 0
*/第一步申请15个字节大小缓冲区初始阶段如下图 Buffer中放入10个byte因此position前移10其他两个不变 flip操作重置position将buffer重写模式转为读并且limit设置到当前position位置作为读取的界限位置 五次读操作与写操作一样重置position到当前位置指定已经读取的位置 再次flip归零position同事limit设置到position位置 Buffer相关操作
Buffer的创建
ByteBuffer byteBuffer ByteBuffer.allocate(15);
byte array[] new byte[1024];
ByteBuffer b ByteBuffer.wrap(array, 0, array.length-1);重置和清空缓冲区
b.rewind();
b.clear();
b.flip();以上三个函数类似功能都重置buyffer这里说明的重置只是重置了标志位并不是清空buffer数据还是存储在buffer中。分别看一下源码
public final Buffer rewind() {position 0;mark -1;return this;}rewind 将position设置零同时清除标志位mark作用在于为提取Buffer的有效数据做准备
public final Buffer clear() {position 0;limit capacity;mark -1;return this;}clear将position设置零将limit设置为cipicity大小清除remark因为limit清空了所有无法指定buffer内那些数据是有效的这个方法用于重新写buffer做准备
public final Buffer flip() {limit position;position 0;mark -1;return this;}flip将limit设置到position位置position清零清除标志位mark一般在读写转换时候使用。
标志缓冲区
标志mark缓冲区类似书签一样的功能数据处理过程中随时纪律当前位置然后任意时刻回到这个位置加快或者简化数据处理流程。如下源码 public final Buffer mark() {mark position;return this;
}
public final Buffer reset() {int m mark;if (m 0)throw new InvalidMarkException();position m;return this;}mark用来记录当前位置reset用于恢复到mark所在的位置如下使用方法 public static void main(String[] args) {ByteBuffer byteBuffer ByteBuffer.allocate(15);for (int i 0; i 10; i) {byteBuffer.put((byte) i);}byteBuffer.flip();for (int i 0; i byteBuffer.limit(); i) {System.out.print(byteBuffer.get());if(i 4){byteBuffer.mark();System.out.println(mark at: i );}}byteBuffer.reset();System.out.println( reset to mark);while (byteBuffer.hasRemaining()){System.out.print(byteBuffer.get());}}/**
01234mark at: 4
56789reset to mark
56789*/复制缓冲区
以原有缓冲区为基础生成一个完全一样的新缓冲区
public abstract ByteBuffer duplicate();特点在于新生成的缓冲区与原缓冲区共享同一内存数据所以数据修改互相可见但两者又独立维护个字positionlimitremark使得操作更加灵活。
缓冲区分片
使用slice方法将现有缓冲区中创建新的子缓冲区字缓冲区和父缓冲区共享数据这个方法有助于将系统模块化当需要处理一个Buffer的一个片段时候可以使用slice方法取得一个子缓冲区然后单独处理。如下案例
ByteBuffer byteBuffer ByteBuffer.allocate(15);for (int i 0; i 10; i) {byteBuffer.put((byte) i);}byteBuffer.position(2);byteBuffer.limit(6);ByteBuffer byteBuffer2 byteBuffer.slice();上例子中分出的子缓冲区如图所示
只读缓冲区
asReadOnlyBuffer()方法获取与当前缓冲区一直的并且内存共享的只读缓冲区保证数据安全性。当只读缓冲区被尝试修改的时候回抛出异常
java.nio.ReadOnlyBufferException文件映射到内存
NIO提供将文件映射到内存的方法进行IO操作比常规IO流快很多由FileChannel.map()实现如下案例
public static void main(String[] args) throws IOException {RandomAccessFile raf new RandomAccessFile(D:\\sentinel.txt, rw);FileChannel fc raf.getChannel();MappedByteBuffer mbb fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());while (mbb.hasRemaining()){System.out.println((char) mbb.get());}raf.close();}将文件0~最后一位byte字节映射到对应MapperByteBuffer内存中之后直接操作内存中数据
处理结构化数据
NIO提供的结构化数据处理散射Scattering聚集Gathering。散射指将数据读入一组Buffer中。聚集指将数据写入一组Buffer中。接口实现如下ScatteringByteChannel用法通道一次填充每个缓冲区甜蜜一个后开始填充下一个类似缓冲区数组的一个大缓冲区
public long read(ByteBuffer[] dsts, int offset, int length throws IOException;
public long read(ByteBuffer[] dsts) throws IOException;如果需要创建指定格式的文件只需要先构造好大小合适的Buffer对象使用聚集写的方式可以很快的创建出文件GatheringByteChannel用法
public long write(ByteBuffer[] srcs, int offset, int length)throws IOException;public long write(ByteBuffer[] srcs) throws IOException;JDK提供的各种通道中DatagramChannelFileChannel和SocketChannel都实现了这两个接口以下FileChannel为例解释如何使用散射和聚集读写结构化文件。
public static void main(String[] args) throws IOException {ByteBuffer bookBuf ByteBuffer.wrap(java性能优化技巧.getBytes(utf-8));ByteBuffer autBuf ByteBuffer.wrap(葛一鸣.getBytes(utf-8));Integer booklen bookBuf.limit();Integer autlen autBuf.limit();ByteBuffer[] bufs new ByteBuffer[]{bookBuf, autBuf};File file new File(D:\\test.txt);if(!file.exists()){file.createNewFile();}FileOutputStream fos new FileOutputStream(file);FileChannel fc fos.getChannel();fc.write(bufs);fos.close();}以上代码建两个ByteBuffer分别存储书名和作者信息构造ByteBuffer数组使用文件通道将数组写入文件 public static void main(String[] args) throws IOException {ByteBuffer bookBuf ByteBuffer.wrap(java性能优化技巧.getBytes(utf-8));ByteBuffer autBuf ByteBuffer.wrap(葛一鸣.getBytes(utf-8));Integer booklen bookBuf.limit();Integer autlen autBuf.limit();ByteBuffer b1 ByteBuffer.allocate(booklen);ByteBuffer b2 ByteBuffer.allocate(autlen);ByteBuffer[] bufs new ByteBuffer[]{b1, b2};File file new File(D:\\test.txt);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);System.out.println(bookName : authname);}同样散射读的方式根据长度精确构造buffer通过文件通道散射读将文件信息装载到对应的Buffer中之后直接从buffer中读取信息。以上两个案例可看到通过和通道的配合使用可以简化Buffer对于结构化数据处理的难度。
MappedByteBuffer性能评估
分别用传统IO基于Buffer的IO基于内存映射的MappedByteBuffer的I三种IO模型下的效率我们用如下案例 //文件写入public static void main(String[] args) throws IOException {long bgTime System.currentTimeMillis();Integer numOfInt 4000000;FileChannel fileChannel new RandomAccessFile(D:\\test.txt, rw).getChannel();IntBuffer ib fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, numOfInt*4).asIntBuffer();for (Integer i 0; i numOfInt; i) {ib.put(i);}if(fileChannel ! null){fileChannel.close();}System.out.println(speed: (System.currentTimeMillis() - bgTime));}//文件读取
public static void main(String[] args) throws IOException {long bgTime System.currentTimeMillis();Integer numOfInt 4000000;FileChannel fileChannel new RandomAccessFile(D:\\test.txt, rw).getChannel();IntBuffer ib fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, numOfInt*4).asIntBuffer();for (Integer i 0; i numOfInt; i) {ib.get();}if(fileChannel ! null){fileChannel.close();}System.out.println(speed: (System.currentTimeMillis() - bgTime));
}此处指给出内存映射的方式文件读写基于上几章节案例对比本次耗时写入109ms读文件耗时61ms大大优于基于Buffer的IO此外基于Buffer的IO大大优于基于传统的IO。总结如下表格
使用StreamByteBufferMaooedByteBuffer写耗时1641954109独耗时129729679
直接内存访问
NIO中直接访问内存的类DireBuffer他是一个借口对应ByteBuffer的实现类是DirectByteBuffer继承自ByteBuffer如下
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer与ByteBuffer不同的是普通ByteBuffer仍然在JVM的堆内存上分配空间最大内存收到最大堆内存限制DirectBuffer直接分配在物理内存中不占用堆内存空间重要普通ByteBuffer访问系统会使用一个“内核缓冲区”进行间接操作而DirectBuffer所处的位置就相当于这个“内核缓冲区”。使用DirectBuffer是一种更接近系统底层的方法。比普通ByteBuffer更快。DirectBuffer相对于ByteBuffer访问速度更快但是创建和销毁DirectBuffer的花费远比ByteBuffer高如下测试配置JVM参数VM options-XX:PrintGCDetails -XX:MaxDirectMemorySize10M -Xmx10M public static void main(String[] args) throws IOException {long begin System.currentTimeMillis();for (int i 0; i 20000; i) {//测试一ByteBuffer b ByteBuffer.allocate(1000);//测试二ByteBuffer b ByteBuffer.allocateDirect(1000);}System.out.println(System.currentTimeMillis() - begin);}//测试一中普通ByteBuffer结果
[GC (Allocation Failure) [PSYoungGen: 2048K-488K(2560K)] 2048K-758K(9728K), 0.0009691 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K-488K(2560K)] 2806K-774K(9728K), 0.0013332 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K-488K(2560K)] 2822K-850K(9728K), 0.0009512 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K-504K(2560K)] 2898K-914K(9728K), 0.0006810 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2552K-504K(2560K)] 2962K-930K(9728K), 0.0010715 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2552K-504K(1536K)] 2978K-930K(8704K), 0.0005587 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1528K-224K(2048K)] 1954K-1184K(9216K), 0.0005771 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1248K-160K(2048K)] 2208K-1160K(9216K), 0.0003433 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1184K-32K(2048K)] 2184K-1080K(9216K), 0.0002990 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-32K(2048K)] 2104K-1080K(9216K), 0.0003482 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-32K(2048K)] 2104K-1080K(9216K), 0.0002876 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-32K(2048K)] 2104K-1080K(9216K), 0.0004324 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-0K(2048K)] 2104K-1048K(9216K), 0.0003900 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1024K-32K(2048K)] 2072K-1080K(9216K), 0.0003066 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-32K(2048K)] 2104K-1080K(9216K), 0.0002950 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1056K-32K(2048K)] 2104K-1080K(9216K), 0.0002153 secs] [Times: user0.00 sys0.00, real0.00 secs]
18
HeapPSYoungGen total 2048K, used 849K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 1024K, 79% used [0x00000000ffd00000,0x00000000ffdcc7d0,0x00000000ffe00000)from space 1024K, 3% used [0x00000000fff00000,0x00000000fff08000,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 7168K, used 1048K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 14% used [0x00000000ff600000,0x00000000ff7062e0,0x00000000ffd00000)Metaspace used 3456K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 376K, capacity 388K, committed 512K, reserved 1048576K//测试二 DirectBuffer 结果
[GC (Allocation Failure) [PSYoungGen: 2048K-488K(2560K)] 2048K-762K(9728K), 0.0011810 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (System.gc()) [PSYoungGen: 2064K-496K(2560K)] 2338K-2288K(9728K), 0.0022161 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K-0K(2560K)] [ParOldGen: 1792K-1482K(7168K)] 2288K-1482K(9728K), [Metaspace: 3464K-3464K(1056768K)], 0.0179332 secs] [Times: user0.02 sys0.00, real0.02 secs]
47
HeapPSYoungGen total 2560K, used 1276K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 62% used [0x00000000ffd00000,0x00000000ffe3f278,0x00000000fff00000)from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)ParOldGen total 7168K, used 1482K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 20% used [0x00000000ff600000,0x00000000ff772a10,0x00000000ffd00000)Metaspace used 3475K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 377K, capacity 388K, committed 512K, reserved 1048576K如上配置JVM信息 XX:MaxDirectMemorySize 指定DirectBuffer的大小最大10-Xmx指定最大堆内存10M在同等内存下DirectBuffer的相对耗时 47 Buffer耗时18可得到频繁创建销毁DirectBuffer的代价更大测试二中DirectBuffer的GC信息简单因为GC只记录堆空间内存回收由于DirectBuffer占用内存并不在堆内存中因此对操作更少日志自然少但是DirectBuffer对象本身还是在堆空间分配只是他指向的内存地址不在对内存空间范围内而已。
引用类型
引用类型分四种 强引用软引用弱引用虚引用
强引用
java引用类似指针通过引用可以对堆中的对象进行操作如下
StringBuffer str new StringBuffer(hello world);
StringBuffer str1 str;以上代码局部变量str分配在栈上对象StringBuilder实例分配在堆内存str会执行StringBuffer所在堆内存空间通过str操作改实例str1 也同时指向StringBuffer实例内存也就是有两个引用str与str1,如上两个引用都是强引用有如下特点 强引用可以直接访问目标对象强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常也不会回收强引用指向的对象强引用可能导致内存泄露
软引用
软引用是通过java.lang.ref.SoftReference使用软引用一个持有软引用的对象不会被jvm回收jvm会判断当堆内存使用临近阀值时候。才会去回收软引用的对象。只有内存足够理论上存活相当长的时间。基于软引用特点我们可以利用软引用来实现对内存敏感的Cache
弱引用
弱引用比软引用更弱在系统GC时只要发现弱引用不管系统堆空间是否足够都会讲对象回收。但是GC的线程通常优先级不高因此并不一定能很快发现持有弱引用的对象。一旦弱引用对象被GC回收便会加入到一个注册引用队列中。软引用弱引用都非常适合来保存那些可有可无的缓存数据如果这么做当系统内存不足时候这些缓存数据会被回收不会导致内存溢出。而当内存资源充足时候这些缓存数据又可以存在相当长的时间从而起到加速系统的作用。
虚引用
虚引用是引用中最弱一个和没有引用几乎一样随时可能被GC回收。当通过虚引用的get()方法去的强引用对象时候总是失败的并且虚引用必须和引用队列一起使用.虚引用的作用在于跟踪垃圾回收过程。清理被销毁对象的相关资源。通常对象不被使用时候重载该类的finalize方法可以回收对象的资源。但是如果finalize方法使用不慎可能导致回收失效对象复活的情况如下案例
public class CanReliveObj {public static CanReliveObj obj;Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println(CanreliveObj finalize called);obj this;}Overridepublic String toString(){return i am CanReliveObj;}public static void main(String[] args) throws InterruptedException {obj new CanReliveObj();obj null;System.gc();Thread.sleep(1000);if(obj null){System.out.println(obj is null);}else {System.out.println(obj is alieve);}System.out.println(second gc);
// obj null;System.gc();Thread.sleep(1000);if(obj null){System.out.println(obj is null);}else {System.out.println(obj is alieve);}}
}
//输出
CanreliveObj finalize called
obj is alieve
second gc
obj is alieve如上案例输出显示obj对象两次gc后还是存活状态我们在第二次gc前加上objnull得到结果才能回收obj对象通过objnull去除强引用由此可看出在复杂应用系统中一旦finalize方法实现有问题容易造成内存泄露。而虚引用则不会有这种情况因为他实际上是已经完成了对象的回收工作的。
WeakHashMapl类及其实现
WeakHashMap类在java.util包内他实现了Map接口是HashMap的一种实现他时使用弱引用作为内部数据的存储方案如下定义源码
public class WeakHashMapK,Vextends AbstractMapK,Vimplements MapK,V {.....}用处如果系统中需要一个很大的Map表Map中的表项作为缓存用这种场景下使用WeakHashMap是比较合适因为WeakHashMap会在系统内存范围保存所有表项而内存一旦不够GC清楚未被引用的表项避免内存泄露。如下测试HashMap与WeakHashMap区别我们设置-Xxm 5M来限制堆内存信息
//案例一public static void main(String[] args) {MapInteger, Byte[] map new WeakHashMap();for (int i 0; i 10000; i) {Integer ii new Integer(i);map.put(ii, new Byte[i]);}}//GC日志堆栈信息
[GC (Allocation Failure) [PSYoungGen: 1024K-504K(1536K)] 1024K-656K(5632K), 0.0406638 secs] [Times: user0.00 sys0.00, real0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 1524K-504K(1536K)] 1677K-806K(5632K), 0.0012177 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1528K-504K(1536K)] 1830K-1878K(5632K), 0.0010152 secs] [Times: user0.11 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1526K-504K(1536K)] 2901K-3014K(5632K), 0.0010945 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1528K-504K(1536K)] 4038K-3960K(5632K), 0.0013167 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 504K-0K(1536K)] [ParOldGen: 3456K-3416K(4096K)] 3960K-3416K(5632K), [Metaspace: 3422K-3422K(1056768K)], 0.0076172 secs] [Times: user0.00 sys0.00, real0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 1021K-0K(1536K)] [ParOldGen: 3416K-1915K(4096K)] 4438K-1915K(5632K), [Metaspace: 3437K-3437K(1056768K)], 0.0047949 secs] [Times: user0.01 sys0.00, real0.00 secs]
.....
HeapPSYoungGen total 1536K, used 1441K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)eden space 1024K, 91% used [0x00000000ffe00000,0x00000000ffeeaae8,0x00000000fff00000)from space 512K, 98% used [0x00000000fff00000,0x00000000fff7dd00,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 4096K, used 3114K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)object space 4096K, 76% used [0x00000000ffa00000,0x00000000ffd0a9b8,0x00000000ffe00000)Metaspace used 3458K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 376K, capacity 388K, committed 512K, reserved 1048576K因为限制堆内存来实验可以看到没固定一段时间后就会触发Full GC因为内存不够会将之前申请的所有key都释放以维持内存需求避免OOM
//案例二
public static void main(String[] args) {MapInteger, Byte[] map new HashMap();for (int i 0; i 10000; i) {Integer ii new Integer(i);map.put(ii, new Byte[i]);}}[GC (Allocation Failure) [PSYoungGen: 1024K-504K(1536K)] 1024K-616K(5632K), 0.0009105 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1528K-496K(1536K)] 1640K-862K(5632K), 0.0007723 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1518K-504K(1536K)] 1885K-1862K(5632K), 0.0011210 secs] [Times: user0.00 sys0.00, real0.00 secs]
.......
[Full GC (Allocation Failure) Exception in thread main [PSYoungGen: 1023K-1023K(1536K)] [ParOldGen: 4091K-4091K(4096K)] 5115K-5115K(5632K), [Metaspace: 3443K-3443K(1056768K)], 0.0025873 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 1023K-0K(1536K)] [ParOldGen: 4095K-874K(4096K)] 5119K-874K(5632K), [Metaspace: 3449K-3449K(1056768K)], 0.0039615 secs] [Times: user0.00 sys0.00, real0.00 secs]
Heapat com.ljm.resource.nio.WeakHashmapDemo.main(WeakHashmapDemo.java:16)PSYoungGen total 1536K, used 66K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe10808,0x00000000fff00000)from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 4096K, used 874K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)object space 4096K, 21% used [0x00000000ffa00000,0x00000000ffadaa08,0x00000000ffe00000)Metaspace used 3480K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 379K, capacity 388K, committed 512K, reserved 1048576K案例二中用的HashMap运行语段时间后直接OOM因为5M堆内存显然不够用此处就体现出WeakHashMap的优势一下分析JDK中WeakHashMap的源码如下Entry的定义片段
private static class EntryK,V extends WeakReferenceObject implements Map.EntryK,V {V value;final int hash;EntryK,V next;/*** Creates new entry.*/Entry(Object key, V value,ReferenceQueueObject queue,int hash, EntryK,V next) {super(key, queue);this.value value;this.hash hash;this.next next;}.....}Entry的模式还是沿用jdk1.6以及之前的HashMap的一个Entry结构并且Entry继承了WeakReference在构造函数中构造了Key的弱引用此外WeakHashMap各项操作get(),put()等直接或者间接的调用了expungeStaleEntries 函数用来清理持有的弱引用的key的表项如下源码
private void expungeStaleEntries() {for (Object x; (x queue.poll()) ! null; ) {synchronized (queue) {SuppressWarnings(unchecked)EntryK,V e (EntryK,V) x;int i indexFor(e.hash, table.length);//找到这个表项的位置EntryK,V prev table[i];EntryK,V p prev;while (p ! null) {//移除以及被回收的表项EntryK,V next p.next;if (p e) {if (prev e)table[i] next;elseprev.next next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value null; // Help GCsize--;break;}prev p;p next;}}}}总结WeakHashMap使用弱引用可以自动释放以及被回收的key所在的表项但如果WeakHashMap的key都在系统内持有强引用那么WeakHashMap就退化成了普通的HashMap因为所有的表项都无法被自动清理。
有助于改善性能的技巧
慎用异常
使用局部变量
调用方法传递的参数以及调用中创建的临时变量都保存在栈Stack中速度较快。其他变量如静态变量实例变量等都在堆Heap中创建速度较慢
位运算代替乘除法
所有运算中位运算最为高效如下优化:
//方案一
long a 100;
for(int i0; i100000; i){a2;a/2;
}
//方案二
long a 100;
for(int i0; i100000; i){a1;a1;
}看两段代码执行相同功能第一段普通运算耗时219ms第二段位运算耗时31ms
一维数组替代二维数组
提取表达式避免重复计算
布尔运算代替位运算
位运算速度高于算数运算但是条件判断时候 布尔运算速度高于位运算java对布尔运算做了充分优化比如abc布尔运算abc更具逻辑与操作只有一个false立刻返回false因此a为false立刻返回false当bc运算需要消耗大量系统资源时候这种处理方式效果最大化同理计算a||b||c逻辑或时候一个未true返回true。位运算虽然位运算效率也搞但是位运算只能将所有子表达式全部计算完成后在给出最终结果。因此从这个角度说使用位运算替代布尔运算可能会有额外消耗。如下案例对比
public static void main(String[] args) {boolean a false;boolean b true;int d 0;for (int i 0; i 1000000; i) {if(abjavaperform.contains(java)){ //位运算d0;}}}public static void main(String[] args) {boolean a false;boolean b true;int d 0;for (int i 0; i 1000000; i) {if(abjavaperform.contains(java)){ //布尔运算d0;}}}
//位运算耗时250ms
//布尔运算耗时16ms使用arrayCopy
数组赋值功能JDK提供的高效API public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);System.arraycopy()函数时native函数通常native函数性能要优于普通的函数仅处于性能考虑在软件开发时候应尽可能调用native函数。
使用Buffer进行I/O操作
处理NIO外使用java进行IO操作有两种基础方式 使用inputStream与OutputStream使用Writer和Reader 两种方式都应该合理配合使用缓冲能有效提高IO性能。以文件IO为例如下图组件 如下测试demo
public static void main(String[] args) throws Exception {
// DataOutputStream dos new DataOutputStream(new BufferedOutputStream(new FileOutputStream(D:\\test.txt)));DataOutputStream dos new DataOutputStream(new FileOutputStream(D:\\test.txt));long start System.currentTimeMillis();for (int i 0; i 10000; i) {dos.writeBytes(String.valueOf(i) \r\n);}dos.close();System.out.println(speed time: (System.currentTimeMillis() - start));start System.currentTimeMillis();
// DataInputStream dis new DataInputStream(new BufferedInputStream(new FileInputStream(D:\\test.txt)));DataInputStream dis new DataInputStream(new FileInputStream(D:\\test.txt));while (dis.readLine() ! null){
// System.out.println(dis.readLine());}dis.close();System.out.println(speed time: (System.currentTimeMillis() - start));}没有Buffer的耗时分别是203 138 添加缓存BufferInputStream的方式耗时分别是11,10 读写都差了一个数量级很明显的性能提升。并且FileReader和FileWriter的性能要优于直接使用FileInputStream和FileOutputStream用同样代码可以得出
使用clone()代替new
java中新建对象一般new关键字但是如果对象构造函数过于复杂会导致对象实例化变得十分耗时且消耗资源此时Object.clone方法会更好的选择。原因在于Object.clone()方法可以绕过对象构造方法快速复制一个对象实例跳过了构造函数对性能的影响
静态方法替换实例方法
使用static字段修饰的方法是静态方法java中由于实例方法需要维护一张类似虚拟函数表的结构以实现对多台的支持。与今天方法相比实例方法的调用需要更多资源。工具方法没有对其进行重载的必要更适合于用作为静态方法。
并行程序开发及优化
并行程序设计模式
常见的并行设计模式有Future模式Master-WorkerGuarded Suspeionsion不变模式生产者消费者模式
Future模式 简单说就是一个异步获取的流程在main函数中利用Future提交一个任务线程task但是这个任务执行是有耗时的并不会立刻返回此时main函数无需等待或者阻塞可以继续main函数之后的逻辑等结果是必要因素的时候通过Future.get来阻塞等待获取对应信息。如下图解 如下源码自己实现的一个Future模式实现
public interface Data {public String getResult();
}public class RealData implements Data {protected final String result;public RealData(String para){StringBuffer sb new StringBuffer();for (int i 0; i 10; i) {sb.append(para);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}result sb.toString();}Overridepublic String getResult() {return result;}
}public class FutureData implements Data {protected RealData realData null;protected boolean isReady false;public synchronized void setRealData(RealData realData){if(isReady){return;}this.realData realData;isReady true;notifyAll();}Overridepublic String getResult() {while (!isReady){try {wait();} catch (InterruptedException e) {e.printStackTrace();}}return realData.result;}
}public class Client {public Data request(final String queryStr){final FutureData futureData new FutureData();new Thread(){Overridepublic void run(){RealData realData new RealData(queryStr);futureData.setRealData(realData);}}.start();return futureData;}
}public class MainTest {public static void main(String[] args) {Client client new Client();Data data client.request(my name);System.out.println(request over);try {System.out.println(sleep 2000ms);Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(data data.getResult());}
}以上代码中FutureData实现了一个快速返回的RealData包装。他只是一个包装或者说是一个RealData的虚拟实现并且设置FutureData的getReault的时候回被wait阻塞等待RealData被注入才能返回FutureData是Future模式的关键时间上是真实数据RealData的代理封装了RealData的等待过程。UML图如下
JDK的内置实现 Future模式在JDK的并发包中内置了一个实现比以上Demo更加复杂如下类图 其中FutureTask类重要类,FutureTask提供线程控制的功能
public boolean cancel(boolean mayInterruptIfRunning)
public boolean isCancelled()
public boolean isDone()
public V get() throws InterruptedException, ExecutionException
public V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException我们将上一步骤中自己的Demo用JDK的Future实现。如下改进的代码变更简单直接通过RealData构造FutureTask将其作为单独线程运行通过FutureTask.get()方法获取结果。
public class NewRealData implements CallableString {private String para;public NewRealData(String para){this.para para;}Overridepublic String call() throws Exception {StringBuffer sb new StringBuffer();for (int i 0; i 10; i) {sb.append(para);Thread.sleep(100);}return sb.toString();}
}public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTaskString futureTask new FutureTask(new NewRealData(my name));ExecutorService executor Executors.newFixedThreadPool(1);executor.submit(futureTask);System.out.println(request over);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(data futureTask.get());}
Future模式核心优势在于去除了调用方的等待时间,并使得原本需要等待的时间段可以用于处理其他的业务逻辑从而充分利用计算机资源。
Masher-Worker模式
核心思想两部分一个Master进程和一个Worker进程Master负责接收分配任务Worker负责处理任务Worker处理完后结果返回给Master由Master进程做归纳和汇总从而得到最终结果。优点能够将大任务切分执行提高系统吞吐量每个子任务都是异步处理无需Client等待如下流程
Master-Worker模式结构
维护一个Worker进程队列不断处理新任务由master进程维护Worker队列类似线程池如图 自实现Demo
public class Worker implements Runnable {protected QueueObject workQueue;protected MapString, Object resultMap;public void setWorkQueue(QueueObject workQueue) {this.workQueue workQueue;}public void setResultMap(MapString, Object resultMap) {this.resultMap resultMap;}//子任务处理逻辑子类中实现具体逻辑public Object handle(Object input) {return input;}Overridepublic void run() {while (true) {//获取子任务Object input workQueue.poll();if (input null) {break;}//处理子任务Object re handle(input);//处理结果写入结果集resultMap.put(Integer.toString(input.hashCode()), re);}}
}public class Master {//任务队列protected QueueObject workQueue new ConcurrentLinkedQueue();//worker进程队列protected MapString, Thread threadMap new HashMap();//子任务处理结果集,此处必须用ConcurrentHashMap否则多线程put必然存在线程不安全问题造成size值与实际数据量不符合。protected MapString, Object resultMap new ConcurrentHashMap();//所有子任务结束public boolean isComplete(){for (Map.EntryString, Thread stringThreadEntry : threadMap.entrySet()) {if(stringThreadEntry.getValue().getState() ! Thread.State.TERMINATED){return false;}}return true;}//Master的构造需要一个Worker进程逻辑需要Worker进程数public Master(Worker worker, int countWroker){worker.setResultMap(resultMap);worker.setWorkQueue(workQueue);for (int i 0; i countWroker; i) {threadMap.put(Integer.toString(i), new Thread(worker, Integer.toString(i)));}}//提交一个任务public void submit(Object obj){workQueue.add(obj);}//返回子任务结果集public MapString, Object getResultMap(){return resultMap;}//运行所有worker进程进行处理public void execute(){for (Map.EntryString, Thread stringThreadEntry : threadMap.entrySet()) {stringThreadEntry.getValue().start();}}}利用如上Demo计算1~100 立方和
public class PlusWorker extends Worker {Overridepublic Object handle(Object input){Integer i (Integer) input;return i*i*i;}
}public class MainTest {public static void main(String[] args) {Master m new Master(new PlusWorker(), 5);for (int i 0; i 100; i) {m.submit(i);}m.execute();int re 0;MapString, Object resultMap m.getResultMap();while (resultMap.size() 0 || !m.isComplete()){SetString keys resultMap.keySet();String key null;for (String s : keys) {key s;break;}Integer i null;if(key ! null){i (Integer)resultMap.get(key);}if(i! null){rei;}if(key ! null){resultMap.remove(key);}}System.out.println(re);}
}
Guarded Suspension 模式 保护暂停模式当服务进程准备好时候才提供服务。如下场景服务器吞吐量有限的情况下客户端不断的请求可能会超时丢失请求次数我们将客户端请求进入队列服务端一个一个处理。如下工作流图 ResquestQueue队列用来缓存请求使这种模式可以确保系统仅在有能力处理任务时候才获取队列这任务执行其他时间则等待。类似MQ提供的生产者消费者模式。如上模式并没有返回值的获取有一定缺陷如下改进流程图 如上添加了Data类型其实是参照了Future模式代码一样此模式环境系统压力将系统负载再时间轴上均匀分布可以做到流量削峰的效果。
不变模式
为了在多线程环境下对象读写的线程安全性如果新建的对象中属性是不可更改的这样即使在高并发情况下也可以做到天生的线程安全。并且这中模式实现也简单满足如下要求 去除setter方法以及所有修改自身属性方法将所有属性设置私有用final标记确保不可修改确保没有子类可以重载修改它的行为有一个可以创建完整对象的构造函数。 不变模式在JDK中使用比较多比如基本数据类型和String类型中都是定义的final但是不变模式是通过回避问题而不是解决问题的态度来处理多线程并发访问控制可以在需求允许的情况下提高系统并发性能和并发量
生产者-消费者模式
经典的多线程设计模式该模式中有两类线程若干个生产者线程和若干消费者线程在两者中间共享内存缓冲区生产者和消费者互相不直接通信通过内存缓冲区来交换信息这样即使生产消费的速度不一致也不会影响业务的正常进行。
public class PCData {private final int intData;public PCData(int d ){intData d;}public PCData(String d){intData Integer.valueOf(d);}public int getIntData() {return intData;}Overridepublic String toString() {return PCData{ intData intData };}
}//生产者
public class Producer implements Runnable {private volatile boolean isRunning true;private BlockingQueuePCData queue;private static AtomicInteger count new AtomicInteger();private static final int SLEEPTIME 1000;public Producer(BlockingQueuePCData blockingQueue) {this.queue blockingQueue;}Overridepublic void run() {PCData data null;Random r new Random();System.out.println(start producer id Thread.currentThread().getName());try {while (isRunning) {Thread.sleep(r.nextInt(SLEEPTIME));data new PCData(count.incrementAndGet());System.out.println(data is put into queue);if (!queue.offer(data, 2, TimeUnit.SECONDS)) {System.out.println(failed to put data: data);}}} catch (Exception e) {e.printStackTrace();Thread.currentThread().interrupt();}}public void stop(){isRunning false;}
}
//消费者
public class Consumer implements Runnable {private BlockingQueuePCData queue;private static final int SLEEPTIME 1000;public Consumer(BlockingQueuePCData queue){this.queue queue;}Overridepublic void run() {System.out.println(start consumer id Thread.currentThread().getId());Random r new Random();try{PCData pcData queue.take();if(null ! pcData){int re pcData.getIntData() * pcData.getIntData();System.out.println(MessageFormat.format({0} * {1} {2}, pcData.getIntData(), pcData.getIntData(), re));Thread.sleep(r.nextInt(SLEEPTIME));}}catch (Exception e){e.printStackTrace();Thread.currentThread().interrupt();}}
}//run
public static void main(String[] args) throws InterruptedException {BlockingQueuePCData queue new LinkedBlockingQueue(10);Producer producer1 new Producer(queue);Producer producer2 new Producer(queue);Producer producer3 new Producer(queue);Consumer consumer1 new Consumer(queue);Consumer consumer2 new Consumer(queue);Consumer consumer3 new Consumer(queue);ExecutorService executorService Executors.newCachedThreadPool();executorService.execute(producer1);executorService.execute(producer2);executorService.execute(producer3);executorService.execute(consumer1);executorService.execute(consumer2);executorService.execute(consumer3);Thread.sleep(10*1000);producer1.stop();producer2.stop();producer3.stop();Thread.sleep(3000);executorService.shutdown();}生产者消费者模式能很好对生产者线程和消费者现在进行解耦优化了系统整体结构同事由于缓冲区作业运行
JDK多任务执行框架
JDK提供了用于多线程管理的线程池
无限制线程池的缺陷
多线程的确可以最大限度的利用多核处理器的计算能了但同时线程的创建销毁时有系统开销的需要消耗内存占用CPU资源当线程创建无限制时候反而会耗尽CPU和内存导致正常业务无法进行。实际生产环境中线程数必须得到控制盲目地大量创建线程对系统性能是有伤害的。
简单线程池实现
线程池作用在于在多线程环境下避免线程不断创建和销毁所带来的额外开销。有线程池的存在当系统需要一个线程时候并不立刻创建线程而是先去线程池查找是否有空余若有直接使用若没有则将任务放入等待队列或者创建新线程任务完成后将线程放回线程池。简单线程池Demo
/*** author liaojiamin* Date:Created in 10:33 2020/4/15*/
public class ThreadPool {private static ThreadPool instance null;//空闲队列private ListPThread idleThreads;//线程总数private int threadCounter;private boolean isShutDown false;private ThreadPool(){this.idleThreads new Vector(5);threadCounter 0;}public int getCreatedThreadsCount(){return threadCounter;}//获取线程池实例public synchronized static ThreadPool getInstance(){if(instance null){instance new ThreadPool();}return instance;}//结束池中所有线程public synchronized void shutDown(){isShutDown true;for (int i 0; i idleThreads.size(); i) {PThread idleThread (PThread) idleThreads.get(i);idleThread.shutDown();}}//将线程放入池中protected synchronized void repool(PThread repoolingThread){if(!isShutDown){idleThreads.add(repoolingThread);}else {repoolingThread.shutDown();}}//执行任务public synchronized void start(Runnable target){PThread thread null;//有空闲闲置至今获取最后一个使用if(idleThreads.size() 0){int lastIndex idleThreads.size() - 1;thread (PThread) idleThreads.get(lastIndex);idleThreads.remove(lastIndex);thread.setTarget(target);}else {//没有空闲线程新增一个线程并使用threadCounter ;thread new PThread(target, PThread # threadCounter, this);thread.start();}}
}public class PThread extends Thread {//线程池private ThreadPool threadPool;//任务private Runnable target;private boolean isShutDown false;private boolean isIdle false;public PThread(Runnable target,String name, ThreadPool pool){super(name);this.threadPool pool;this.target target;}public Runnable getTarget(){return target;}public boolean isIdle(){return isIdle;}Overridepublic void run(){//只有没有关闭一直运行不结束线程while (!isShutDown){isIdle false;if(target ! null){//运行任务target.run();}//结束修改闲置状态isIdle true;threadPool.repool(this);synchronized (this){try {//等待新任务wait();} catch (InterruptedException e) {e.printStackTrace();}}isIdle false;}}public synchronized void setTarget(Runnable newTarget){target newTarget;//设置任务后通知run方法开始执行任务notifyAll();}public synchronized void shutDown(){isShutDown true;notifyAll();}
}public class MyThread implements Runnable{protected String name;public MyThread(){}public MyThread(String name){this.name name;}Overridepublic void run() {try {Thread.sleep(100);System.out.println(run target targetname: Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {for (int i 0; i 1000; i) {ThreadPool.getInstance().start(new MyThread(testThreadPool Integer.toString(i)));//new Thread(new MyThread(testThreadPool Integer.toString(i))).start();}}
}以上用线程池做1000 个任务与直接创建1000个线程做对比线程池花费218 ms 普通453ms简单线程池的实现看起来效率更高
Executor框架 其实JDK中提供了一套Executor框架帮助开发人员有效的进行线程控制如下ExecutorsUML图 我们用executors来执行上面的demo
ExecutorService executorService Executors.newCachedThreadPool();for (int i 0; i 1000; i) {executorService.execute(new MyThread(testJDKThreadPool Integer.toString(i)));}运行的世界和上面线程池Demo近似265msExecutors工厂类还有如下方法 public static ExecutorService newFixedThreadPool(int nThreads) public static ExecutorService newWorkStealingPool(int parallelism) public static ExecutorService newSingleThreadExecutor()public static ExecutorService newCachedThreadPool()public static ScheduledExecutorService newSingleThreadScheduledExecutor()public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)以上工厂方法分表返回具有不同工种特性的线程池 newFixedThreadPool()方法返回一个固定线程数量的线程池改线程池中线程数不变当有一个新任务提交线程池有空闲则只需没有则添加到等待队列等空闲之后才处理在队列中的任务newSingleThreadExecutor()返回一个只有一个线程的线程池多余任务提交放入一个FIFO先入先出的顺序队列的任务中newCacheThreadPool()返回一个可根据实际情况调整线程数量的线程池线程池数量不确定有空闲则复用没有则会建新线程处理任务所有线程执行完放回线程池中newSingleThreadScheduledExecutor()返回一个ScheduledExecutorServer对象线程池大小1ScheduledExecutorService接口在ExecutorService接口上扩展了在给定实际执行某任务的功能比如在某固定延迟后执行或者周期性来执行任务。newScheduledThreadPool返回一个ScheduledExecutorService对象但是可以指定线程数量。 如没有特殊要求一般使用JDK内置线程池。不自己实现
自定义线程池
线程池底层实现用最多的是ThreadPoolExecutor其中newFixedThreadPool newSingleThreadExecutornewCachedThreadPool都是使用的我们来看ThreadPoolExecutor的构造。
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueueRunnable workQueue,ThreadFactory threadFactory) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);}依次介绍参数含义 corePoolSize指定线程池中核心线程数量maximumPoolSize指定线程池中最大线程数keepAliveTime当线程池数超过corePoolSize多余的空闲线程存活时间。也就是超过corePoolSize的空闲线程在多久空闲时间后会被销毁unitkeepAliveTime的单位workQueue任务队列被提及尚未被执行的任务threadFactory线程工厂用于创建线程一般用默认即可handler拒绝策略任务太多来不及处理如何拒绝任务。 workQueue是一个BlockingQueue接口对象用于存放Runnable对象可以用一下几个BlockingQueue 直接提交的队列这个由SynchronousQueue对象提供他是一个特殊BolckingQueue没有容量每次insert操作都需要等待delete操作反之也是同样。SynchronousQueue不保存任务只是将任务提交给线程执行没有空闲线程则创建直到达到最大线程数执行拒绝策略。有界任务队列可以使用ArrayBlockingQueue实现。ArrayBlockingQueue的构造函数必须带一个容量参数标识最大容量。 当使用有界队列时候线程池中的情况有新任务需要执行如果线程池现有线程数corePoolsize则新建线程如果线程池现有线程数corePoolSize将任务加入等待队列如果等待队列满则无法加入在总线程 maximumPoolSize前提下新建线程执行任务如果此时现有线 maximumPoolSize执行拒绝策略。 无界队列LinkedBlockingQueue无上限的一个队列除非耗尽了系统资源否则无界队列不存在无法入队列情况但是按照上面的规则线程数量一直都会是corePoolSize的数量。优先队列带有执行优先级的队列通过PriorityBlockingQueue实现可控制任务的先后顺序特殊的无界队列总是确保最高优先级的任务先执行。 依据上面的规则我们在来看JDK中几个线程池的实现newFixedThreadPool实现,返回一个corePoolSize和maximimPoolSize一样大的使用LinkedBlockingQueue任务队列线程池从而他不存在线程数的动态变化而且他用的无界队列任务提交频繁时候迅速耗尽资源
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable());}newSingleThreadExecutor方法corePoolSize和maximumPoolSize都是1 说明他永远只有一个线程存活队列用的LinkedBlockingQueue也存在资源耗尽情况
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable()));}newCacheThreadPool方法,corePoolSize为0 maximumPoolSize看成无穷大意味着任务提交时候回用空闲线程执行如果没有空闲则加入SynchronousQueue队列这种队列的特定是无容量的也就是说没有空闲线程也不会入队列而是直接创建线程这中线程池也存在资源耗尽的危险。
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueueRunnable());}拒绝策略
ThreadPoolExecutor最后一个参数指定拒绝策略当任务超过系统承载的处理策略JDK内置了四种策略。 AborPolicy策略抛出异常组织系统正常工作这个也是默认策略CallerRunsPolicy只要线程池未关闭该策略直接在调用者线程运行当前被丢弃的任务DiscardOledestPolicy丢弃最老的一个请求也就是即将被执行的一个任务并尝试再次提交当前任务Discardpolicy直接丢弃无法处理的任务不做任务处理。 以上策略都实现了RejectedExecutionHandler如果这几个都不满足业务我们可以自己实现
public interface RejectedExecutionHandler {void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}以下用优先级队列的Demo
public class MyThread implements Runnable, ComparableMyThread{private String name;public MyThread(){}public MyThread(String name){this.name name;}Overridepublic int compareTo(MyThread o) {int me Integer.parseInt(this.name.split(_)[1]);int other Integer.parseInt(o.name.split(_)[1]);if(me other) {return 1;}else if(me other) {return -1;}else {return 0;}}Overridepublic void run() {try {Thread.sleep(100);System.out.println(name );} catch (InterruptedException e) {e.printStackTrace();}}
}public class MainTest {public static void main(String[] args) {ExecutorService executorService new ThreadPoolExecutor(100,200 , 0L, TimeUnit.SECONDS,new PriorityBlockingQueueRunnable());for (int i 0; i 1000; i) {executorService.execute(new MyThread(testThreadPoolExecutor3_ Integer.toString((999-i))));}}
}/**
testThreadPoolExecutor3_989
testThreadPoolExecutor3_998
testThreadPoolExecutor3_999
testThreadPoolExecutor3_997
testThreadPoolExecutor3_996
testThreadPoolExecutor3_995
testThreadPoolExecutor3_973
testThreadPoolExecutor3_936
testThreadPoolExecutor3_970
......
testThreadPoolExecutor3_1
testThreadPoolExecutor3_6
testThreadPoolExecutor3_8
testThreadPoolExecutor3_9
testThreadPoolExecutor3_5
testThreadPoolExecutor3_11
*/调度中线程以999,998~0 的顺序加入以上是一个输出片段可以看到在省略号之前都是按照我们预期的顺序执行的之后是不规则顺序那是因为我们设置了初始线程数量是100也就是刚开始的时候并不会直接放入队列中而是用现有的线程执行。由此可见使用自定义线程池可以提供更灵活的任务处理和电镀方式。在JDK内置线程池无法满足应用需求的时候则可以考虑使用自定义线程池。
优化线程池大小
对线程池的大小设置需要考虑CPU数量内存JDBC链接等因素在并发编程实战的数字有如下公式
Ncpu cpu数量
Ucpu 目标Cpu的使用率 0 Ucpu 1
W/C 等待时间与计算时间的比率为了保持处理器达到期望的使用率最优的线程池的大小由如下公式得出在java中我们用二方法得到
//公式
Nthread Ncpu * (1W/C)
//方法
Runtimg.getRuntime().availableProcessors();扩展ThreadPoolExecutor
ThreadPoolExecutor是一个可扩展的线程池提供了三个可扩展接口
protected void beforeExecute(Thread t, Runnable r)
protected void afterExecute(Runnable r, Throwable t)
protected void terminated()ThreadPoolExecutor.Worker.runWorker方法有如下实现部分代码: final void runWorker(Worker w) {Thread wt Thread.currentThread();Runnable task w.firstTask;w.firstTask null;w.unlock(); // allow interruptsboolean completedAbruptly true;try {while (task ! null || (task getTask()) ! null) {w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted. This// requires a recheck in second case to deal with// shutdownNow race while clearing interruptif ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() runStateAtLeast(ctl.get(), STOP))) !wt.isInterrupted())wt.interrupt();try {//运行前beforeExecute(wt, task);Throwable thrown null;try {task.run();} catch (RuntimeException x) {thrown x; throw x;} catch (Error x) {thrown x; throw x;} catch (Throwable x) {thrown x; throw new Error(x);} finally {//运行结束afterExecute(task, thrown);}} finally {task null;w.completedTasks;w.unlock();}}completedAbruptly false;} finally {processWorkerExit(w, completedAbruptly);}}Worker是ThreadPoolExecutor的一个内部类实现Runnable接口ThreadPoolExecutor线程池中的工作线程也就是WorkerWorker.runWorker()会被线程池中多线程异步调用所以beforeafter方法也会被多线程范文ThreadPoolExecutor实现中提供空的before和after方法实际应用我们可以对他扩展实现对线程状态的跟踪比如增加一些日志的输出等。如下demo
public class MyThreadPoolExecutor extends ThreadPoolExecutor {public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}Overrideprotected void beforeExecute(Thread t, Runnable r){System.out.println(beforeExecute myThread name: ((MyThread)r).getName() TID: t.getId());}Overrideprotected void afterExecute(Runnable r, Throwable t){System.out.println(afterExecute TID: Thread.currentThread().getId());System.out.println(afterExecute PoolSize: this.getPoolSize());}
}以上扩展方法带有日志输出功能更好排查问题。
JDK并发数据结构
并发List
Vector或者CopyOnWriteArrayList是线程安全的ListArrayList是非线程安全的应该避免在多线程环境中使用如果业务需要我们可以用Collections.synchronizedList(List list)进行包装CopyOnWriteArrayList和Vector的内部实现是不同的CopyOnWriteArrayList是一种比较特殊的实现例如get()方法如下
//CopyOnWriteArrayList源码
private E get(Object[] a, int index) {return (E) a[index];
}
public E get(int index) {return get(getArray(), index);
}
//Vectorpublic synchronized E get(int index) {if (index elementCount)throw new ArrayIndexOutOfBoundsException(index);return elementData(index);}如上源码对比来看CopyOnWriteArrayList的get()方法并没有任何锁操作而对比Vector的get()是用了Synchronized所有get操作之前必须取得对象锁才能进行。在高并发读的时候大量锁竞争非常消耗系统性能我们可以得出在读性能上CopyOnWriteArrayList是更优的一种方式。 接下来看下两个的写入方式add的源码
//vector中add方法public synchronized boolean add(E e) {modCount;ensureCapacityHelper(elementCount 1);elementData[elementCount] e;return true;}//CopyOnWriteArrayList中add方法public boolean add(E e) {final ReentrantLock lock this.lock;lock.lock();try {Object[] elements getArray();int len elements.length;Object[] newElements Arrays.copyOf(elements, len 1);newElements[len] e;setArray(newElements);return true;} finally {lock.unlock();}}由上部分源码中CopyOnWriteArrayList 每次add方法都会是一次复制新建一个新数组将老数组拷贝并在新数组加入新来的元素这种开销还是挺大的Vector中add方法其实就是一次synchronized之后的数组操作出发需要扩容才会出现数组复制的情况因此绝大多数情况写入性能比CopyOnWriteArrayList更优秀。
并发Set
CopyOnWriteArraySet线程安全Set实现了Set接口内部实现完全依赖CopyOnWriteArrayList所以特性和他一样适合读多写少的情况。如果需要并发写的时候用到Set我们可以用一下方式得到线程安全Set
//Collections包中的方法
public static T SetT synchronizedSet(SetT s)高并发Map
因为Map是非线程安全我们可用Collections的synchronizedMap()方法得到一个线程安全的Map但并不是最优解JDK提供专用高并发MapConcurrentHashMap性能更优ConcurrentHashMap维持高吞吐量得益于其内部实现是用的锁分离技术同事ConcuttentHashMap的get操作也算无锁的。put操作的锁粒度小于同步的HashMapHashTable因此性能更优于同步HashMap
并发Queue
JDK中有两套并发队列实现ConcurrentLinkedQueue高性能队列BlockingQueue阻塞队列都继承自Queue接口前者适用于高并发场景通过无锁的凡是实现高并发状态的高性能性能高于BlockingQueueBlockingQueue的代表有LinkedBlockingQueue与ConcurrentLinkedQueue使用场景不同BlockingQueue主要用来简化多线程之间数据共享典型的生产者–消费者模式提供一种读写阻塞的模式以下两种实现 ArrayBlockingQueue基于数组的阻塞队列维护一个定长数组缓存队列中的数据对象还保存着两个整型变量分表标识队列头部尾部在数组中位置还可以控制内部锁是否采用公平锁默认非公平锁。LinkedBlockingQueue一个基于链表的阻塞队列生产者放入数据时候将会缓存到一个链表中生产者立刻返回只又当缓冲链表达到最大容量才阻塞生产者知道有消费者从链表获取数据生产者才被换醒。
并发Deque
jdk1.6 以后提供了一种双端队列运行在队列头部或者尾部进行出队列和入队列。主要接口Deque有三个实现类LinkedListArrayDeque和LinkedBlockingDeque都实现了Deque linkedList使用的链表实现有利于扩容无需数组的复制随机读取性能低ArrayDeque使用数组实现拥有高效随机访问性能更好的遍历性能不利于数据扩容LinkedBlockingDeque线程安全的双端队列实现使用双向链表没有读写锁分离所有效率远低于LinkedBlockingQueue更低于CocurrentLinkedQueue
并发控制方法
JDK中提供多种途径实现多线程的并发控制例如内部锁重入锁读写锁信号量等。
Java内存模型与volatile
java每个线程有自己的工作内存并且还有另外一块内存是给所有线程共享的线程自己的工作内存区域存放这共享内存区域中变量值的拷贝如下步骤以及图解 当执行拷贝时候线程会先锁定并清除自己的工作内存区这保证共享变量从共享内存区正确拷贝到自己的工作内存区域装载到线程工作内存区域后线程就可以操作对应的内存区域的数据应为内存拷贝之后对值修改是共享的即修改了本地内存变量也就同时修改了共享内存中变量所有只要保证当线程解锁是保证该工作内存区中变量值都已经写回到共享内存中。 以上图中步骤使用use赋值assign装载load存储store锁定lock解锁unlock而主内存可以执行的操作有读read写write锁lock解锁unlock共享内存中变量值一定是由他本身或者其他线程存储到变量中的值即使他被多个线程操作也就是其中某个线程修改后的值而线程工作内存中的局部变量的是线程安全的只能自己线程访问。如上图4.18所示描述了线程工作内存与主内存的一些操作其中useassign很好理解下面主内存和工作内存的操作 read操作与相匹配的load操作总是成对出现read读操作相就想线程工作内存需要用一个局部变量去读取主内存中变量数据之后在通过load操作加载到工作内存Store操作与write操作总是成对出现store操作我需要将线程工作内存中的变量赋值给主内存变量的一个拷贝中可以看成是一个局部变量接着write操作将这个拷贝后的值写入到主内存中变量完成修改操作。 Double和Long操作比较特殊因为只有他两是双精度64位但是在32位操作系统中CPU最多一次性处理32位字长的数据这将导致Double,Long类型需要CPU处理两次才能完成对一个数据的操作可能存在多线程下非原子性的问题所以需要将其声明为volatile进行同步处理。volatile关键字破事所有线程均读写主内存中的对应变量从而使得volatile变量在多线程之间可见 olatile变量可以做以下保证其他线程对变量的修改可以及时反映在当前线程中确保当前线程对volatile变量的修改能及时写会共享主内存中并被其他线程所见使用volatile申明的变量编译器会保证其有序性。
同步关键字synchronized
synchronized是java预约中最常见的同步方法与其他同步方式比更简洁代码可读性和维护性更好。并且从JDK1.6开始对其性能优化已经和非公平锁的差距缩小并且不断优化中。synchronized最常用方法如下,当method方法被调用调用线程首先必须获得当前对象的锁如果当前对象锁被其他线程持有则需等待方法结束才会释放锁接着获取。synchronized中同步的代码越少越好粒度越小越好。
public synchronized void method(){}
//等价如下
public void method(){synchronized(this){....}
}还有如下情况用于static函数时候相当于Class对象上加锁所有对改方法调用都需要获取Class对象锁。
public synchronized static void method(){}为了实现多线程之间的交互还需要使用Object对象的wait和notify方法 wait可以让线程等待当前对象上的通知notify被调用在wait过程中线程会释放对象锁将锁让给其他等待线程获取。notify将唤醒一个等待在当前对象上的线程如果当前有多个线程等待则随机选择其中一个。
ReentrantLock重入锁
比synchronized功能更强大可以中断可定时。RentrantLock提供了公平非公平两种锁 公平锁保证锁在等待队列中的各个线程是可以公平获取的不存在插队永远先进先出。非公平锁不保证申请锁的公平性申请锁能差多 公平锁实现代价更大从性能上分析非公平锁性能好更多无特殊需求选择非公平锁
public RentrantLock(boolean fair)RentrantLock使用完后必须释放锁相比synchronizedJVM虚拟机总是会在最后自动释放synchronized锁。RentrantLock提供几个重要方法
abstract void lock();//获得锁如果被占用则等待
public void lockInterruptibly() throws InterruptedException //获得锁但有限响应中断
public boolean tryLock()//尝试获得成功返回true否则false改方法不等待立刻返回
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException //给定实际内尝试获取锁
public void unlock()//释放锁lock与lockInterruptibly不同之处在于lockInterruptibly在锁等待的过程中可以响应中断事件。
ReadWriteLock读写锁
读写锁jdk5开始提供这种锁读写锁允许多个线程同时读取但是如果有写操作在执行其他操作需要等待写操作执行完后获取锁才能执行。当读操作次数远远大于写操作读写锁效率很高提升系统性能
Condition对象
Condition对象可以用于协调多线程间的复杂协作Condition与锁相关联的通过Lock接口的Condition newCondition方法可以生成一个与锁板顶的Condition实例。Condition对象与锁关系类似Object.wait与Object.notify两个函数已经synchronized关键字一样配合使用完成多线程控制。Condition提供如下方法
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();await()方法使当前线程等待同时释放当前锁当其他线程中使用signal()或者signalAll()方法时候线程重新获得锁并执行。或者当前线程被中断时也能跳出等待。awaitUninterruptibly()方法与await()基本相同但是他并不会在等待过程中响应中断Singal()方法用于唤醒一个等待中的线程相对singalAll方法会唤醒所有在等待中的线程。ArrayBlockingQueue为案例利用ConditionReentrantLock来实现队列满时候让生产者线程等待又在队列空时让消费者线程等待。 /** Main lock guarding all access */final ReentrantLock lock;/** Condition for waiting takes */private final Condition notEmpty;/** Condition for waiting puts */private final Condition notFull;/**初始化锁对Condition进行赋值*/public ArrayBlockingQueue(int capacity, boolean fair) {if (capacity 0)throw new IllegalArgumentException();this.items new Object[capacity];lock new ReentrantLock(fair);notEmpty lock.newCondition();notFull lock.newCondition();}//加入队列public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock this.lock;lock.lockInterruptibly();try {while (count items.length)notFull.await();//队列满 利用await阻塞添加enqueue(e);} finally {lock.unlock();}}//从队列中获取元素public E take() throws InterruptedException {final ReentrantLock lock this.lock;lock.lockInterruptibly();try {while (count 0)notEmpty.await();//队列空利用await阻塞获取return dequeue();} finally {lock.unlock();}}
Semaphore信号量
信号量可以指定多个线程同时访问某个资源如下构造方法 public Semaphore(int permits) {sync new NonfairSync(permits);}//参数一指定信号量准入大小fair 指定是否公平获取信号量public Semaphore(int permits, boolean fair) {sync fair ? new FairSync(permits) : new NonfairSync(permits);}
提供如下几个主要方法控制线程
//尝试获取一个准入许可无法获取则等待知道有线程释放或者中断
public void acquire() throws InterruptedException
//尝试获取一个许可成功返回true否则false不会等待直接返回
public boolean tryAcquire()
//线程访问资源结束后释放一个许可使其他等待许可的线程可以进行资源访问
public void release()ThreadLocal线程局部变量
ThreadLocal不提供锁使用空间换时间的方式为每个线程提供变量的独立副本用这种方式解决线程安全的问题因此他不是一种数据共享的解决方案。ThreadLocal并不具有绝对优势只是并发的另外一种解决思想在高并发量或者锁竞争激烈的场合使用ThreadLocal可以在一定程度上减少锁竞争从而减少系统CPU损耗。
public void set(T value);//将次现场局部变量的当前线程副本中的值设置为指定值
public void remove();//移除此线程局部变量当前线程的值
public T get();//返回次现场局部变量的氮气线程副本中的值我们用如下demo来测试ThreadLocal中变量是线程安全的
public class ThreadLocalDemo implements Runnable{public static final ThreadLocalDate localvar new ThreadLocal();private long time;public ThreadLocalDemo(long time){this.time time;}Overridepublic void run() {Date d new Date(time);for (int i 0; i 50000; i) {localvar.set(d);//将d的值设置到线程的局部变量中if(localvar.get().getTime() ! time){//判断线程中值是否变化如果有则输出System.out.println(id time localvar localvar.get().getTime());}}}public static void main(String[] args) throws InterruptedException {//每次给与不同的时间测试是否有时间变化for (int i 0; i 1000; i) {ThreadLocalDemo threadLocalDemo new ThreadLocalDemo(new Date().getTime());new Thread(threadLocalDemo).start();Thread.sleep(100);}}
}
以上没有输出即使在并发时候也可以保证多个线程间localvar变量是本线程独立的即使没有同步操作单多个线程数据互不影响。如下set源码解释本地线程安全性问题
public void set(T value) {Thread t Thread.currentThread();//获取当前线程ThreadLocalMap map getMap(t);//获取线程ThreadLocalMap对象每个线程独有if (map ! null)map.set(this, value);将value设置到map中get方法也是从这里获取数据elsecreateMap(t, value);}
锁的性能和优化
锁性能优化和常见思路避免死锁 减小锁粒度锁分离
线程开销
多核时代并发提高效率是因为多线程尽可能的利用了多核心的工作优势但是对比单线程方式也增加开销系统需要维护多线程环境的上下文切换线程共享内存局部变量信息线程调度等。
避免死锁
死锁是多线程特有问题满足以下情况下出现死锁 互斥条件一个资源每次只能被一个线程持有请求与保持条件一个进程请求资源阻塞时候对已获得的资源保持不放不剥夺条件若干线程之前形成在未使用完之前不能强行剥夺循环等待条件若干进程之间形成一种头尾相接的循环等待资源关系
减少锁持有时间
尽量减少持有锁的时间以减少线程间互斥的可能。
减少锁粒度
典型使用场景就是ConcurrentHashMap类实现的锁分段即使他将整个HashMap分成若干段Segment每一段都是一个HashMap例如在ConcurrentHashMap中增加一个新表项并不是将整个HashMap加锁首先更具HashCode获取数据落到哪一个段segment上然后对该段加锁并完成put操作在并发put的时候只要被操作的不是同一个段就可以真正做到并行减少锁粒度缺点当系统需要去全局锁时候其资源的消耗会更多例如ConcurrentHashMap里面我们需要获取每个Segment的锁后才能获取全局锁因此我们应该通过其他方式避免这种操作
读写分离锁替换独占锁
读写锁共享读独占写可以有效提升系统并发能力。
锁分离
从读写锁中可以衍生出锁分离我们在LinkedBlockingQueue中takeput操作实现可以看出这种思想的实现因为LinkedBlockingQueue是链表存取是不冲突的如果是全局所你们存取只能单个进行效率很低锁竞争激烈JDK中用如下方法 /** Lock held by take, poll, etc take操作需要持有takeLock*/private final ReentrantLock takeLock new ReentrantLock();/** Wait queue for waiting takes */private final Condition notEmpty takeLock.newCondition();/** Lock held by put, offer, etc put操作必须持有putLock*/private final ReentrantLock putLock new ReentrantLock();/** Wait queue for waiting puts */private final Condition notFull putLock.newCondition();
如上将takeput操作用两个锁来避开竞争关系削弱了竞争可能性如下实现
public E take() throws InterruptedException {E x;int c -1;final AtomicInteger count this.count;//获取takeLockfinal ReentrantLock takeLock this.takeLock;//不能有两个线程同时读数据takeLock.lockInterruptibly();try {while (count.get() 0) {//没有数据一直等notEmpty.await();//等待put操作中的notEmpty.signal方法释放}x dequeue();//获取第一个数据节点c count.getAndDecrement();//统计数据增加1 原子操作if (c 1)notEmpty.signal();//如果取出元素后还有剩余元素则通知其他take操作} finally {takeLock.unlock();//释放锁}if (c capacity)signalNotFull();//通知put操作已经有剩余空间return x;}
//put操作类似不重复重入锁ReentrantLock内部所synchronized
内部锁重入锁功能上是重复的使用上synchronized更简单jvm会自动释放ReentrantLock需要在finally中自己释放。功能上ReentrantLock功能更强大功能如下
boolean tryLock(long time, TimeUnit unit);//锁等待时间功能public void lockInterruptibly();//支持锁中断public boolean tryLock()//快速锁轮换以上功能都有效避免死锁的产生从而提高系统稳定性。而且重入锁中的Condition机制可以进行复杂的线程控制上面一节中已经有过说明。
锁粗化Lock Coarsening
本来应该锁的粒度越小越好但是在某些情况下有必要对锁粒度进行扩大化比如我们连续不断请求释放同一个锁这样JVM需要不断对同一个锁进行请求释放造成系统资源浪费这个时候其实JVM也会帮我们做优化将连续锁操作合成一个。
自旋锁Spinning Lock
自旋锁是JVM虚拟机多锁的优化当多线程并发时候频繁的官气和恢复操作会有很大系统开销当获取锁的资源仅仅需要一小段CPU时间时候锁的等待获取的时间可能就非常短以至于比挂起和恢复的时间还要短这就是一个问题了JVM引入的自旋锁就是为了解决上面问题可以使线程在未获取到锁时候不被挂起而作一个空循环即所谓的自旋若干个空循环后线程如果获得了锁则只需没有获取则挂起自旋后被挂起的几率变小这种锁对那竞争不激烈并且锁占用时间少的并发线程有利但是对竞争激烈锁占用时间长大概率自旋后也大概率不能获取锁徒增了CPU时间的占用得不偿失。JVM通过虚拟机参数-XX: UseSpinning参数开启自旋JVM通过虚拟机参数-XXPreBlockSpin参数来设置自旋等待次数
锁消除Lock Elimination
锁消除是JVM在编译时候做的一个优化当JVM对上下文进行解析时候会去除一些不可能存在共享资源竞争的锁。也就是如果一个变量不可能存在多线程并发竞争的情况但是他却有加锁JVM会编译时候讲锁去除以此避免不必要的锁获取消耗过程。JDK的API中有一些线程安全的工具类StringBufferVector在某些场合工具类内部同步方法是不必要的JVM在运行时基于逃逸分析技术捕获这些不存在禁止却有锁的代码消除这些锁以此提高性能。逃逸分析可以使用JVM参数-XX:DoEscapeAnalysis消除锁可以用JVM参数-XX:EliminateLocks用如下案例分析
public class LockTest {private static final int CREATE 20000000;public static void main(String[] args) {long start System.currentTimeMillis();for (int i 0; i CREATE; i) {createStringBuffer(Java, Performance);}long bufferCost System.currentTimeMillis() - start;System.out.println(createStringBUffer: bufferCost ms);}public static String createStringBuffer(String str1, String str2){StringBuffer sb new StringBuffer();sb.append(str1);sb.append(str2);return sb.toString();}
}
使用参数 -server -XX:-DoEscapeAnalysis -XX:-EliminateLocks 关闭逃逸分析和锁消除运行相对耗时1567ms使用参数 -server -XX:DoEscapeAnalysis -XX:EliminateLocks 开启逃逸分析和锁消除运行相对耗时943ms
偏向锁Biased Lock
JDK1.6开始提出的一种锁优化。核心思想如果没有程序竞争则取消之前已经取得锁的线程同步操作。就是说如果一段时间内所有竞争这个锁资源的线程都是同一个那么我就在这段时间点取消锁竞争避免锁定获取释放过程。JVM中可以用-XX:UseBiasedLocking可以设置启用偏向锁缺点偏向锁在竞争激烈场合没有优化效果因为竞争大导致有锁线程不停切换锁也很难保存在偏向模式次数使用偏向锁大不大上面提到的性能优化还有损性能。
无锁的并行计算
非阻塞同步的线程安全方式
非阻塞的同步/无锁
基于锁方式存在他的弊端就是收核心限制线程竞争已经相互等待而非阻塞则避开了这个问题类似ThreadLoadl还有一种更重要的基于比较交换的CASCompare And Swap算法的无锁并发控制。CAS优势非阻塞无死锁情况没有锁竞争开销没有线程调度开销。CAS算法过程CASVEN。V表示需要更新的变量E预期值N新值。类似乐观锁的思想当VE时候才将V值设置为N如果V和E不同则标识其他线程已经修改则不作更新CAS返回当前V的真实值。更新失败并不挂起而是被告知失败。大部分处理器支持原子化CAS命令JDK5 以后JVM可以使用这个指令实现并发操作和并发数据结构。
原子操作
JDK的java.util.concurrent.atomc包下有一组无锁计算实现的原子操作类例如AtomicIntegerAtomicLongAtomicBoolean等他们封装了对整数长整型等对象多线程安全操作。以AtomicInteger为案例 public final boolean get()public final boolean compareAndSet(boolean expect, boolean update) public boolean weakCompareAndSet(boolean expect, boolean update)public final void set(boolean newValue)public final void lazySet(boolean newValue) public final boolean getAndSet(boolean newValue)以getAndSet方法为例看原因如何实现CAS算法工作流程
public final int getAndSet(int newValue) {return unsafe.getAndSetInt(this, valueOffset, newValue);//unsafe方法操作系统底层命令的调用
}
public final int getAndSetInt(Object var1, long var2, int var4) {int var5;do {var5 this.getIntVolatile(var1, var2);//获取当前值} while(!this.compareAndSwapInt(var1, var2, var5, var4));//尝试设置var4 作为新值如果失败则继续循环直到成功为止return var5;
}如上代码是一个循环循环退出条件是设置成功并且返回原值此处并没有用到锁只是不停尝试。更新失败后继续获取当前值然后比较设置继续循环而已并没有干扰其他线程的相关代码。java.util.concurrent.atomic包中性类性能是非常优越的包中的原子类是基于无锁算法实现的他们的性能远优于普通有锁操作。无锁的操作实际将多线程并发冲突交给应用层不仅提升系统性能还增加系统灵活性。相对的算法以及编码的复杂度也明显增加了。
Amino框架介绍
无锁算法缺点是需要应用层处理线程的冲突问题这增加开发难度和算法复杂度。现在Amino无锁框架提供了可用于线程安全的基于无锁算法的一些数据结构同事还内置了一些多线程调度如下优势 对死锁问题免疫确保系统整体进度高并发下无锁竞争带来的性能开销可以轻易使用一些成熟无锁结构无需自己研发
Amino集合
反正就是性能比JDK搞的一个框架提供了基础数据例如ListSet这些
协程
进程— 线程— 协成这三个概念是逐步拆分的一个过程进程是一种重量级的调度方式因为创建调度上下文切换的成本太高因此我们将进程拆分为多个线程将线程作为并发控制的基本单元但是单并发在趋于激烈为了在进一步提升系统性能我们对线程在进一步分割就是协程。无论进程线程协成在逻辑上都对应一个任务执行一段逻辑代码。当使用协程实现一个任务时候协程并不完全占据一个线程当一个协程处于等待状态CPU交给线程内的其他协程。与线程相比协程间的切换更轻便因此具有更低的操作系统成本和更高的任务并发性。协程优势 协程的执行效率非常高因为子进程切换不是线程切换而是由程序自身控制因此没有线程切换的开销和多线程比较线程数量越多协程的性能优势越明显协程不需要多线程的锁机制在协成中控制共享资源不加锁只需要判断状态。
Kilim框架 协程不被java语言原生支持我们可以使用kilim框架通过较低成本开发引入协程。 协程没有锁同步块等概念不会有多线程程序的复杂度但是与java其他框架不同的是开发后期需要通过kilim框架提供的织入weaver工具将协程控制代码块织入原始代码已支持协程的正常工作。如下图 Task是协程的任务载体execute方法用于执行任务代码fiber对象保存和管理任务执行堆栈以实现任务可以正常暂停和继续。Mailbox对象为协程间的通信载体用于数据共享和信息交流。
Task及其状态
Task多谢负责执行代码完成任务kilim官网中状态图 Task创建后处于Ready状态调用execute方法后开始运行期间可以被暂停也可以被唤醒。正常结束后Task对象成为完成状态。
Fiber及其状态 Fiber用来维护Task执行堆栈Kilim框架对线程进行分割以支持更小的并行度所以Kilim需要自己维护执行堆栈 Fiber主要成员如下 pc程序计数器记录当前执行位置curState:当前状态stateStack协程的状态堆栈istack当前栈位置 Fiber的up方法down方法用于维护堆栈的生长和回退如下fiber状态机 上图所示^ 符号 标识并且的医生其中begindownendup标识Fiber操作iStack0/up可以解毒为进行up操作后iStack0 Kilim 框架中协程间的通讯和数据交换依靠Mailbox邮箱进行就像管道多个Task间共享以生产者消费者作类比生产者向Mailbox提交一个数据消费者协程则想Mailbox中获取数据。
Kilim示例以及性能评估
协程拥有比线程更小粒度所以理论上kilim的并发模型可以支持更高的并行度。使用kilim可以让系统更低成本的支持更高的并行度
JVM调优
java虚拟机内存模型
程序计数器用于存放下一条运行的指令虚拟机栈本地方法栈用于存放函数用堆栈信息java堆存放程序运行时候需要的对象等数据方法区方法区用于存放程序的类元数据信息
程序计数器
是一块很小的空间当我们用多线程时候其实一个CPU一个时刻只能为一个现场提供服务所以线程数超过CPU个数的时候线程质检轮询争夺CPU资源。此时当一个线程被切换出去为此需要程序计数器来记录这个独立线程运行到哪一个步骤指令以便下一次轮询到继续执行的时候从这个步骤指令开始往下执行。各个线程质检的计数器互补影响独立工作是一块线程私有的内存空间。如果一个线程在执行java方法程序计数器就在记录正在执行java字节码的地址如果是一个Native方法程序计数器为空。
java虚拟机栈
也是线程私有的内存空间同java线程统一时刻创建保存局部变量部分结果并参与方法调用放回java虚拟机运行栈空间动态边如果java线程计算栈深度大于最大可用深度异常StackOverFlowException如果java动态扩容到内存不够异常OutOfMamoryException可用-Xss参数控制栈大小如下测试我们设置-Xss1M之后设置-Xss2M
public class TestStack {private static int count 0;public void recursion(){count ;System.out.println(deep of stack : count);recursion();}public static void main(String[] args) {TestStack testStack new TestStack();try{testStack.recursion();}catch (Exception e){System.out.println(deep of stack : count);e.printStackTrace();}}
}
/**输出
1M栈输出
deep of stack :5544
Exception in thread main java.lang.StackOverflowErrorat sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)......2M栈输出
deep of stack :11887
Exception in thread main java.lang.StackOverflowErrorat sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
**/栈存储结构是栈帧每个栈帧存放 方法局部变量表操作数栈动态连接方法返回地址 每个方法调用就是一次入栈出栈过程参数多局部变量表就大需要内存就大。如下图 再次测试如下代码
public class TestStack {private static int count 0;public void recursion(long a, long b, long c){long d0,e0,f0;count ;System.out.println(deep of stack : count);recursion(1l,2l,3l);}public static void main(String[] args) {TestStack testStack new TestStack();try{testStack.recursion(1l,2l,3l);}catch (Exception e){System.out.println(deep of stack : count);e.printStackTrace();}}
}
//-Xss1M 输出
deep of stack :4167
Exception in thread main java.lang.StackOverflowErrorat sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579) 如上可看出调用函数参数局部变量越多占用栈内存越大导致调用次数比无参数时候下降5544–4167 我们用idea中jclasslib插件看下class文件中的recursion方法 可见最大局部变量是13是这么算的局部变量存放单位字32位并非字节longdouble是64为两个字所以三个参数三个局部变量都是long 6*212个字还有一个非static方法虚拟机回将当前对象this最为参数通过局部变量传递给当前方法所以最终13字 局部变量中的字对GC有一定影响如果局部变量被保存在局部变量表GC根能引用到这个局部变量所指向的空间可达性算法判断不可回收因此不会清除如下
public static void test1(){{byte[] b new Byte[6*1224*1024];}System.gc();System.out.println(s)
}
//GC日志
[GC (System.gc()) [PSYoungGen: 10006K-512K(38400K)] 10006K-7856K(125952K), 0.0077538 secs]
[Times: user0.01 sys0.01, real0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 512K-0K(38400K)]
[ParOldGen: 7344K-7739K(87552K)] 7856K-7739K(125952K),
[Metaspace: 3117K-3117K(1056768K)], 0.0070191 secs]
[Times: user0.00 sys0.00, real0.00 secs]
the first byte以上GC日志显示已经回收但是依据可达性分析算法是不能回收的是GC做的其他优化后续解答局部变量表中字可能会影响GC回收如果这个字没有被后续代码复用那么他引用的对象不会被GC释放。
本地方法栈
本地方法栈和虚拟机栈类似只不过是用来管理本地方法调用本地方法比不是用java实现而是C实现SUN的HotSpot虚拟机中不区分本地方法栈和虚拟机栈。因此和虚拟机栈一样会抛出StackOverFlowErrorOutOfMemoryError
java堆 java堆运行是内存几乎所有对象和数组都在堆空间分配内存堆内存分为新生代存放刚产生对象和年轻对象老年代存放新生代中一定时间内一直没有被回收的对象。如下 如上图新生代分为三个部分 eden对象刚建立时候存放的位置s0surivivor space0 或者 from spaces1survivor space1 或者通spaceservivor意为幸存者空间也就是存放其中的对象至少经历一次GC并新村。并如果幸存区到指定年龄还没被回收则有机会进入老年代 如下案例我用JVM参数
-XX:PrintGCDetails //打印GC日志
-XX:SurvivorRatio8 //JVM参数中有一个比较重要的参数SurvivorRatio它定义了新生代中Eden区域和Survivor区域From幸存区或To幸存区的比例默认为8也就是说Eden占新生代的8/10From幸存区和To幸存区各占新生代的1/10
-XX:MaxTenuringThreshold15 //设置对象进入老年代需要的GC次数
-Xms20M // jjvm最小可用内存
-Xmx20M //jvm最大可用内存
-Xmn12M //年轻代大小设置public class TestHeapGC {public static void main(String[] args) {byte[] b1 new byte[1024*1024/2]; //0.5Mbyte[] b2 new byte[1024*1024*8]; //8Mb2 null;b2 new byte[1024*1024*8]; //8M
// System.gc();}
}//GC日志
HeapPSYoungGen total 11264K, used 9066K [0x00000007bf400000, 0x00000007c0000000, 0x00000007c0000000)eden space 10240K, 84% used [0x00000007bf400000,0x00000007bfc66868,0x00000007bfe00000)from space 1024K, 45% used [0x00000007bff00000,0x00000007bff74010,0x00000007c0000000)to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)ParOldGen total 8192K, used 1032K [0x00000007bec00000, 0x00000007bf400000, 0x00000007bf400000)object space 8192K, 12% used [0x00000007bec00000,0x00000007bed02010,0x00000007bf400000)Metaspace used 3134K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 349K, capacity 386K, committed 512K, reserved 1048576K
上面GC日志是注释掉GC代码后生成可以看到进行了多次内存分配我们算一下先分配8.5M从fromspace使用率 92%因为触发了GC将eden中的b1移动到了from空间最后分配的8MB被分配到eden新生代新生代设置12 并且按照8:1:1 分配所以清理掉的0.5 M可以存放在from中如果from更小导致存放不下回直接到old区我们打开gc看下
HeapPSYoungGen total 11264K, used 8744K [0x00000007bf400000, 0x00000007c0000000, 0x00000007c0000000)eden space 10240K, 85% used [0x00000007bf400000,0x00000007bfc8a190,0x00000007bfe00000)from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)ParOldGen total 8192K, used 1025K [0x00000007bec00000, 0x00000007bf400000, 0x00000007bf400000)object space 8192K, 12% used [0x00000007bec00000,0x00000007bed00658,0x00000007bf400000)Metaspace used 3175K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 352K, capacity 386K, committed 512K, reserved 1048576K
如上调试来几次Xmn的大小得出的结果因为回更具对内存中年轻态的大小来决定GC之后存储的位置上日志看出FUllGC后新生代有一部分老年态也有一部分我猜测是原来的b1 到老年态二最好的b2的8M内存在新生态。
方法区
方法区最重要的是类信息常量池域信息方法信息。 类信息类的名称父类名称类型修饰符public/private/protected类型的直接接口类表常量池类方法域等信息引用的常量域信息域名称域类型域修饰符方法信息方法名返回类型方法参数方法修饰符方法字节码操作数栈方法栈帧的局部变量区大小以及异常表。 HotSpot中方法去也成为永久区GC对此部分也能回收两点 GC对永久区常量池的回收永久区对类元数据的回收 如下Demo测试常量池回收
//jvm参数-XX:PermSize1M -XX:MaxPermSize2m -XX:PrintGCDetails
public class TestGCPermGen {public static void main(String[] args) {for (int i 0; i Integer.MAX_VALUE; i) {String t String.valueOf(i).intern();}}
}
//GC日志
.....
[GC (Allocation Failure) [PSYoungGen: 133568K-464K(138240K)] 133584K-480K(225792K), 0.1607520 secs] [Times: user0.16 sys0.00, real0.16 secs]
[GC (Allocation Failure) [PSYoungGen: 133584K-448K(266752K)] 133600K-464K(354304K), 0.2681573 secs] [Times: user0.26 sys0.00, real0.27 secs]
[GC (Allocation Failure) [PSYoungGen: 266688K-0K(266752K)] 266704K-428K(354304K), 0.4384817 secs] [Times: user0.43 sys0.01, real0.44 secs]
....
以上代码无限循环中向常量池中添加对象如果不清理必然OutOfMemoary但是日上日志中得出GC在一段时间后清理使程序正常运行类元数据回收需要构造N个类来测试对应GC回收情况如下DEMO用Javassist类库来生成类。
//jvm参数-XX:PermSize1M -XX:MaxPermSize2m -XX:PrintGCDetails
public class JavaBeanObject {private String name java;public String getName() {return name;}public void setName(String name) {this.name name;}
}
public class JavaBeanGCTest {public static void main(String[] args) throws Exception{for (int i 0; i Integer.MAX_VALUE; i) {CtClass cc ClassPool.getDefault().makeClass(Geym i);cc.setSuperclass(ClassPool.getDefault().get(com.ljm.jvm.JavaBeanObject));Class clz cc.toClass();JavaBeanObject v (JavaBeanObject) clz.newInstance();}}
}GC日志
......
[GC (Allocation Failure) [PSYoungGen: 832K-320K(1024K)] 2970K-2738K(3584K), 0.0010876 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 320K-0K(1024K)] [ParOldGen: 2418K-2058K(2560K)] 2738K-2058K(3584K), [Metaspace: 5480K-5480K(1056768K)], 0.0193576 secs] [Times: user0.05 sys0.00, real0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 512K-320K(1024K)] 2570K-2386K(3584K), 0.0004815 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 832K-352K(1024K)] 2898K-2690K(3584K), 0.0007751 secs] [Times: user0.01 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 352K-0K(1024K)] [ParOldGen: 2338K-2556K(2560K)] 2690K-2556K(3584K), [Metaspace: 5757K-5757K(1056768K)], 0.0174732 secs] [Times: user0.04 sys0.01, real0.01 secs]
......以上日志看出在一段时间后就回触发一次FullGC之后并没有OOM由此可见GC能够保证方法区中类元数据的回收。
JVM内存分配参数
设置最大堆内存
用-Xmx指定最大堆内存最大堆指的是新生代 老年代大小只和的最大值。
设置最小堆内存
用-Xms设置最小对内存意义在于Java启动优先满足-Xms的指定大小当指定内存不够才向操作系统申请直到触及Xmx导致OOM-Xms过小JMV为保证系统尽量在指定范围内存工作只能频繁的GC操作来释放内存间接导致MinorGC和FUllCc的次数如此啊测试
//参数-XX:PrintGCDetails -Xms1M -Xmx11M public class GCTimesTest {public static void main(String[] args) {Vector vector new Vector();for (int i 0; i 20; i) {byte[] bytes new byte[1024*1024];vector.add(bytes);if(vector.size() 3){vector.clear();}}}
}
//GC日志
......
[GC (Allocation Failure) [PSYoungGen: 506K-400K(1024K)] 506K-408K(1536K), 0.0008198 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 805K-480K(1024K)] 7981K-7664K(9216K), 0.0010682 secs] [Times: user0.00 sys0.00, real0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 480K-496K(1024K)] 7664K-7680K(9216K), 0.0005386 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 496K-0K(1024K)] [ParOldGen: 7184K-1424K(3072K)] 7680K-1424K(4096K), [Metaspace: 3117K-3117K(1056768K)], 0.0063393 secs] [Times: user0.01 sys0.00, real0.00 secs] [GC (Allocation Failure) [PSYoungGen: 10K-96K(1536K)] 7579K-7664K(9728K), 0.0005220 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 96K-128K(1536K)] 7664K-7696K(9728K), 0.0003231 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 128K-0K(1536K)] [ParOldGen: 7568K-1411K(3584K)] 7696K-1411K(5120K), [Metaspace: 3169K-3169K(1056768K)], 0.0046456 secs] [Times: user0.00 sys0.01, real0.00 secs] [GC (Allocation Failure) [PSYoungGen: 31K-96K(3072K)] 7586K-7651K(11264K), 0.0003348 secs] [Times: user0.00 sys0.00, real0.00 secs]
......以上demo控制循环次数越多FullGC的次数越多20次循环中有两次FullGC修改JVM参数
-XX:PrintGCDetails -Xms1M -Xmx11M 将最大堆堆小堆都设置11M只触发一次FUllGC由此可见 JVM将系统内存尽量控制在-Xms大小内当内存触及-Xms时候就Full GC我们将-Xms与-Xmx设置成一样可以在系统运行初期减少GC次数和耗时。
设置新生代
用-Xmn设置新生代的大小新生代也会堆GC有比较大影响如上案例在继续增加如下
-XX:PrintGCDetails -Xms11M -Xmx11M -Xmn2M
结果可以看的20次循环中又有2次FullGCHotSpot虚拟机中-XX:NewSize设置新生代初始大小-XX:MaxNewSize设置新生代最大值只设置-Xmn等效同时设置这两个参数。
设置永久代方法区
用-XX:MaxPermSize,-XX:PermSize分别设置永久代最大最小值永久代直接决定来系统可以支持多少个类的定义以及多少个产量合理设置有助于系统动态类的生成。
public class JavaBeanGCTest {public static void main(String[] args) throws Exception{Integer count 0;try {for (int i 0; i Integer.MAX_VALUE; i) {CtClass cc ClassPool.getDefault().makeClass(Geym i);cc.setSuperclass(ClassPool.getDefault().get(com.ljm.jvm.JavaBeanObject));Class clz cc.toClass();JavaBeanObject v (JavaBeanObject) clz.newInstance();count;}}catch (Exception e){System.out.println(test test--------------------- count);}}
}
//JVM参数-XX:PermSize1M -XX:MaxPermSize2m -XX:PrintGCDetails
//JVM参数-XX:PermSize2M -XX:MaxPermSize4m -XX:PrintGCDetails 以上JVM参数分别可以运行4142 9333 次一般我们设置MaxPermSize64M可以满足绝大部分不够的话可以增加128M在不够就代优化代码了。
设置线程栈
使用-Xss参数设置线程栈大小栈过小将会导致线程运行时候没有足够空间分配聚币变量或者达不到足够深度的栈深度调用导致StackOverFlowError。栈空间过大那么将导致开启线程所需要的内存成本上升系统所能支持线程总数下降。如下Demo测试固定栈空间下开设线程数量
public class StackTest {public static class MyThread extends Thread{Overridepublic void run(){try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {int i0;try {for (i 0; i 1000000; i) {new MyThread().start();}}catch (Exception e){System.out.println(count thread is i);}}
}
//JVM参数-Xss1M很大可能是一直运行到结束并没有预期结果调大-Xss10M并且增加循环次数最大可能是死机原因在于次数设置的是线程栈大小10M也就是一个线程10M每次创建都会向系统内存申请10M的内存导致系统内存不够从而电脑卡顿死机都有可能而最终应该是OutOfMemoryError。
堆比例分配
之前提了设置堆最大最小新生代大小JVM参数配置实际生产环境希望能对对空间进行比例分配有如下参数 用-XX:SurivorRatio用来设置新生代中eden空间和from spaceto space空间的比例关系from空间和to空间大小相同只能一样并且在MinorGC后互换角色。
//JVM参数用-XX:PrintGCDetails -Xmn10M -XX:SurvivorRatio8测试PSYoungGen total 9216K, used 2516K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)eden space 8192K, 30% used [0x00000000ff600000,0x00000000ff875110,0x00000000ffe00000)from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 118784K, used 0K [0x0000000082200000, 0x0000000089600000, 0x00000000ff600000)object space 118784K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000089600000)Metaspace used 3435K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 374K, capacity 388K, committed 512K, reserved 1048576K
如上GC日志显示eden space大小8192 from区to区都是1024可见是8:1 的关系
我们使用参数 -XX:PrintGCDetails -Xmn10M -XX:SurvivorRatio2
HeapPSYoungGen total 7680K, used 2327K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)eden space 5120K, 45% used [0x00000000ff600000,0x00000000ff845c60,0x00000000ffb00000)from space 2560K, 0% used [0x00000000ffd80000,0x00000000ffd80000,0x0000000100000000)to space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)ParOldGen total 118784K, used 0K [0x0000000082200000, 0x0000000089600000, 0x00000000ff600000)object space 118784K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000089600000)Metaspace used 3445K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 376K, capacity 388K, committed 512K, reserved 1048576K如上GC日志2:1 的关系因为新生代10M所以计算eden(10/(112))*25MB同类型参数-XX:NewRatio老年代/新生代继续添加参数测试
//Jvm参数设置-XX:PrintGCDetails -Xmn10M -XX:SurvivorRatio2 -XX:NewRatio2 -Xmx20M -Xms20MPSYoungGen total 6144K, used 2350K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)eden space 5632K, 41% used [0x00000000ff980000,0x00000000ffbcba40,0x00000000fff00000)from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)ParOldGen total 13824K, used 0K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)object space 13824K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff980000)Metaspace used 3454K, capacity 4500K, committed 4864K, reserved 1056768Kclass space used 377K, capacity 388K, committed 512K, reserved 1048576K
如上堆内存20M新生代老年代1:2计算得到老年代12MB和GC日志中13824 相符合。总结 -XX:SurvivorRatio2 设置Eden与survivor区的比例-XX:NewRatio2 设置老年代与新生代比例-Xms堆内存初始大小-Xmx堆内存最大大小-Xss线程栈大小-XX:MinHeapFreeRatio设置堆空间最小空闲比例堆空间内存小于这个JVM会扩展堆空间-XX:MasHeapFreeRatio设置堆空间最大空闲比例当对空间空闲内存大于这个会压缩堆空间得到一个较小的堆-XX:NewSize :设置新生代大小-XX:MaxPermSize设置最大持久区大小方法区-XX:PermSize : 设置永久区初始值方法区-XX:TargetSurvivorRatio设置survivior区可使用率当survivor区使用率达到这个数值会将对象送入老年代。 垃圾收集基础
垃圾收集作用
Java和C最大区别就是C需要手动回收分配的内存但是Java使用了垃圾收集器替代C的手工管理方式减少程序员负担减少出错几率。因此GC需解决一下问题 那些对象需要回收什么时候回收怎么回收
GC回收算法与思想
引用计算算法
最古老的的GC算法对象A只要有任何对象引用了A就计数器1任何取消A引用-1当引用为0A就可以被清除弊端在于无法解决相互引用的死局例AB相互引用但是没有任何第三方引用根据算法是不可被回收这种情况导致内存泄露。不适合作为JVM的GC策略
标记清除算法Mark-Sweep
分两个步骤 标记通过根节点java虚拟机栈中对象的引用标记从根节点开始可达对象未被标记则是可回收对象清除清除未被标记的对象 弊端清理后产生空间碎片空间是非连续空间大对象分配时候不连续内存空间工作效率更低。
复制算法Copying 基于标记算法将原内存分两块每次用其中一块GC时候将存活对象复制到另一块内存中清楚原来内存块中所有对象然后交换角色。 优势如果系统中垃圾对象多复制算法需要复制的就少此时复制算法效率最高并且统一复制到某一块内存不会有内存碎片。 弊端内存折半负载自然降低。 用处: Java新生代串行垃圾回收器使用复制算法Eden空间fromto空间三部分from to空间天生就有区域划分并且from与to空间角色互换。因此GC时候eden空间存活对象复制到survivor空间假设是to正在使用的survivorfrom中的年前对象复制到to大对象to放不下直接进入old区域to满了其他放不下的直接去old区域之后清理eden与from去。 体现复制算法优势在新生代的合理性新生代存活对象比例小复制算法效果更佳
标记压缩算法Mark-Compact
一个老年代的回收算法 标记和之前一样的通过根节点可达性分析获取被引用的对象做标记压缩清理没标记对象并且将存活对象压缩到内存的一端接着清理边界外所有空间避免碎片产生 标记压缩避免碎片化的同时不需要进行内存空间减半的风险因此性价比高老年代中存活对象多不使用与复制算法因此可以用标记压缩方式。
增量算法Incremental Collecting
为解决Stop the World状态提出的一种思想GC时间长影响用户体验增量算法思想一次性完成GC需要更多时间我们让GC收集线程与应用线程交替执行每次收集一部分区域将对系统影响降到最低弊端线程切换和上下文切换的消耗一定程度影响应用程序性能是系统吞吐量下降使GC总体成本上升
分代收集Generational Collecting
主要思想按每个GC算法不同给合适的区域使用对应的GC算法。HotSpot案例 年轻代对象存活度低使用复制算法经过N次后存活对象进入old区老年代对象存活度高使用标记压缩算法提高GC效率
垃圾收集器类型
如下图表分类
分类垃圾回收器类型线程数串行垃圾回收器线程数并行垃圾回收器工作模式并发垃圾回收器工作模式独占垃圾回收器碎片处理压缩垃圾回收器碎片处理非压缩垃圾回收器分代新生代垃圾回收器分代老年代圾回收器
评价GC策略的指标
一下指标评价GC处理器好坏 吞吐量系统吞吐量 系统总运行时间 应用程序耗时 GC耗时垃圾回收器负载垃圾回收器耗时与系统运行总时间比值停顿时间stop the word 时间垃圾回收频率通常增加对内存空间可以有效降低GC频率但是会增加一次GC的耗时反应时间标记为垃圾后多久能清理堆分配不同垃圾收集器堆内存分配不同好的收集器应该有一个合理的分配方式。
新生代串行收集器
JDK中最老的也是最基本的回收器有两个特点 第一仅仅使用单线程进行垃圾回收第二他是独占式垃圾回收 此垃圾回收器工作java应用程序必须听着就是“Stop The World”但是他是最成熟的一个并且新能高新生代中使用复制算法逻辑简单JVM参数设置-XX:UseSerialGC 来指定新生代中使用串行收集器老年代中使用串行收集器
老年代串行收集器
老年代不同在于使用标记压缩算法也是同样需要其他线程停顿但是可以和多种新生代回收器配合用如下JVM参数 -XX:UseSerialGC: 新生代老年代都使用串行收集-XX:UseParNewGC:新生代用并行收集器老年代用串型-XX:UseParallelGC:新生代用并行回收收集器老年代使用串型收集器
并行收集器
只是将串型收集器多线程化回收策略算法都一样。多核CPU系统下更快的处理单核CPU系统下甚至比串型差用以下参数 -XX:UseParNewGC:新生代用并行收集器老年代用串型-XX:UseConcMarkSweepGC:新生代使用并行老年代使用CMS 线程数指定JVM参数-XX:ParallelGCThreads,一般设置CPU数量一样的线程当cpu数量大于8设置规则可安这个公式3[(5*CPU_Count)/8]
新生代并行回收Parallel Scavenge收集器
一个很牛逼的收集器和并行比较都是多线程独占但是他有一些关键的参数可以设置我们可以用一下参数启用 -XX:UseParallelGC:新生代用并行回收收集器老年代用串型收集器-XX:UseParallelOldGC:新生代老年代都用并行回收收集器 用如下JVM参数设置关键值 -XX:MaxGCPauseMillis:设置最大GC停顿时间它自动调整java堆大小来控制GC的时间如果时间设置的很短他会使用一个较小堆这样回收很快但是回收频繁吞吐量就降低来-XX:GCTimeRatio:这个设置吞吐量比如设置N那么他自动调节系统话费不超过1*(1n)时间用于GC默认99即不超过1% 的时间用来GC-XX:UseAdptiveSizePolicy:这个开启自适应GC调节策略此时新生代eden和survivor的比例晋升老年代时间都回被自动调节以达到对大小吞吐量和停顿时间的平衡点。在手工调节比较困难时候用这个我们只要指定虚拟机最大堆目标吞吐量GCTimeRatio和停顿时间MaxGCPauseMillis
老年代并行回收收集器
和新生代的并行回收收集器一样只不过用的标记压缩算法JVM参数-XX:-UseParallelOldGC开启新生代老年代都使用并行回收收集器其他JVM调优的参数新生代的一致
CMS收集器 Concurrent Mark Sweep并发标记清除标记清除算法同时也是多线程并行回收的垃圾收集器回收过程如下 初始标记 标记GCRoots能直接管理到的对象速度快独占系统资源并发标记GC Roots Tarcing过程即可达性分析并发的与用户线程一起重新标记为修正上一个步骤中标记期间用户程序引起的对象标记的变动重新进行更正独占系统资源并发清楚并行对未标记对象进行清楚与用户线程一起并发重置GC完成之后重新初始化CMS数据结构和数据为下一次垃圾回收做准备与用户线程一起 如下图所示 JVM参数介绍CMS执行期间有部分并行执行对吞吐量影响是不可避免的 CMS默认线程是(ParallelGCThread3)/4parallelGCThreads是新生代收集器线程数量通过-XX:ParallelCMSThread参数手动设定CMS线程数CMS回收启动阀值设置:-XX:CMSInitiatingOccupancyFraction,默认68也就是老年代空间使用率68% 时候执行CMS回收意义在于CMS是并行GCGC进行同时应用线程也在运行必须占用一定资源如果等到100% 在去GC就只能停顿为了互不影响需要GC的同时保留一部分内存给用户线程如果GC到一半内存不够则CMS回收失败JVM将启动老年代串行收集器这样直接Stoptheworld。内存压缩参数-XX:UseCMSCompactAtFullCollection开关可以使CMS的GC完成后进行碎片整理
G1收集器Garbage First
link G1资源 G1是JDK1.6出试用1.7 后才开始正式java1.9的默认收集器用于替代CMSDE新型GC解决CMS空间碎片等一系列缺陷是适用于java HotSpotVM的低暂停服务器风格的分带式垃圾回收器。 要了解G1的GC原理先要知道G1的一些设计理论。
Region
G1的结构内存在传统堆内存基础上在做了一次自定义的分割G1将内存划分成了大小相等的内存块Region默认512kRegion是逻辑上连续的物理内存地址上不一定连续。每个内存地址都属于某一个堆内存区域比如EedenS:SurvivorO:oldH:Humongous其中OH都是老年代。如下图
夸代引用 YoungGC的时候Young区域年轻代对象还可能存在Old区的引用这是夸代引用此时如果年轻代GC触发为避免扫描整个老年代G1 引入了两个概念Card Table和Remember Set。思路是空间换时间用来记录Old到Young区的引用。 RSet全称RememberSet用来记录外部指向Region的所有引用每个Region都维护一个RSetCardTableJVM将内存划分成固定大小Card也就是每个Region划分成多个card逻辑上的划分。 如下图展示上面部分中数据结构 如上图RSet与Card每个Region多个Card绿色部分Card表示这个card中有对象引用了其他card中对象Rset是一个HashTablekey是Region的起始地址value是catdTable类似MapObject1, Array,Array就是整个Regionobject1是Region的起始地址这样就可以通过数组下标找到对应引用的card当Card被引用就标记为dirty_card。
SATB SATB全称Snapshot At the BeginningGC钱存活对象的快照。作用是保证并发标记阶段的正确性。理解整个我们需要先知道三色标记算法如下图 黑色根对象或者该对象与它的子对象都被扫描过 灰色对象本身被扫描单没有扫描他的子对象 白色未被扫描对象扫描完成所有对象后最终为白色的是不可达对象即垃圾对象 如下案例GC扫码C之前颜色如下 并发标记阶段应用线程改变了引用关系,得到之后结果
A.c C
B.c null在重新标记阶段结果如下 这种情况C会被清理因为在第二阶段A已被标记黑色说明A及其子类已经被扫描过之后不会再重复扫描。此时Snapshot存活对象原来是A,B,C现在是A,B。快照被破坏显然不合理。 解决凡是G1 采用pre-write barrier解决在并发标记阶段引用关系变化的记录都通过pre-write barrier存储在一个队列JVM源码中这个队列叫satb_mark_queue。在remark阶段重新标记阶段会扫描这个队列通过这种凡是旧的引用指向对象也会被标记其子孙类递归标记上不会漏任何对象以此保证snapshot完整性。 此部分也是G1 相对CMS的一个优势在remark阶段是独占的CMS中incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root但是G1 的SATB设计在remark阶段只需要扫描队剩下的satb_mark_queue解决CMS的GC重新标记阶段长时间STW的潜在风险。 弊端 Satb方式记录存活对象当snapshot时刻可能之后这个对象就变成了垃圾这个叫浮动垃圾floating garbage这种只能下次GC回收这次GC过程中性分配的对象是活的其他不可达对象就是死的。 接着又有问题:下次GC如何区分哪些对象是GC开始后重新分配哪些是浮动垃圾 Region中通过top-at-mark-startTAMS指针分别为prevTAMS和nextTAMS记录新分配对象如下图 如上Region中指针设计top是region当前指针 [bottom,top) 区间是已用部分[top, end)是未使用的可分配空间[bottom,prevTAMS)这部分对象存活信息通过prevBitMap来记录[prevTAMS,nextTAMS)这部分对象在n-1轮concurrent marking隐式存活[nextTAMS,top)这部分对象在第N轮concurrent marking是隐式存活 通过以上几个区域划分可以得出每次GC应该清理的上次留下来的隐式存活对象并且进行清理。
G1 的Young GC
Young GC回收的所有年轻代Region和其他的垃圾收集器规则算法类似使用复制算法当E区不能在分片新对象时候触发GC如下图所示 E区对象会移动到s区S区空间不够E去对象直接到O区同时S区的数据移动到新的S区如果S区部分对象达到一定年龄晋升O区
Mixed GC
混合回收因为他回收所有年轻代Region 部分老年代的Region为什么是老年代部分Region?什么时候触发Mixed GC?上面的问题一回收部分老年代是参数-XX:MaxGCPauseMillis用来指定一个G1收集过程目标停顿时间默认值200ms当然这只是一个期望值。G1的强大之处在于他有一个停顿预测模型Pause Prediction Model他会有选择的挑选部分Region去尽量满足停顿时间关于G1的这个模型是如何建立的这里不做深究。上面问题二Mixed GC的触发也是由一些参数控制。比如XX:InitiatingHeapOccupancyPercent表示老年代占整个堆大小的百分比默认值是45%达到该阈值就会触发一次Mixed GC。Mixed GC主要分两个阶段阶段一全局并发标记有如下几个环节 初始标记initial markSTW标记GC root开始直接可达的对象。并发标记Concurrent Marking这个阶段GC root开始对heap中对象的标记标记线程与应用线程并行并且手机各个Region存活对象信息还扫描单文提到的SATB write barrier锁记录下的引用。最终标记Remark STW标记那些并发标记阶段发生变化的对象将被回收清除垃圾cleanup部分STW这个阶段如果发现完全没有活对象的region就将其整体回收到可分配region列表清除空Region 阶段二拷贝存活对象Evacuation Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去并行拷贝然后回收原来的region的空间。Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合collection set简称CSetCSet集合中Region的选定依赖于上文中提到的停顿预测模型该阶段并不evacuate所有有活对象的region只选择收益较高的region来evacuate这种暂停的开销就可以一定范围可控。 MixedGC清理过程示意图 Full GC
G1垃圾回收是和应用程序并发执行当Mixed GC速度赶不上程序申请内存的速度Mined G1 就只能降级到Full GC使用的是Serial GC。导致长时间STW导致切换到FullGC的原因可能有如下 Evacuation的时候没有足够to-space存放晋升的对象GC并发处理过程完成之前空间耗尽
G1总结
G1收集器用标记压缩算法不产生碎片没有CMS收集器中独立的碎片整理工作G1收集器可以可以进行精确停顿控制开发人员指定长度为M的时间段中GC时间不超过N。JVM相关参数如下 //JVM参数控制启用G1回收器-XX:UnlockExperimentalVMOptions -XX:UseG1GC -XX:MaxGCPauseMillis 50-XX:GCPauseIntervalMillis 200StopTheWorld案例
用一个Demo模拟StopTheWorld说明Gc对程序性能的影响
public class StopTheWorldTest {public static class MyThread extends Thread{MapLong, Object map new HashMap();Overridepublic void run(){try {while(true) {if (map.size() * 512 / 1204 / 1024 400) {//防止OOMmap.clear();System.out.println(clean map);}byte[] b;for (int i 0; i 100; i) {b new byte[512];map.put(System.nanoTime(), b);}}}catch (Exception e){}}}public static class PrintThread extends Thread{public static final long startTime System.currentTimeMillis();Overridepublic void run(){try{while (true){Long t System.currentTimeMillis() -startTime;System.out.println(t/1000 . t%1000);Thread.sleep(100);}}catch (Exception e){}}}public static void main(String[] args) {MyThread myThread new MyThread();PrintThread p new PrintThread();myThread.start();p.start();}
}
//JVM参数
-Xmx512M -Xms512M -XX:UseSerialGC -Xloggc:gc.log -XX:PrintGCDetails
//输出
......
1 . 977
2 . 715
[Full GC (Allocation Failure) [Tenured: 349567K-349567K(349568K), 0.3569460 secs] 506815K-506815K(506816K), [Metaspace: 3234K-3234K(1056768K)], 0.3569830 secs] [Times: user0.35 sys0.00, real0.36 secs]
3 . 777
[Full GC (Allocation Failure) [Tenured: 349567K-349567K(349568K), 0.3663700 secs] 506815K-506815K(506816K), [Metaspace: 3238K-3238K(1056768K)], 0.3664147 secs] [Times: user0.36 sys0.00, real0.36 secs]
[Full GC (Allocation Failure) [Tenured: 349568K-349568K(349568K), 0.4278699 secs] 506816K-506815K(506816K), [Metaspace: 3239K-3239K(1056768K)], 0.4279018 secs] [Times: user0.42 sys0.01, real0.43 secs]
4 . 144
[Full GC (Allocation Failure) [Tenured: 349568K-349568K(349568K), 0.4185880 secs] 506815K-506815K(506816K), [Metaspace: 3240K-3240K(1056768K)], 0.4186427 secs] [Times: user0.42 sys0.00, real0.42 secs]
[Full GC (Allocation Failure) [Tenured: 349568K-349568K(349568K), 0.4118220 secs] 506815K-506815K(506816K), [Metaspace: 3240K-3240K(1056768K)], 0.4118646 secs] [Times: user0.40 sys0.00, real0.41 secs]
[Full GC (Allocation Failure) [Tenured: 349568K-349567K(349568K), 0.4089358 secs] 506815K-506815K(506816K), [Metaspace: 3240K-3240K(1056768K)], 0.4089639 secs] [Times: user0.41 sys0.00, real0.41 secs]
.....如上我们模拟内存申请每毫秒打印如果有一段时间不输出就是在GC中,如上日志中1.977-2.715-3.777这个过程中一直在fullGC虽然只用几毫秒但是频繁的中断打印线程
GC对系统新能影响
同样用一个DEMO说明GC对程序运行时间上的影响
public class GCTimeTest {static HashMap map new HashMap();public static void main(String[] args) {long beginTime System.currentTimeMillis();for (int i 0; i 10000; i) {if (map.size() * 512 / 1024 / 1024 400) {map.clear();System.out.print(clean up);}byte[] b1;for (int j 0; j 100; j) {b1 new byte[512];map.put(System.nanoTime(), b1);}}long endTime System.currentTimeMillis();System.out.println(endTime - beginTime);}
}
回收器耗时ms-Xmx512M -Xms512M -XX:UseParNewGC1326-Xmx512M -Xms512M -XX:UseParallelOldGC -XX:ParallelGCThreads81565-Xmx512M -Xms512M -XX:UseSerialGC1530-Xmx512M -Xms512M -XX:UseConcMarkSweepGC1687
无奈开始用20M堆测试和预期结果相反但是耗时差不多推测应该是数量太小GC需要清理的总内存也就那么多得不出多月的结果我们调节更大的对内存清理时间必然更大应该就能得到对应的预期信息。
GC相关参数总结
与串型GC相关参数 -XX:UseSerialGC:年轻代老年代都用串型收集器-XX:SurvivorRatio:设置eden区域和survivor区域的比例-XX:PretenureSizeThreshold:奢姿大对象进入老年代的阀值当对象大小超过这个值直接进入老年代-XX:MaxTernuringThreshold:设置对象进入老年代年龄最大值每次MinorGC后对象年龄1任何大于这个年龄的对象都进入老年代。 与并行相关参数 -XX:UseparNewGC:新生代使用并行收集-XX:UseParallelOldGC:老年代使用并行回收收集器-XX:parallelGCThreads:设置用于垃圾回收的线程通常情况和CPU一样多核心CPU设置小的值也没错-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间此时JVM自动调节对内存大小或者其他参数控制时间在设置时间以内-XX:GCTimeRatio:设置吞吐量如果设置N那么系统话费不超过1/(1n)的时间用于GC-XX:UseAdaptiveSizePolicy: 打开自适应GC此时新生代eden和survivor比例晋升老年代对象年龄等参数都回被自动分配以达到堆大小吞吐量和停顿时间的平衡 与CMS回收器相关参数 -XX:UseConcMarkSweepGC:新生代使用并行收集器老年代使用CMS串型收集器-XX:ParallelGCThreads设置CMS的线程数量-XX:CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后触发默认68%-XX:UseCMSCompactAtFullCollection设置CMS收集器在完成垃圾收集后师傅要进行一次内存碎片整理-XX:CMSFullGCsBeforeCompaction设定进行多少次CMS垃圾回收后进行一次内存压缩-XX:CMSClassUnloadingEnabled允许堆类元数据进行回收-XX:CMSParallelRemarkEnable启动并行重标记-XX:CmsInitiatingPermOccupancyFraction当永久区占用率达到这百分比时候启动CMS回收前提是CMSClassUnloadingEnabled 激活了-XX:CMSIncrementalMode:使用增量模式比较适合单CPU 与G1相关的参数 -XX:UseG1GC使用G1回收器-XX:UnlockExperimentalVMOptions允许使用试验性参数-XX:MaxGCPauseMillis设置最大垃圾收集停顿时间-XX:GCPauseIntervalMillis设置停顿时间间隔 其他参数 -XX:DisableExplicitGC禁用显示GC
常用调优方案和方法
将对象预留在新生代
因为FullGC成本是全堆内存的清理MinorGC是年轻代的GC后者成本底因此我们可以合理的设置新生代的内存大小尽量将对象放在年轻代。如下Demo
public class PutInEden {public static void main(String[] args) {byte[] b1,b2,b3,b4,b5;//5M内存分配b1 new byte[1024*1024];b2 new byte[1024*1024];b3 new byte[1024*1024];b4 new byte[1024*1024];b5 new byte[1024*1024];}
}我们首先使用JVM参数 -XX:PrintGCDetails -Xmx20M -Xms20M可以看的如下GC日志,eden,from,to 年轻代三部分中分配占用5M0.5M0.5M空间老年代13M内存我们分配5M内存其中eden使用1M就是说只有b5在eden其他的在from和old PSYoungGen total 6144K, used 1633K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)eden space 5632K, 20% used [0x00000007bf980000,0x00000007bfa9c470,0x00000007bff00000)from space 512K, 96% used [0x00000007bff00000,0x00000007bff7c010,0x00000007bff80000)to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)ParOldGen total 13824K, used 4104K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)object space 13824K, 29% used [0x00000007bec00000,0x00000007bf002040,0x00000007bf980000)Metaspace used 3103K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 343K, capacity 386K, committed 512K, reserved 1048576K
修改JVM参数-XX:PrintGCDetails -Xmx20M -Xms20M -Xmn9M可以看的所有对象都在eden区域增大来年轻代的大小使其不必在老年代分配空间存储同样的参数类型有-XX:NewRatio(新生代与老年代比值) PSYoungGen total 8192K, used 6463K [0x00000007bf700000, 0x00000007c0000000, 0x00000007c0000000)eden space 7168K, 90% used [0x00000007bf700000,0x00000007bfd4ff00,0x00000007bfe00000)from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)ParOldGen total 11264K, used 0K [0x00000007bec00000, 0x00000007bf700000, 0x00000007bf700000)object space 11264K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf700000)Metaspace used 3102K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 343K, capacity 386K, committed 512K, reserved 1048576K
案例二
public class PutInEden {public static void main(String[] args) {byte[] b1,b2,b3,b4,b5;b1 new byte[1024*512];b2 new byte[1024*1024*4];b3 new byte[1024*1024*4];b3null;b3 new byte[1024*1024*4];}
}使用JVM参数 -XX:PrintGCDetails -Xmx20M -Xms20M -Xmn10M对内存20M新生代10M从GC日志中看出eden区8M内存在b3被分配时候eden超过一般触发gc将b1b2 移动到了old区域显然是不合理期望的结果应该是到from我们通过JVM参数调整from区大小 PSYoungGen total 9216K, used 6123K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)eden space 8192K, 74% used [0x00000007bf600000,0x00000007bfbfae40,0x00000007bfe00000)from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)ParOldGen total 10240K, used 8192K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)object space 10240K, 80% used [0x00000007bec00000,0x00000007bf400020,0x00000007bf600000)Metaspace used 3178K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 353K, capacity 386K, committed 512K, reserved 1048576K
JVM参数调节from区域-XX:PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio2,设置eden:from2,实际是5120:2560符合预期设置调整了from区域的大小使其能够存储下b1如下from区占用40%b2最终在old区符合预期 PSYoungGen total 7680K, used 5274K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)eden space 5120K, 83% used [0x00000007bf600000,0x00000007bfa26858,0x00000007bfb00000)from space 2560K, 40% used [0x00000007bfb00000,0x00000007bfc00010,0x00000007bfd80000)to space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000)ParOldGen total 10240K, used 8200K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)object space 10240K, 80% used [0x00000007bec00000,0x00000007bf402020,0x00000007bf600000)Metaspace used 3178K, capacity 4494K, committed 4864K, reserved 1056768Kclass space used 353K, capacity 386K, committed 512K, reserved 1048576K大对象进入老年代
一般情况我们应该提倡对象在eden区域但是如果一个大对象占用整个年轻代其他对象不得不分配在old区域这和堆GC相当不利我们用-XX:PretenureSizeTHreshold来设置进入老年代的阀值当对象大小超过设置直接在老年代分配内存。例如-XX:PretenureSizeThreshold1000000 ,设置阀值1M
设置对象进入老年代年龄
JVM参数-XX:MaxTenuringThreshold设置进入老年代年龄默认是15eden中对象经一次GC依然存活进入from年龄记1每经一次GC增加一直到年龄等于设置的值则进入old区并不是设置10 就一定10次GC才进入因为对内存空间有限制如果内存不足回提前进入old区所以实际上对象进入old区的年龄是虚拟机更具内存大小动态计算的一个值和 设置的值中取最小值。
稳定与震荡的堆大小
一般我们设置堆内存-Xms-Xmx最大最小堆设置相同值得到一个稳定的堆内存空间这样可以减少GC的次数但是每次GC的时间都是比较长的和小堆内存比较但是有一些特殊的业务比如在某个时间段才需要1G的对内存其他时间都只需要300M内存这样每次GC需需要清理的控制仍然会是1G此时我们可以利用如下两个参数 -XX:MinHeapFreeRatio设置堆内存最小空闲比例默认40。当堆内存空间空闲区域小于40%时候自动扩大堆内存-XX:MaxHeapFreeRatio设置堆内存最大空闲比例默认70当堆内存空间空闲区域大于70%时候自动缩小堆内存注意用这两个参数必须设置-Xms与-Xmx不一样才能生效
吞吐量优先案例
现在服务器都是多核服务器吞吐量优先情况我们优先考虑并行回收收集器如下参数
java -Xmx3800M -Xms3800 -Xmn2G
-Xss128k //减少线程栈大小可以支持更多线程
-XX:UseParallelGC //年轻代用并行回收收集器
-XX:ParallelGCThreads20 //设置用于GC线程的数量
-XX:UseParallelOldGC //老年代使用并行回收收集器使用大页案例
在Solaris系统Sun Microsystems研发的计算机操作系统。JVM支持大页使用使用大的内存分页可以增强CPU的内存寻址能力从而提升系统新能。JVM参数使用-XX:LargePageSizeInBytes
降低停顿案例
降低GC耗时就能降低停顿首先考虑关注系统停顿的CMS收集器其次减少FullGC尽量将对象预留在新生代使用Minor GC成本更小。我们有如下JVM参数
java -Xms2506M -Xmx2506M -Xmn1536M -Xss128k
-XX:ParallelGCThread20 //并行回收收集线程20个
-XX:UseConcMarkSweepGC //老年代使用CMS收集器
-XX:UseParNewGC //年轻代使用并行回收器
-XX:SurvivorRatio8 //年轻代edenfrom8:1
-XX:TargetSurvivorRatio90 //设置survivor可食用的百分比90%默认50%超过则进入老年代
-XX::MaxTenuringThreshold31 //晋升老年代年龄31次GC默认15实用JVM参数
堆快照堆Dump
通过使用如下JVM参数可以使得到程序在OOM的时候将堆Dump信息输出到固定的目录中 -XX:HeapDumpOnOutOfMemoryError 指定OOM的时候到处应用程序当前Dump-XX:HeapDumpPath 指定保存位置
Java VisualVM使用教程
以MAC为案例进行分析其他基本一致 进入java_home目录下控制态输入/usr/libexec/java_home 可以获取java_home地址 进入bin目录找到jvisualVM运行即可可以看到如下 执行需要监控的代码可以在本地目录下看到以及在运行的项目双击既可到监控页面 本次案例
//JVM参数-XX:PrintGCDetails -Xmx5M -Xms5M -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/Users/jiamin
public class PutInEden {public static void main(String[] args) throws InterruptedException {MapObject,Object map new HashMap();byte[] b1;for (int i 0; i Integer.MAX_VALUE; i) {b1 new byte[100];map.put(System.nanoTime(), b1);Thread.sleep(100);}}
}分配4M堆指定dump文件目录每一毫秒分配100B内存观察堆内存占用GC次数一次每次GC时候堆内存变化线程数量等新能指标 以上堆空间循环上升按规律下降可以看出每一毫秒分配一个byte对象并且加入map中一定时间后回触发GC应为堆内存只有4M也可手动执行GC查看线程当前内存信息CPU信息呢可从一下入口 可以dump出当前堆快照查看当前时刻各个性能指标等功能。自动生成Dump文件同样左边控制台上查看并且支持OQL查询功能 错误处理
系统OOM内存占满CPU带宽等资源应某种不可预知方式占用异常可能导致程序假死我们可以设置某个情况触发运行第三方脚本来解除这种状态如下JVM参数-XX:OnOutOfMemoryErrorc:/reset.bat, 每当oom的时候我们执行这个第三方脚本
Jvisual对OQL的支持
堆内存快照分析时候因为信息太多我们可能一时查不到需要的类信息你们OQL就提供了对象查询语言的支持
VisualVM的OQL基本语法
基本语法如下所示
select javascript expression to select
[
from [instanceof] class name identifier
[where javaScript boolean expression to filter]
]三部分组成 select子句指定查询结果要线上的内容from子句指定查询范围可指定类名如java.lang.Stringchar[]where子句指定查询条件 支持javascript语法处理较复杂的查询逻辑select 中可以使用类似JSON的语法输出多个例from中可以使用instanceof关键字将给定的子类也包括到输出中如下案例中可以直接访问对象的属性和部分方法
select s from byte[] s where s.length10如下查询语句还是PutInEden 案例来查询byte数组大于10 的对象信息执行语句如下图结果信息 上面案例是最基础查询方式解析我们用JSON语法进行查询
select {instance:s, content:s.toString()} from java.lang.String s where /^\d{2}$/(s.toString))上面用的JSON语法指定输出两例String对象已经String.toString的输出where正则匹配指定符合条件的字符串查询文件路径已经文件对象其中调用了类的toString方法
select {content:file.path.toString(), instance:file} from java.io.File file 利用instance查询所有ClassLoader包括子类
select c1 from instanceof java.lang.ClassLoader c1内置heap对象
heap对象是Visual VM OQL的内置对象通过heap可以实现强大OQL功能 forEachClass()对每一个class对象执行一个回调操作他的使用方法类似heap.forEachClass(callback),其中callback为javascript函数。findClass():查找指定名称的类对象返回类的方法和属性类似heap.findClass(className);objects()返回对快照中所有对象集合使用方法heap.objects(clazz,[includeSubtypes], [filter])其中clazz指定类名称includeSubtypes指定是否选出子类filter为过滤器指定筛选规则includeSubtypes和filter可以省略。livepaths返回指定对象的存活路径即显示哪些对象直接或者间接引用类给定对象。roots这个堆的根对象使用方法 heap.roots()
//查找demo中指定名称对应的类
select heap.findClass(com.ljm.jvm.PutInEden).superclasses()
//查找IO包下的对象
select filter(heap.classes(), java.io)对象函数
VIsualVM中OQL还提供类对象为操作目标的设置函数如下classof函数返回给定java对象类调用方法如下返回属性如下 有如下属性name类名superclass父类statics类的静态变量的名称和值fields类的域信息。class拥有以下方法isSubclassOf():是否是指定类的子类isSuperclassOf():是否是指定类的父类subclasses():返回所有子类superclasses():返回所有父类
select classof(v) from instanceof java.util.Vector vobjectid函数返回对象ID
select objectid(v) from java.util.Vector vreachables函数返回给定对象的可达对象集合使用方法如下
reachables(obj,[filter])
//返回56 这个String对象的所有可达对象
select reachables(s) from java.lang.String s
where s.toString 56referees函数返回给定对象的直接引用对象集合 referees(obj)
//返回FIle对象的静态成员变量引用
select referees(heap.findClass(java.io.File))sizeof函数返回指定对象大小不包括对象的引用对象因此sizeof的返回值由对象的类型决定和对象具体内容无关
//返回所有byte数组的大小以及对象
select {size:sizeof(o), Object:o} from byte[] orsizeof函数返回对象及其引用对象的大小综合即深堆Retained Size不仅与类本身有关还与对象当前数据内容有关
select {size:sizeof(o), rsize:rsizeof(o)}
from java.util.Vector o
集合统计函数
contains函数判断给定集合是否包含满足给定表达式的对象contains(set, boolexpression)内置对象 it当前访问对象index当前对象索引array当前迭代的数组/集合
//返回被File对象引用的String对象的集合先通过referrers得到所有引用String对象的对象集合使用contains函数以及布尔等式将contains的筛选条件设置为类名是java.io.File的对象
select s.toString() from java.lang.String s where
contains(referrers(s), classof(it).name java.io.File)count函数返回指定集合内满足给定布尔表达式的对象数量。count(set, [boolexpression]),boolexpression是布尔条件表达式同上面一样三个内置对象
select count(heap.classes(),java.io)filter函数返回给定集合中满足某一个布尔表达式的对象子集合length函数返回给定集合的数量map函数将结果集中的每一个元素按照特定规则转换以方便输出显示。max函数计算病得到给定集合的最大元素min函数计算病得到给定集合的最小元素sort函数堆指定集合进行排序top函数在给定集合中按照特定顺序排序的前几个对象sum函数用于计算给定集合的累计值
程序化OQL
VisualVM可以通过OQL相关的JAR包将OQL查询程序化从而获得更加灵活的查询功能。实现堆快照分析的自动化。略
取得GC信息
有如下JVM参数来获取GC信息 -XX:PrintGCDetails打印GC总体情况并且输出新生代来年的格子GC以及耗时-XX:PrintTenuringDistribution查看新生对象晋升老年代实际阀值-XX:PrintHeapAtGC打印堆使用情况比第一个更加详细包括两部分GC前堆信息GC后堆信息-XX:PrintGCApplicationStoppedTime显示应用程序在GC时候停顿时间-XX:PrintGCApplicationConcurrentTime显示应用程序在GC停顿期间执行时间-Xloggc 指定GC日志输出位置-Xloggc:C:\gc.log
类与对象跟踪
JVM参数中提供类用来观察类加载写在的参数 -XX:TraceClassLoading类加载情况-XX:TraceClassUnloading类卸载情况-verbos:class:同时打开类加载卸载
控制GC
JVM参数中-XX:DisableExplicitGC,用于禁止显示的GC也就是我们在代码中System.gc这种操作这做的原因在于JVM认为应该相信他的判断会在恰当时候发起GC类回收问题因为在绝大多数情况不需要进行类的回收因为类回收性价比太低元数据被载入后基本回持续整个程序进行类回收基本上释放不了多少空间。JVM参数通过 -Xnoclassgc 参数来控制在GC过程中不会发生类回收间接提升GC性能-Xincgc这个JVM参数让系统进行增量式GC意思是用特定算法让GC线程和应用程序线程交叉执行从而减少应用程序因为GC产生的停顿时间。
选择类校验器
JDK1.6后默认用新的类校验器参数-XX:-UseSplitVerifier 用来关闭新校验器开启旧校验器-XX:-FailOverToOldVerifier 关闭再教育功能作用在于当新校验器校验失败可以开启老校验器进行再次校验但是时间增加
压缩指针
64位操作系统比32位版本内存占用大1.5倍左右因为64位拥有更宽的寻址空间与32位比指针长度翻倍我们用如下JVM参数解决 -XX:UseCompressedOops打开后可以堆以下进行压缩 class属性指针静态成员变量对象属性指针普通对象数组的每个元素指针
实战JVM调优
Tomcate简介与启动加速
首先增加打印GC参数在catalina.sh增加配置参数如下每次启动后都会生成一个GC日志问题
JAVA_OPTS -Xloggc:/Users/jiamin/work_soft/tomcat7.0/apache-tomcat-7.0.77/gc.$$.log我们配置堆内存病通过观察GC日志来堆Tomcat启动情况进行分析配置如下参数
JAVA_OPTS-Xms32m -Xmx32m -Xloggc:/Users/jiamin/work_soft/tomcat7.0/apache-tomcat-7.0.77/gc.$$.log -XX:DisableExplicitGC启动Tomcat并且查看GC日志信息
CommandLine flags: -XX:DisableExplicitGC -XX:InitialHeapSize33554432 -XX:MaxHeapSize33554432 -XX:MaxNewSize16777216 -XX:NewSize16777216 -XX:PrintGC -XX:PrintGCTimeStamps -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:UseParallelGC
0.449: [GC (Allocation Failure) 12288K-3206K(30720K), 0.0054239 secs]
0.730: [GC (Allocation Failure) 15494K-5902K(30720K), 0.0059441 secs]
0.931: [GC (Allocation Failure) 18190K-8205K(30720K), 0.0049580 secs]
1.086: [GC (Allocation Failure) 20493K-10396K(30720K), 0.0067136 secs]
1.215: [GC (Allocation Failure) 22684K-12239K(30720K), 0.0054546 secs]
1.332: [GC (Allocation Failure) 24527K-14135K(24576K), 0.0048293 secs]
1.405: [GC (Allocation Failure) 20279K-15202K(27648K), 0.0049688 secs]
1.462: [GC (Allocation Failure) 21346K-15679K(27648K), 0.0027720 secs]
1.465: [Full GC (Ergonomics) 15679K-13990K(27648K), 0.0583449 secs]
141.964: [Full GC (Ergonomics) 20134K-9003K(27648K), 0.0463495 secs]以上日志显示经过8次MinorGC两次FullGC我们通设置新生代老年代比值的参数来扩大新生代减少FUllGC如下
JAVA_OPTS-Xms32m -Xmx32m -Xloggc:/Users/jiamin/work_soft/tomcat7.0/apache-tomcat-7.0.77/gc.$$.log -XX:DisableExplicitGC -XX:NewRatio2得到gc日志信息
CommandLine flags: -XX:DisableExplicitGC -XX:InitialHeapSize33554432 -XX:MaxHeapSize33554432 -XX:NewRatio2 -XX:PrintGC -XX:PrintGCTimeStamps -XX:UseCompressedClassPointers -XX:UseCompressedOops -XX:UseParallelGC
0.311: [GC (Allocation Failure) 8704K-2424K(31744K), 0.0041321 secs]
0.617: [GC (Allocation Failure) 11128K-4419K(31744K), 0.0042971 secs]
0.835: [GC (Allocation Failure) 13123K-6160K(31744K), 0.0060738 secs]
0.983: [GC (Allocation Failure) 14864K-8129K(31744K), 0.0051707 secs]
1.072: [GC (Allocation Failure) 16833K-9418K(31744K), 0.0044853 secs]
1.193: [GC (Allocation Failure) 18122K-11046K(26624K), 0.0066215 secs]
1.238: [GC (Allocation Failure) 14630K-12013K(29184K), 0.0043997 secs]
1.298: [GC (Allocation Failure) 15597K-12212K(29184K), 0.0023517 secs]
1.334: [GC (Allocation Failure) 15794K-12356K(29184K), 0.0025246 secs]
1.356: [GC (Allocation Failure) 15940K-12762K(29184K), 0.0029685 secs]
1.390: [GC (Allocation Failure) 16346K-12849K(29184K), 0.0024812 secs]
1.453: [GC (Allocation Failure) 16433K-13049K(29184K), 0.0011466 secs]
1.512: [GC (Allocation Failure) 16633K-13312K(29184K), 0.0010865 secs]
1.531: [GC (Allocation Failure) 16896K-13693K(29184K), 0.0011136 secs]
1.557: [GC (Allocation Failure) 17277K-13926K(29184K), 0.0012882 secs]
1.578: [GC (Allocation Failure) 17510K-14371K(29184K), 0.0012781 secs]FullGC以及不存在我们通过将新生代内存扩大方式减少FUllgc用MinorGC来替代。继续优化可以使用新生代并行回收收集器因为大多都是MinorGC这样可以将GC的影响尽可能缩小。
JAVA_OPTS-Xms32m -Xmx32m -Xloggc:/Users/jiamin/work_soft/tomcat7.0/apache-tomcat-7.0.77/gc.$$.log -XX:DisableExplicitGC -XX:NewRatio2 -XX:UseParallelGC优化结果Tomcat启动速度快来100来毫秒以上是针对一个裸的tomcat我们可以通过JVM参数进行优化启动速度如果有项目部署到上面我们需要更具具体情况具体分析
Web项目调优情况
还是用刚才的Tomcat我们用一个最简单的Blog项目做试验利用Jmeter来做压力测试与性能测试JMeter是一个开源软件能方便的堆Web应用程序尽量压力与性能测试我们逐步用如下参数进行优化压测
第一次
JAVA_OPTS-Xms512m -Xmx512m -XX:PermSize32M -XX:MaxPermSize32M -XX:DisableExplicitGC第二次
JAVA_OPTS-Xms512m -Xmx512m -XX:PermSize32M -XX:MaxPermSize32M -XX:PrintGCDetails -XVerify:none -XX:UseParallelGC -XX:UseParallelOldGC -XX:ParallelGcThreads8 -XX:DisableExplicitGC第三次
JAVA_OPTS-Xms512m -Xmx512m -XX:PermSize32M -XX:MaxPermSize32M -XX:PrintGCDetails -XX:DisableExplicitGC -XX:UseConcMarkSweepGC -XX:ParallelCMSTHreads8, -XX:UseCMSCOmpactAtFullCollection -XX:CMSFullGCsBeforeCompaction0(设定进行多少次CMS垃圾回收后进行一次内存压缩)
-XX:CMSInitiatingOccupancFraction78
-XX:SoftRefLRUPolicyMSPerMB0
-XX:CMSParallelRemarkEnabled
-XX:SurvivorRatio1
-XX:UseParNewGC 如上参数分别测试 第一次用JMeter测试GC总次数下降到39次TOmcat吞吐量上升到47.9/是可以看到堆内存大小堆系统性能影响如需要进一步提高吞吐量可以使用并行回收收集器第二三次年轻代老年代都使用并行回收收集器吞吐量在次提升51/s谁我们还可以使用CMS收集器测试。同时我们还设置来一个较大都survivor区将对象尽量留在年轻代通过将FullGC触发的阀值设置为78%即老年代占用达到78%时候才触发 结论JVM调优主要流程 确定堆内存大小-Xmx -Xms合理分配新生代老年代-XX:NewRatio,-Xmn,-XX:SurvivorRatio确定永久区大小-XX:Permsize, -XX:MaxPermSize选择垃圾收集器堆垃圾收集器进行合理设置禁用显示GC-XX:DeisableExplicitGC禁用类元数据回收-Xboclassgc禁用类验证-XVerify:none
Java性能调优工具
Linux命令行工具
top命令是linux下常用的性能分析工具能够实时显示各个进程的资源占用状态。如下图 top中信息分两部分上面是系统统计信息后面是进程信息第一部分中信息 第一行是分五对了结果等于uptime命令左到右依次表示系统当前时间系统运行时间当前登陆用户数最后的load average 表示系统平均负载即任务队列的平均长度这三个值分别表示1分钟5分钟15分钟到现在的平均值第二行进程统计信息分别有正在运行的睡眠的进程停止的进程僵尸进程数量第三行是CPU统计信息us表示用户空间CPU占用SY表示内核空间CPU占用NI表示用户进程空间改变过优先级的进程CPU占用id表示空间CPU占用wa表示等待输入输出的CPU时间百分比hi表示硬件中断请求si表示软件中断请求Men行中从左到右依次表示物理内存总量已使用物理内存空闲物理内存内核缓冲使用量Swap行依次表示交换区总量空闲交换区大小缓冲交换区大小 第二部分是进程信息区显示系统中各个进程的资源使用情况 PID进程idPPID父进程IDREUSEFReal user nameUID进程所有这的用户idUSER进程所有者用户名GROUP进程所有者组名TTY启动进程的终端名称不是从终端启动则显示“”NInice值优先级负数表示优先级高P最后使用的CPU禁止多CPU环境下有意义%CPU上次更新到现在CPU时间占用百分比TIME进程使用CPU总时间TIME进程使用CPU时间总计单位1/100秒%MEM进程使用物理内存百分比VIRT进程使用虚拟内存总量 单位KB VIRTSWAPRESSWA进程使用虚拟内存中被换出的大小RES进程使用的未被换出的物理内存大小RESCODEDATACODE可执行代码占用物理内存大小DATA可执行代码以外的部分占用物理内存大小SHR共享内存大小S进程状态 D表示不可终端的睡眠 R表示运行 S表示睡眠T表示跟踪/停止 Z表示僵尸进程 TOP命令下还可以进一步执行按f可进行列选择 h显示帮助信息K终止一个进程q推出c切换显示命令和完整命令行M根据主流内存大小进行排序P更具CPU使用辈分比进行大小排序T更具时间/累计时间进行排序数字1:显示所有CPU负载情况
sar命令
Linux 命令sar作用在周期性对内存和CPU使用情况进行采样基本语法
sar [options] [interval [count]]interval采样周期 和 count采样数量。option选项可以指定sar命令对那些性能数据进行采样 -A所有报告的总和-uCPU利用率-dIO情况-q查看队列长度-r内存使用统计信息-n网络信息统计-o采样结果输出到文件 以下案例统计CPU使用情况每秒采样一次共五次 sar -u 1 5 获取内存使用情况 sar -r 1 5 获取IO信息 sar -b 1 5
vmstat 命令 vmstat也是一个功能齐全的性能检测工具统计CPU内存使用情况swap使用情况等信息同样周期采样格式雷同 vmstat 1 5 没秒一次共五次 输出中各项意义 procs r等待运行的进程数b处于非中断睡眠状态的进程数 memory swpd虚拟内存使用情况单位KBfree空闲内存,单位KBbuff被用来最为呼延村的内存数单位KB swap si从磁盘交换到内存的交换页数量单位KB/秒so从内存交换到磁盘的交换页数量单位kb/秒 io bi发送到块设备的块数单位块/秒bo从块设备接收到块数单位块/秒 System in每秒中断数包括时钟中断cs每秒上下文切换次数 CPU us用户CPU使用时间sy内核CPU系统使用时间id空闲时间 总结vmstat工具可以查看内存交互分区IO操作上下文切换时钟中断以及CPU的使用情况
iostat命令 iostat可以提供详细的IO信息基本使用也雷同上几个iostat 1 5 以上命令每秒采样一次持续五次展示了CPU使用和磁盘IO的信息如果只需要磁盘IO信息iostat -d 1 5 -d标识输出磁盘使用情况结果各参数含义如下 tps该设备每秒传输次数KB_read/s每秒从设备读取的数据量KB_wrtn/s每秒向设备写入的数据量KB_read读取的总数据量KB_wrtn写入的总数据量 iostat可以快速定位系统是否产生了大量IO操作‘
pidstat工具
pidstat是一个功能强大的性能监控工具也是Sysstat组件之一pidstat不仅可以监视进程性能也可以监视线程的性能。
CPU使用监控
如下demo
public class HoldCPUMain {public static class HoldCpuTesk implements Runnable{Overridepublic void run() {while (true){double a Math.random()* Math.random();}}}public static class LazyTask implements Runnable{Overridepublic void run() {while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {new Thread(new HoldCpuTesk()).start();new Thread(new LazyTask()).start();new Thread(new LazyTask()).start();new Thread(new LazyTask()).start();}
} 以上demo中跑四个线程其中一个占用CPU资源其他空闲先使用jps找出java程序的PID 接着使用pidstat命令输出程序CPU使用情况 pidstat -p 1187 -u 1 3 如上图所示我们先用jps找到Java程序的PID然后pidstat命令输出CPU使用情况 pidstat参数 -p 用来指定进程ID-u指定值需要CPU的信息最后13表示每秒一次打印三次从输出看出CPU占用最后能达到100%idstat功能还可以监控线程信息如下命令
pidstat -p 1187 1 3 -u -t参数解析 -t参数将系统性能的监控细化到线程级别。如上图中看java引用程序之所以占用如此之高CPU是因为线程1204的缘故。我们使用以下命令导出指定Java应用的所有线程
jstack -l 1187 /temp/thread.txt如上图片信息线程HoldCPUTask类他的nidnative ID为0x4b4转为10进制后 1024,通过pidstat分析就很容易去获取到java应用程序中大量占用CPU的线程。
IO使用监控 磁盘也是常见性能瓶颈之一pidstat也可以监控IO情况,如下排查流程 top命令查看基础系统资源信息如下可以看到如下图中PID10580 的java进程占用VIRT虚拟内存%CPU上次结束CPU占比都居于第一资源消耗厉害 获取到pid后用如下命令获取对应详细线程信息如下图所示
pidstat -p 10580 -d -t 1 3如下结果看出进程10580中线程 10582,105831058410585线程产生了大量IO操作通过jstack命令来查一下10585线程可以定位到具体的线程信息。
内存监控
使用pidstat命令还可以监控指定进程的内存使用情况我们用如下命令得出如下图中的结果
pidstat -r -p 10580 1 5输出结果中各列含义如下 minflt/s:该进程每秒minor faults不需要从磁盘中调出内存页的总数majflt/s:该进程每秒major faults需要从磁盘中调出内存页的总数VSZ:标识该进程使用的虚拟内存大小单位KBRSS:标识该进程占用物理内存大小单位KB%MEM标识占用内存比率 如上图中可以看到改进程占用虚拟内存5G 占用物理内存3G内存占用比率29.56%
总结pidstat是一款多合一工具可以同时监控CPUI-uIO-d线程-t内存-r同时在定位具体线程的时候基本上可以找到出问题的点方便引用程序的故障派查。
windows工具
最常用的Windows任务管理器perfmon性能监控工具Windows下专业级的监控工具并且windows中自带的一个工具在“运行”对话宽中输入perfmon或者控制面包中性能快捷方式都可以打开。Process Explorer一个进程管理工具可以替代Windows的任务管理器Pslist命令行windows下的一个命令行工具他可以显示进程乃至线程的详细信息类似pidstat
JDK命令行工具
用的最多的java.exejavac.exe这些都在jdk的bin目录下面所有的可执行文件的实现都在tools.jar中实现。
JPS命令
jps类似linux下的ps命令直接jps可以列出java程序的进程ID以及Main函数的名称
jiamindeMacBook-Pro:~ jiamin5$ jps
75712 AppMain
75664 RemoteMavenServer
75737 Jps
75711 Launcher
75454 Jps还提供类一系列参数来指定输出内容 -qjps -q 只输出进程ID不输出类的短名称-m用于输出传递给Java进程主函数的参数-l用于输出主函数的完整路径-v可以显示传递给JVM的参数
jstat命令
一个可以用来观察java应用程序运行时信息的工具功能强大可以查看堆信息
jstat -option [-t] [-hlines] vmid [interval] [coune]Option可以由以下构成 class显示ClassLoader相关信息compiler显示JIT变异的相关信息gc显示与GC相关的堆信息gccapacity显示各个代的容量以及使用情况gccause显示垃圾收集相关信息同-gcutil 同时显示最后一次或当前正在发生的GC的诱发原因gcnew显示新生代信息gcnewcapacity显示新生代大小与使用情况gcold显示老年代和永久代信息gcoldcapacity显示老年代大小gcutil显示垃圾收集信息printcompiliation输出JIT变异的方法信息 -t参数可以在输出信息前加timestamp列interval用来指定统计数据周期count指定统计次数如下案例
jstat -gccapacity -t 78305 1000 2 //显示各个代的容量以及使用情况
jstat -class -t 78305 1000 2 //每一秒统一一次类加载信息总共统计两次
jstat -gc -t 80800 1000 100 //打印堆内存各个区域使用情况GC情况如下输出
Timestamp S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 89.9 512.0 512.0 0.0 496.0 1024.0 892.5 4096.0 40.0 4864.0 3194.5 512.0 353.9 1 0.001 0 0.000 0.00190.9 512.0 512.0 0.0 496.0 1024.0 892.5 4096.0 40.0 4864.0 3194.5 512.0 353.9 1 0.001 0 0.000 0.00191.9 512.0 512.0 0.0 496.0 1024.0 913.1 4096.0 40.0 4864.0 3194.5 512.0 353.9 1 0.001 0 0.000 0.001以上含义从S0C开始依次为 s0Cs0from的大小S1Cs1from的大小S0Us0from已使用空间S1Us1from已使用空间ECeden大小EUeden区已使用空间OCold区大小OUold区已使用空间PC永久代大小PU永久代已使用空间YGC新生代GC次数YGCT新生代GC耗时FGCfullGC次数FGCTfullGC耗时GCTGC总耗时
jinfo命令
用了查看正在运行的java应用程序的扩展参数甚至支持运行是修改部分参数语法
jinfo option pid
option操作有如下 -flag打印指定JVM参数值
jmap
生成java应用程序的堆快照和对象的统计信息。通过jmap -histo 83009 /Users/jiamin/s.txt 可以得到运行的java程序在内存中实例数量和合计另外一个功能是生成当前堆快照 jmap -dump:formatb,file/Users/jiamin/dump.hprof 83009 生成dump文件可以用visualVM查看这个也是JDK自带的工具上面有对应说明
jhat命令
用于分析java应用程序堆快照信息上文中dump问及就可以用这个命令分析如下
jhat dump.hprof jiamindeMacBook-Pro:~ jiamin5$ jhat dump.hprof
Reading from dump.hprof...
Dump file created Thu May 07 22:06:52 CST 2020
Snapshot read, resolving...
Resolving 13100 objects...
Chasing references, expect 2 dots..
Eliminating duplicate references..
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.如上运行后可以在Http://127.0.0.1:7000中看到结果信息 如上图中因为堆快照信息非常大信息太多jhat还支持使用OQL语句最下面Query Language功能用来查询当前Java程序中所有java.io.File对象的路径如下select file.path.value.toString() from java.io.FIle PutInEden
jstack命令
用于到处java引用程序的线程堆栈语法如下 jstck [-l] -l 选项用了答应锁的附加信息jstack工具回在控制台输出程序中所有的锁信息可以使用重定向将输出保存文件
jstack -l 83183 /Users/jiamin/deadlock.txt//死锁输出如下
Found one Java-level deadlock
。。。。。。jstack输出中很容易找到死锁信息并同时显示发生死锁的两个线程以及死锁线程持有对象和等待对象帮助开发人员解决死锁问题。
jstatd命令 之前都是本机java程序的监控jstatd是远程监控jstatd是一个RMI服务员端程序相当于一个代理服务器建立本地与被监控计算机的监控工具的通信jstatd服务器将本机的java引用程序信息传递到远程计算机如下图 测试demo如下
public class HoldCPUMain {public static class HoldCpuTesk implements Runnable{Overridepublic void run() {while (true){double a Math.random()* Math.random();}}}public static class LazyTask implements Runnable{Overridepublic void run() {while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {new Thread(new HoldCpuTesk()).start();new Thread(new LazyTask()).start();new Thread(new LazyTask()).start();new Thread(new LazyTask()).start();}
}
hprof工具
hprof也不是独立的监控工具只是一个java agent工具可以用于监控java应用程序运行时CPU信息和堆信息。使用java -agentlib:hprofhelp可以查询hprof帮助文档使用hprof可以查看程序中各个函数的CPU占用时间
JConsole工具
JDK自带的图形化性能监控工具通过JConsole可以查看java引用程序的运行概况监控堆内存永久区类加载等
JConsole连接java程序
jdk的bin目录下运行jconsole.exe 就可以直接启动在本地连接窗口选择需要监控的进程id就可以。 java程序概况
以下基本情况监控包括堆内存系统线程类加载数量CPU使用 内存监控
切换内存监控,JConsole对堆内存监控精确到每一个区域包括edensurvivor老年代同时非堆区域的监控也存在。提供强制FullGC功能。
线程监控
还是上面所提到的demo代码对应的线程监控其中有Thread0,2,3,其中只有Thread-0是runable状态其他是TIMED_WAITING并且提供检测死锁按钮可以很好帮助研发人员获取死锁信息。
虚拟机监控
针对虚拟机的监控是对整体数据的一个监控其中包括线程堆操作系统概况信息包括虚拟机版本类型等。
MBean管理
MBean页面运行通过JConsole进行MBean的管理包括查看或者设置MBean的属性运行MBean的方法等。
使用插件
除了基本功能JConsole还支持插件JDK安装目录下就自带了一个插件位于JAVA_HOME/demo/management/JTop下面用如下命令启动可以让JConsole加载插件并启动
jconsole -pluginpath %JAVA_HOME%/demo/management/JTop/JTop.jar链接任意java程序进入JTop页面JTop插件按CPU占用时间排序
Visual VM 多合一工具
Visual VM是一个多功能组合的性能监控可视化工具集成了多种性能统计工具的功能可以替代上面提到的jstatjmapjhatjstack甚至JConsole在JDK67以后VIsualVM作为JDK一部分发布开源。VIsualVM 可以查询应用程序基本情况比如进程ID MainClass启动参数等Monitor页面可见CPU堆永久区类加载线程数的总体情况可手动执行FullGCThreadDump和分析可一键生成当前堆内存ThreadDump可以导出当前所有线程的堆栈信息可以统计调用时长但是无法给出调用堆栈因此VisualVM还是无法确认一个函数被调用了多少次右边菜单中HeapDump命令可以立刻获取当前内存快照进行分析MBean管理和JConsole是完全一样的。
TDA
TDA是Thread Dump Analyzer的缩写是一款线程快照分析工具当使用jstack或者VisualVM等工具取得线程快照文件后通过文本编辑器查看分析线程快照文件是一件困难的事情而TDA办证分析线程抓取快照
BTrace介绍
BTrace是一个非常好用的工具新开了一篇文章来说明Java动态追踪技术–BTrace
MAT内存分析工具(这东西说是免费的)
Memory analyzer的简称一款功能强大的java堆内存分析工具有类似VisualVM的功能并且在堆内存的功能上更加详细
深堆浅堆
浅堆表示一个对象结构所占用堆内存大小深堆表示一个对象被GC回收后可以真实释放的内存大小或者说指只能通过该对象直接或者间接访问到的对象的浅堆只和如下案例解析
public class Point {private int x;private int y;public Point(int x, int y){this.x x;this.y y;}
}public class Line {private Point startPoint;private Point endPoint;public Line(Point startPoint, Point endPoint){this.startPoint startPoint;this.endPoint endPoint;}public static void main(String[] args) {Point a new Point(0,0);Point b new Point(0,0);Point c new Point(0,0);Point d new Point(0,0);Point e new Point(0,0);Point f new Point(0,0);Point g new Point(0,0);Line aLine new Line(a,b);Line bLine new Line(a,c);Line cLine new Line(d,e);Line dLine new Line(f,g);anull;bnull;cnull;dnull;enull;}
} 如上demo中引用关系如下图所示其中abcde对象用完就被赋null 更加Point类结构使用MAT得到内存快照得到每个point浅堆和深堆大小都是16字节 如上图引用dLine对象的两个点fg没有被null引用不被回收所以dline被回收后fg并不会被回收因此能释放的就是本身的16字节所以深堆是16 cLine的d和e被赋值null并且仅仅被Cline引用所以cline被释放de必然被释放所以深堆16*21648 aLine和bLine两个均持有对方一个点所以aLine被回收只有b是只被aline引用所以b被回收a因为还在bLine中被引用所以不被回收深堆是161632bLine同理
垃圾回收根
在java系统中作为GC的根节点可能是以下对象之一 系统类被bootstrap/system ClassLoader加载的类如在rt.jar包中的所有类JNI局部变量本地代码中局部变量如用户自定义的JNI代码或者JVM内部代码JNI全局变量本地代码中全局变量正在使用的锁作为锁对象比如掉了wait或者notify方法的对象或调用了synchronizedObject操作的对象java局部变量如函数中输入参数以及方法中局部变量本地栈本地代码中输入输出参数。比如用户自定义的JNI代码或者JVM内部代码Finalizer在等待队列中将要被执行析构函数的对象unfinalized拥有析构函数单没有被析构且不再析构队列中的对象不可达对象从任何一个根对象无法达到的对象但为了能够在MAT中分析被MAT标为根未知对象
最大对象报告
查找支配者
线程分析
集合使用情况分析
JProfile简介
JProfile是一款商业化的软件功能全面主要有内存分享CPU分享线程分析JVM性能信息收集等
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/930263.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!