当前位置: 首页 > news >正文

网站建设 套格式营销型企业网站的建设步骤

网站建设 套格式,营销型企业网站的建设步骤,个人主页网页设计模板免费,wordpress本地访问满微信公众号转载#xff0c;关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、字符串与集合性能优化 1.String 对象的实现 在 Java 语言中#xff0c;Sun 公司的工程师们对 String 对象做了大量的优化#xff0c;来节… 微信公众号转载关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、字符串与集合性能优化 1.String 对象的实现 在 Java 语言中Sun 公司的工程师们对 String 对象做了大量的优化来节约内存空间提升 String 对象在系统中的性能。 (1)在 Java6 以及之前的版本中String对象是对 char 数组进行了封装实现的对象主要有四个成员变量char 数组、偏移量 offset、字符数量 count、哈希值 hash。 String 对象是通过 offset 和 count 两个属性来定位 char[] 数组获取字符串。这么做可以高效、快速地共享数组对象同时节省内存空间但这种方式很有可能会导致内存泄漏。 (2)Java7/Java8Java 对 String 类做了一些改变。String 类中不再有 offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些同时String.substring 方法也不再共享 char[]从而解决了使用该方法可能导致的内存泄漏问题。 (3)从 Java9 版本开始将 char[] 字段改为了 byte[] 字段又维护了一个新的属性 coder它是一个编码格式的标识。 一个 char 字符占 16 位2 个字节。这个情况下存储单字节编码内的字符占一个字节的字符就显得非常浪费。JDK1.9 的 String 类为了节约内存空间于是使用了占 8 位1 个字节的 byte 数组来存放字符串。 而新属性 coder 的作用是在计算字符串长度或者使用 indexOf函数时我们需要根据这个字段判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值0 代表 Latin-1单字节编码1 代表 UTF-16。如果 String 判断字符串只包含了 Latin-1则 coder 属性值为 0反之则为 1。 2.String 对象的优化 (1)合理选择StringBuilder和String ①大量字符串拼接选择StringBuilder 循环体内不要使用””进行字符串拼接而直接使用StringBuilder不断append public String appendStr(String oriStr, String… appendStrs) { if (appendStrs null || appendStrs.length 0) { return oriStr; } for (String appendStr : appendStrs) { oriStr appendStr; } return oriStr; 每次虚拟机碰到””这个操作符对字符串进行拼接的时候会new出一个StringBuilder然后调用append方法最后调用toString()方法转换字符串赋值给oriStr对象即循环多少次就会new出多少个StringBuilder()来这对于内存是一种浪费。(只有代码换行使用String拼接)而且创建对象的过程也会浪费时间 ②少量的字符串拼接选择String String str ab cd ef  此时系统会自动完成优化 (2)合理String.intern 方法节省内存 ①针对可能出现大量重复的字符串使用intern(国家、省市、地区) SharedLocation sharedLocation new SharedLocation(); sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getCountryCode().intern());sharedLocation.setRegion(messageInfo.getCountryRegion().intern()); 在字符串常量中默认会将对象放入常量池在字符串变量中对象是会创建在堆内存中同时也会在常量池中创建一个字符串对象复制到堆内存对象中并返回堆内存对象引用。 如果调用 intern 方法会去查看字符串常量池中是否有等于该对象的字符串的引用如果没有jdk1.8只是会把首次遇到的字符串的引用添加到常量池中如果有就返回常量池中的字符串引用。 String a new String(abc).intern();String b new String(abc).intern(); if(ab) { System.out.print(ab);} 在一开始字符串abc会在加载类时在常量池中创建一个字符串对象。 创建 a 变量时调用 new Sting() 会在堆内存中创建一个 String 对象String 对象中的char 数组将会引用常量池中字符串。在调用 intern 方法之后会去常量池中查找是否有等于该字符串对象的引用有就返回引用。 创建 b 变量时调用 new Sting() 会在堆内存中创建一个 String 对象String 对象中的 char 数组将会引用常量池中字符串。在调用 intern 方法之后会去常量池中查找是否有等于该字符串对象的引用有就返回引用。 而在堆内存中的两个对象由于没有引用指向它将会被垃圾回收。所以 a 和 b 引用的是同一个对象。 ②针对重复值少不重复值多的字段不要使用intern 常量池的实现是类似于一个 HashTable 的实现方式HashTable 存储的数据越大遍历的时间复杂度就会增加。如果数据过大会增加整个字符串常量池的负担。如果 string pool 设置太小而缓存的字符串过多也会造成较大的性能开销。 (3)合理使用split ①避免回溯问题 除非是必须的否则应该避免使用splitsplit由于支持正则表达式使用不恰当会引起回溯问题很可能导致 CPU 居高不下如果确实需要频繁的调用split可以考虑使用apache的StringUtils.split(string,char)频繁split的可以缓存结果。 用 NFA 自动机实现的比较复杂的正则表达式在匹配过程中经常会引起回溯问题。大量的回溯会长时间地占用 CPU从而带来系统性能开销。 text“abbc”regex“ab{1,3}c” 首先读取正则表达式第一个匹配符 a 和字符串第一个字符 a 进行比较a 对 a匹配。 接着继续使用 b{1,3} 和字符串的第四个字符 c 进行比较发现不匹配了此时就会发生回溯已经读取的字符串第四个字符 c 将被吐出去指针回到第三个字符 b 的位置。 那么发生回溯以后匹配过程怎么继续呢程序会读取正则表达式的下一个匹配符 c和字符串中的第四个字符 c 进行比较结果匹配结束。 ②部分关键字比如.[]()\| 等需要转义 反例 a.ab.abc.split(.); // 结果为[]a|ab|abc.split(|); // 结果为[a, |, a, b, |, a, b, c] 正例 a.ab.abc.split(\\.); // 结果为[a, ab, abc]a|ab|abc.split(\\|); // 结果为[a, ab, abc] (4)关于toString()方法的使用 把一个基本数据类型转为字符串基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据 最慢 String.valueOf()方法底层调用了Integer.toString()方法但是会在调用前做空判断  i “”底层使用了StringBuilder实现先用append方法拼接再用toString()方法获取字符串  public static void main(String[] args){ int loopTime 50000; Integer i 0; long startTime System.currentTimeMillis(); for (int j 0; j loopTime; j) { String str String.valueOf(i); } System.out.println(“String.valueOf()” (System.currentTimeMillis() - startTime) “ms”); startTime System.currentTimeMillis(); for (int j 0; j loopTime; j) { String str i.toString(); } System.out.println(“Integer.toString()” (System.currentTimeMillis() - startTime) “ms”); startTime System.currentTimeMillis(); for (int j 0; j loopTime; j) { String str i “”; } System.out.println(“i \”\”” (System.currentTimeMillis() - startTime) “ms”);} 运行结果为 String.valueOf()11ms  Integer.toString()5ms i “”25ms  (5)使用 Apache Commons StringUtils.Replace 而不是 String.replace 一般来说String.replace 方法可以正常工作并且效率很高尤其是在你使用 Java 9 的情况下。但是如果你的应用程序需要大量的替换操作并且没有更新到最新的 Java 版本那么检查更快和更有效的替代品依然是有必要的。有一种候选方案是 Apache Commons Lang 的 StringUtils.replace 方法。 // replace thistest.replace(“test”, “simple test”);// with thisStringUtils.replace(test, “test”, “simple test”); 3.集合性能优化 (1)尽量初始ArrayList的大小 如果能估计到待添加的内容长度为底层以数组方式实现的集合、工具类指定初始长度。比如ArrayList、StringBuilder、StringBuffer、HashMap、HashSet等等以StringBuilder为例  StringBuilder() 默认分配16个字符的空间 StringBuilder(int size) 默认分配size个字符的空间 StringBuilder(String str) 默认分配16个字符str.length()个字符空间 可以通过类这里指的不仅仅是上面的StringBuilder的构造函数来设定它的初始化容量这样可以明显地提升性能。 比如StringBuilderlength表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候它会将自身容量增加到当前的2倍再加2无论何时只要StringBuilder达到它的最大容量它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中——这是十分耗费性能的一个操作。 但是注意像HashMap这种是以数组链表实现的集合别把初始大小和你估计的大小设置得一样因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂如果能估计到有2000个元素设置成new HashMap(128)、new HashMap(256)都可以。  (2) 公用的集合类中不使用的数据一定要及时remove掉  如果一个集合类是公用的也就是说不是方法里面的属性那么这个集合里面的元素是不会自动释放的因为始终有引用指向它们。 所以如果公用集合里面的某些数据不使用而不去remove掉它们那么将会造成这个公用集合不断增大使得系统有内存泄露的隐患。  (3)ArrayList进行优化 trimToSize()   减少无用的集合空间 ensureCapacity()   减少数组扩容所需的时间 subList()   进行集合中数据的批量删除 (4)arrayList和linkedList的选择 推荐使用arrayList只有在特定情况下才可以使用linkedList (5)hashMap的优化 当查询操作较为频繁时我们可以适当地减少加载因子如果对内存利用率要求比较 高我可以适当的增加加载因子。 (6)contains方法使用:使用HashSet替换ArrayList Set()的contain时间复杂度是O(1)而List.contain的时间复杂度是O(n)。 ①ArrayList得contains实现 contains()方法调用了indexOf()方法该方法通过遍历数据和比较元素的方式来判断是否存在给定元素。当ArrayList中存放的元素非常多时这种实现方式来判断效率将非常低。 public boolean contains(Object o) { return indexOf(o) 0;} ②HashSet得contains实现 containsKey()方法调用getEntry()方法。在该方法中首先根据key计算hash值然后从HashMap中取出该hash值对应的链表链表的元素个数将很少再通过变量该链表判断是否存在给定值。这种实现方式效率将比ArrayList的实现方法效率高非常多。 public boolean contains(Object o) { return map.containsKey(o);} 4.Stream性能优化 多核 CPU 服务器配置环境下对比长度 100 的 int 数组的性能 多核 CPU 服务器配置环境下对比长度 1.00E8 的 int 数组的性能 多核 CPU 服务器配置环境下对比长度 1.00E8 对象数组过滤分组的性能 单核 CPU 服务器配置环境下对比长度 1.00E8 对象数组过滤分组的性能。 常规的迭代stream 并行迭代 stream 串行迭代 span Stream 并行迭代 常规的迭代stream 串行迭代 span Stream 并行迭代 常规的迭代stream 串行迭代 span 常规的迭代stream 串行迭代 stream 并行迭代 span 在循环迭代次数较少的情况下常规的迭代方式性能反而更好在单核 CPU 服务器配置环境中也是常规迭代方式更有优势而在大数据循环迭代中如果服务器是多核 CPU 的情况下Stream 的并行迭代优势明显。 5.变量性能调优 (1)尽可能使用基本类型 避免任何开销并提高应用程序性能的另一种简便快速的方法是使用基本类型而不是其包装类。所以最好使用 int 而不是 Integer 是 double 而不是 Double 。这将使得你的 JVM 将值存储在堆栈而不是堆中以减少内存消耗并更有效地处理它。 避免引用类型和基础类型之间无谓的拆装箱操作请尽量保持一致自动装箱发生太频繁会非常严重消耗性能。 (2)尽量避免大整数和小数 大整数和小数尤其是后者因其精确性而受欢迎但这是有代价的。大整数和小数比一个简单的 long 型或 double 型需要更多的内存并会显著减慢所有的运算。所以如果你需要额外的精度或者如果你的数字超出一个较长的范围最好要三思。这可能是你需要更改并解决性能问题的唯一方法尤其是在实现数学算法时。 (3)已经获取到的值尽量传递不要重新计算 public Response dealRequest(Request request){ UserInfo userInfo userInfoDao.selectUserByUserId(request.getUserId); if(Objects.isNull(request)){ return ; } insertUserVip(request.getUserId); } private int insertUserVipString userId{ //又查了一次 UserInfo userInfo userInfoDao.selectUserByUserId(request.getUserId); //插入用户vip流水 insertUserVipFlow(userInfo); ....} 6.if条件 // 当使用时应将常不满足条件的判断放在前面// 比如isUserVip经常为false只要判断isUserVip就可以避免后续判断if(isUserVip isFirstLogin){ sendMsgNotify();} 二、IO与网络性能优化 I/O 的速度要比内存速度慢尤其在大数据时代背景下I/O 的性能问题更是尤为突出I/O 读写已经成为很多应用场景下的系统性能瓶颈。 1.传统IO问题及优化 (1)传统 I/O 的性能问题 ①多次内存复制 在传统 I/O 中我们可以通过 InputStream 从源数据中读取数据流输入到缓冲区里通过 OutputStream 将数据输出到外部设备包括磁盘、网络。 JVM 会发出 read() 系统调用并通过 read 系统调用向内核发起读请求 内核向硬件发送读指令并等待读就绪 内核把将要读取的数据复制到指向的内核缓存中 操作系统内核将数据复制到用户空间缓冲区然后 read 系统调用返回。 在这个过程中数据先从外部设备复制到内核空间再从内核空间复制到用户空间这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换从而降低 I/O 的性能。 ②阻塞 在传统 I/O 中InputStream 的 read() 是一个 while 循环操作它会一直等待数据读取直到数据就绪才会返回。如果没有数据就绪这个读取操作将会一直被挂起用户线程将会处于阻塞状态。 在少量连接请求的情况下使用这种方式没有问题响应速度也很高。但在发生大量连接请求时就需要创建大量监听线程这时如果线程没有数据就绪就会被挂起然后进入阻塞状态。一旦发生线程阻塞这些线程将会不断地抢夺 CPU 资源从而导致大量的 CPU 上下文切换增加系统的性能开销。 (2)优化 I/O 在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 java.io.* 已经以 NIO 为基础重新实现了所以现在它可以利用 NIO 的一些特性。例如 java.io.* 包中的一些类包含以块的形式读写数据的方法这使得即使在更面向流的系统中处理速度也会更快。 可以看到1.4后的IO经过了集成。所以NIO的好处集中在其他特性上而非速度: 分散与聚集读取  文件锁定功能  网络异步IO 2.序列化与反序列化 当前大部分后端服务都是基于微服务架构实现的。服务按照业务划分被拆分实现了服务的解耦但同时也带来了新的问题不同业务之间通信需要通过接口实现调用。两个服务之间要共享一个数据对象就需要从对象转换成二进制流通过网络传输传送到对方服务再转换回对象供服务方法调用。这个编码和解码过程我们称之为序列化与反序列化。 在大量并发请求的情况下如果序列化的速度慢会导致请求响应时间增加而序列化后的传输数据体积大会导致网络吞吐量下降。所以一个优秀的序列化框架可以提高系统的整体性能。 减少序列化(避免RPC)如果无法减少替换java序列化的方法 (1)java序列化的实现原理 Java 提供了一种序列化机制这种机制能够将一个对象序列化为二进制形式字节数组用于写入磁盘或输出到网络同时也能从网络或磁盘中读取字节数组反序列化成对象在程序中使用。JDK 提供的两个输入、输出流对象 ObjectInputStream 和 ObjectOutputStream它们只能对实现了 Serializable 接口的类的对象进行反序列化和序列化。 具体实现序列化的是 writeObject 和 readObject通常这两个方法是默认的当然也可以在实现 Serializable 接口的类中对其进行重写定制一套属于自己的序列化与反序列化机制。 另外Java 序列化的类中还定义了两个重写方法writeReplace() 和 readResolve()前者是用来在序列化之前替换序列化对象的后者是用来在反序列化之后对返回对象进行处理的。 (2)Java 序列化的缺陷 ①无法跨语言 现在的系统设计越来越多元化很多系统都使用了多种语言来编写应用程序。而 Java 序列化目前只适用基于 Java 语言实现的框架其它语言大部分都没有使用 Java 的序列化框架也没有实现 Java 序列化这套协议。因此如果是两个基于不同语言编写的应用程序相互通信则无法实现两个应用服务之间传输对象的序列化与反序列化。 ②易被攻击 对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的这个方法其实是一个神奇的构造器它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着在反序列化字节流的过程中该方法可以执行任意类型的代码这是非常危险的。 对于需要长时间进行反序列化的对象不需要执行任何代码也可以发起一次攻击。攻击者可以创建循环对象链然后将序列化后的对象传输到程序中反序列化这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。 Set root new HashSet(); Set s1 root; Set s2 new HashSet(); for (int i 0; i 100; i) { Set t1 new HashSet(); Set t2 new HashSet(); t1.add(foo); // 使 t2 不等于 t1 s1.add(t1); s1.add(t2); s2.add(t1); s2.add(t2); s1 t1; s2 t2; } 后续解决方案 很多序列化协议都制定了一套数据结构来保存和获取对象。例如JSON 序列化、ProtocolBuf 等它们只支持一些基本类型和数组数据类型这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单但足以满足当前大部分系统的数据传输需求。 可以通过反序列化对象白名单来控制反序列化对象可以重写 resolveClass 方法并在该方法中校验对象名字。 Overrideprotected Class resolveClass(ObjectStreamClass desc) throws IOException,ClassNotFoundException {if (!desc.getName().equals(Bicycle.class.getName())) { throw new InvalidClassException(Unauthorized deserialization attempt, desc.getName());}return super.resolveClass(desc);} ③序列化后的流太大 序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大占用的存储空间就越多存储硬件的成本就越高。如果我们是进行网络传输则占用的带宽就更多这时就会影响到系统的吞吐量。 Java 序列化实现的二进制编码完成的二进制数组大小比 ByteBuffer 实现的二进制编码完成的二进制数组大小要大上几倍。因此Java 序列后的流会变大最终会影响到系统的吞吐量。 User user new User();user.setUserName(test);user.setPassword(test); ByteArrayOutputStream os new ByteArrayOutputStream();ObjectOutputStream out new ObjectOutputStream(os);out.writeObject(user); byte[] testByte os.toByteArray();System.out.print(ObjectOutputStream 字节编码长度 testByte.length \n); ByteBuffer byteBuffer ByteBuffer.allocate( 2048); byte[] userName user.getUserName().getBytes();byte[] password user.getPassword().getBytes();byteBuffer.putInt(userName.length);byteBuffer.put(userName);byteBuffer.putInt(password.length);byteBuffer.put(password); byteBuffer.flip();byte[] bytes new byte[byteBuffer.remaining()];System.out.print(ByteBuffer 字节编码长度 bytes.length \n); ObjectOutputStream 字节编码长度99 ByteBuffer 字节编码长度16 ④序列化性能太差 序列化的速度也是体现序列化性能的重要指标如果序列化的速度慢就会影响网络通信的效率从而增加系统的响应时间。Java 序列化中的编码耗时要比 ByteBuffer 长很多 User user new User(); user.setUserName(test); user.setPassword(test); long startTime System.currentTimeMillis(); for(int i0; i1000; i) { ByteArrayOutputStream os new ByteArrayOutputStream(); ObjectOutputStream out new ObjectOutputStream(os); out.writeObject(user); out.flush(); out.close(); byte[] testByte os.toByteArray(); os.close(); }long endTime System.currentTimeMillis(); System.out.print(ObjectOutputStream 序列化时间 (endTime - startTime) \n); long startTime1 System.currentTimeMillis();for(int i0; i1000; i) {ByteBuffer byteBuffer ByteBuffer.allocate( 2048);byte[] userName user.getUserName().getBytes(); byte[] password user.getPassword().getBytes(); byteBuffer.putInt(userName.length); byteBuffer.put(userName); byteBuffer.putInt(password.length); byteBuffer.put(password); byteBuffer.flip(); byte[] bytes new byte[byteBuffer.remaining()];}long endTime1 System.currentTimeMillis();System.out.print(ByteBuffer 序列化时间 (endTime1 - startTime1) \n); ObjectOutputStream 序列化时间29 ByteBuffer 序列化时间6 (3)解决方案 目前业内优秀的序列化框架有很多而且大部分都避免了 Java 默认序列化的一些缺陷。例如最近几年比较流行的 FastJson、Kryo、Protobuf、Hessian 等。我们完全可以找一种替换掉 Java 序列化推荐使用 Protobuf 序列化框架。 3.减少编码 Java 的编码运行比较慢这是 Java 的一大硬伤。在很多场景下只要涉及字符串的操作 如输入输出操作、I/O 操作都比较耗 CPU 资源不管它是磁盘 I/O 还是网络 I/O因 为都需要将字符转换成字节而这个转换必须编码。 每个字符的编码都需要查表而这种查表的操作非常耗资源所以减少字符到字节或者相反 的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。 那么如何才能减少编码呢例如网页输出是可以直接进行流输出的即用 resp.getOutputStream() 函数写数据把一些静态的数据提前转化成字节等到真正往外 写的时候再直接用 OutputStream() 函数写就可以减少静态数据的编码转换。 三、多线程性能调优——降低锁竞争 互斥锁能够满足各类功能性要求特别是被锁住的代码执行时间不可控时它通过内核执行线程切换及时释放了资源但它的性能消耗最大。 如果能够确定被锁住的代码取到锁后很快就能释放应该使用更高效的自旋锁它特别适合基于异步编程实现的高并发服务。 如果能区分出读写操作读写锁就是第一选择它允许多个读线程同时持有读锁提高了并发性。读写锁是有倾向性的读优先锁很高效但容易让写线程饿死而写优先锁会优先服务写线程但对读线程亲和性差一些。还有一种公平读写锁它通过把等待锁的线程排队以略微牺牲性能的方式保证了某种线程不会饿死通用性更佳。另外读写锁既可以使用互斥锁实现也可以使用自旋锁实现我们应根据场景来选择合适的实现。 当并发访问共享资源冲突概率非常低的时候可以选择无锁编程。然而一旦冲突概率上升就不适合使用它因为它解决冲突的重试成本非常高。 1.深入了解Synchronized同步锁的优化方法 (1)自旋锁的关闭 在锁竞争不激烈且锁占用时间非常短的场景下自旋锁可以提高系统性能。一旦锁竞争激烈或锁占用的时间过长自旋锁将会导致大量的线程一直处于 CAS 重试状态占用 CPU 资源反而会增加系统性能开销。所以自旋锁和重量级锁的使用都要结合实际场景。在高负载、高并发的场景下我们可以通过设置 JVM 参数来关闭自旋锁优化系统性能示例代码如下 -XX:-UseSpinning //参数关闭自旋锁优化(默认打开) -XX:PreBlockSpin //参数修改默认的自旋次数。JDK1.7后去掉此参数由jvm控制 (2)减小锁粒度 当锁对象是一个数组或队列时集中竞争一个对象的话会非常激烈锁也会升级为重量级锁。我们可以考虑将一个数组和队列对象拆成多个小对象来降低锁竞争提升并行度。 最经典的减小锁粒度的案例就是 JDK1.8 之前实现的 ConcurrentHashMap 版本。我们知道HashTable 是基于一个数组 链表实现的所以在并发读写操作集合时存在激烈的锁资源竞争也因此性能会存在瓶颈。而 ConcurrentHashMap 就很很巧妙地使用了分段锁 Segment 来降低锁资源竞争。 ①锁分离 与传统锁不同的是读写锁实现了锁分离也就是说读写锁是由“读锁”和“写锁”两个锁实现的其规则是可以共享读但只有一个写。 这样做的好处是在多线程读的时候读读是不互斥的读写是互斥的写写是互斥的。而传统的独占锁在没有区分读写锁的时候读写操作一般是读读互斥、读写互斥、写写互斥。所以在读远大于写的多线程场景中锁分离避免了在高并发读情况下的资源竞争从而避免了上下文切换。 ②锁分段 在使用锁来保证集合或者大对象原子性时可以考虑将锁对象进一步分解。例如 ConcurrentHashMap 就使用了锁分段。 (3)减少锁的持有时间 锁的持有时间越长就意味着有越多的线程在等待该竞争资源释放。如果是 Synchronized 同步锁资源就不仅是带来线程间的上下文切换还有可能会增加进程间的上下文切换。 优化前 public synchronized void mySyncMethod(){ businesscode1(); mutextMethod(); businesscode2(); } 优化后 public void mySyncMethod(){ businesscode1(); synchronized(this) { mutextMethod(); } businesscode2(); } (4)使用读写分离锁代替独占锁 使用ReadWriteLock读写分离锁可以提高系统性能, 使用读写分离 锁也是减小锁粒度的一种特殊情况. 第二条建议是能分割数据结构 实现减小锁的粒度,那么读写锁是对系统功能点的分割. 在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在 读多写少的情况下,使用读写锁可以大大提高系统的并发能力 (5)粗锁化 为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽 量短.但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和 释放,也会消耗系统资源.如: public void method1(){ synchronized( lock ){ 同步代码块 1 } synchronized( lock ){ 同步代码块 2 }} JVM 在遇到一连串不断对同一个锁进行请求和释放操作时,会把所 有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫 锁的粗化,如上一段代码会整合为: public void method1(){ synchronized( lock ){ 同步代码块 1 同步代码块 2 }} 在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其 在循环体内请求锁时,如: for(int i 0 ; i 100; i){ synchronized(lock){}} 这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更 合理的做法就是在循环外请求一次锁,如: synchronized( lock ){ for(int i 0 ; i 100; i){}} 2.使用乐观锁 在读大于写的场景下读写锁 ReentrantReadWriteLock、StampedLock 以及乐观锁的读写性能是最好的在写大于读的场景下乐观锁的性能是最好的其它 4 种锁的性能则相差不多在读和写差不多的场景下两种读写锁以及乐观锁的性能要优于 Synchronized 和 ReentrantLock。 乐观锁虽然去除了锁操作但是一旦发生冲突重试的成本非常高。所以只有在冲突概率 非常低且加锁成本较高时才考虑使用乐观锁。 3.减少上下文切换 (1)检测与避免 多线程的上下文切换会导致速度的损失。一般在单个逻辑比较简单而且速度相对来非常快的情况下可以使用单线程。例如Redis从内存中快速读取值不用考虑 I/O 瓶颈带来的阻塞问题。而在逻辑相对来说很复杂的场景等待时间相对较长又或者是需要大量计算的场景建议使用多线程来提高系统的整体性能。例如NIO 时期的文件读写操作、图像处理以及大数据分析等。 在 Linux 系统下可以使用 Linux 内核提供的 vmstat 命令来监视 Java 程序运行过程中系统的上下文切换频率cs 如下图所示 如果是监视某个应用的上下文切换就可以使用 pidstat 命令监控指定进程的 Context Switch 上下文切换。 (2)wait/notify 的使用导致了较多的上下文切换 4.合理地设置线程池大小避免创建过多线程 线程池的线程数量设置不宜过大因为一旦线程池的工作线程总数超过系统所拥有的处理器数量就会导致过多的上下文切换。并且服务器CPU核数有限能够同时并发的线程数有限。 还有一种情况就是在有些创建线程池的方法里线程数量设置不会直接暴露给我们。比如用 Executors.newCachedThreadPool() 创建的线程池该线程池会复用其内部空闲的线程来处理新提交的任务如果没有再创建新的线程不受 MAX_VALUE 限制这样的线程池如果碰到大量且耗时长的任务场景就会创建非常多的工作线程从而导致频繁的上下文切换。因此这类线程池就只适合处理大量且耗时短的非阻塞任务。 N核服务器通过执行业务的单线程分析出本地计算时间为x等待时间为y则工作线程数线程池线程数设置为 N*(xy)/x能让CPU的利用率最大化。 一般来说非CPU密集型的业务加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务瓶颈都在后端数据库访问或者RPC调用本地CPU计算的时间很少所以设置几十或者几百个工作线程是能够提升吞吐量的。 根据实际的测试发现减少线程等待时间对提升性能的影响没有想象得那么大它并不是线性的提升关系。 真正对性能有影响的是 CPU 的执行时间。这也很好理解因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试如果减少 CPU 一半的执行时间就可以增加一倍的 QPS。 也就是说应该致力于减少 CPU 的执行时间。 5.使用协程实现非阻塞等待——协程不适合计算密集型的场景。协程适合I/O 阻塞型 (1)如何通过切换请求实现高并发 我们知道主机上资源有限一颗 CPU、一块磁盘、一张网卡如何同时服务上百个请求呢多进程模式是最初的解决方案。内核把 CPU 的执行时间切分成许多时间片timeslice比如 1 秒钟可以切分为 100 个 10 毫秒的时间片每个时间片再分发给不同的进程通常每个进程需要多个时间片才能完成一个请求。这样虽然微观上比如说就这 10 毫秒时间 CPU 只能执行一个进程但宏观上 1 秒钟执行了 100 个时间片于是每个时间片所属进程中的请求也得到了执行这就实现了请求的并发执行。 不过每个进程的内存空间都是独立的这样用多进程实现并发就有两个缺点一是内核的管理成本高二是无法简单地通过内存同步数据很不方便。于是多线程模式就出现了多线程模式通过共享内存地址空间解决了这两个问题。然而共享地址空间虽然可以方便地共享对象但这也导致一个问题那就是任何一个线程出错时进程中的所有线程会跟着一起崩溃。这也是如 Nginx 等强调稳定性的服务坚持使用多进程模式的原因。事实上无论基于多进程还是多线程都难以实现高并发这由两个原因所致。首先单个线程消耗的内存过多比如64 位的 Linux 为每个线程的栈分配了 8MB 的内存还预分配了 64MB 的内存作为堆内存池。所以我们没有足够的内存去开启几万个线程实现并发。其次切换请求是内核通过切换线程实现的什么时候会切换线程呢不只时间片用尽当调用阻塞方法时内核为了让 CPU 充分工作也会切换到其他线程执行。一次上下文切换的成本在几十纳秒到几微秒间当线程繁忙且数量众多时这些切换会消耗绝大部分的 CPU 运算能力。 下图以上一讲介绍过的磁盘 IO 为例描述了多线程中使用阻塞方法读磁盘2 个线程间的切换方式。 那么怎么才能实现高并发呢把上图中本来由内核实现的请求切换工作交由用户态的代码来完成就可以了异步化编程通过应用层代码实现了请求切换降低了切换成本和内存占用空间。异步化依赖于 IO 多路复用机制比如 Linux 的 epoll 或者 Windows 上的 iocp同时必须把阻塞方法更改为非阻塞方法才能避免内核切换带来的巨大消耗。Nginx、Redis 等高性能服务都依赖异步化实现了百万量级的并发。下图描述了异步 IO 的非阻塞读和异步框架结合后是如何切换请求的。 然而写异步化代码很容易出错。因为所有阻塞函数都需要通过非阻塞的系统调用拆分成两个函数。虽然这两个函数共同完成一个功能但调用方式却不同。第一个函数由你显式调用第二个函数则由多路复用机制调用。这种方式违反了软件工程的内聚性原则函数间同步数据也更复杂。特别是条件分支众多、涉及大量系统调用时异步化的改造工作会非常困难。有没有办法既享受到异步化带来的高并发又可以使用阻塞函数写同步化代码呢协程可以做到它在异步化之上包了一层外衣兼顾了开发效率与运行效率。 (2)协程是如何实现高并发的 协程与异步编程相似的地方在于它们必须使用非阻塞的系统调用与内核交互把切换请求的权力牢牢掌握在用户态的代码中。但不同的地方在于协程把异步化中的两段函数封装为一个阻塞的协程函数。这个函数执行时会使调用它的协程无感知地放弃执行权由协程框架切换到其他就绪的协程继续执行。当这个函数的结果满足后协程框架再选择合适的时机切换回它所在的协程继续执行。如下图所示 看起来非常棒然而异步化是通过回调函数来完成请求切换的业务逻辑与并发实现关联在一起很容易出错。协程不需要什么“回调函数”它允许用户调用“阻塞的”协程方法用同步编程方式写业务逻辑。那协程的切换是如何完成的呢 实际上用户态的代码切换协程与内核切换线程的原理是一样的。内核通过管理 CPU 的寄存器来切换线程我们以最重要的栈寄存器和指令寄存器为例看看协程切换时如何切换程序指令与内存。每个线程有独立的栈而栈既保留了变量的值也保留了函数的调用关系、参数和返回值CPU 中的栈寄存器 SP 指向了当前线程的栈而指令寄存器 IP 保存着下一条要执行的指令地址。因此从线程 1 切换到线程 2 时首先要把 SP、IP 寄存器的值为线程 1 保存下来再从内存中找出线程 2 上一次切换前保存好的寄存器值写入 CPU 的寄存器这样就完成了线程切换。其他寄存器也需要管理、替换原理与此相同不再赘述。协程的切换与此相同只是把内核的工作转移到协程框架实现而已下图是协程切换前的状态 从协程 1 切换到协程 2 后的状态如下图所示 创建协程时会从进程的堆中分配一段内存作为协程的栈。线程的栈有 8MB(太大导致单机无法实现高并发的原因)而协程栈的大小通常只有几十 KB。而且C 库内存池也不会为协程预分配内存它感知不到协程的存在。这样更低的内存占用空间为高并发提供了保证毕竟十万并发请求就意味着 10 万个协程。当然栈缩小后就尽量不要使用递归函数也不能在栈中申请过多的内存这是实现高并发必须付出的代价。 由此可见协程就是用户态的线程。然而为了保证所有切换都在用户态进行协程必须重新封装所有的阻塞系统调用否则一旦协程触发了线程切换会导致这个线程进入休眠状态进而其上的所有协程都得不到执行。比如普通的 sleep 函数会让当前线程休眠由内核来唤醒线程而协程化改造后sleep 只会让当前协程休眠由协程框架在指定时间后唤醒协程。再比如线程间的互斥锁是使用信号量实现的而信号量也会导致线程休眠协程化改造互斥锁后同样由框架来协调、同步各协程的执行。 所以协程的高性能建立在切换必须由用户态代码完成之上这要求协程生态是完整的要尽量覆盖常见的组件。比如 MySQL 官方提供的客户端 SDK它使用了阻塞 socket 做网络访问会导致线程休眠必须用非阻塞 socket 把 SDK 改造为协程函数后才能在协程中使用。当然并不是所有的函数都能用协程改造。比如提到的异步 IO它虽然是非阻塞的但无法使用 PageCache降低了系统吞吐量。如果使用缓存 IO 读文件在没有命中 PageCache 时是可能发生阻塞的。这种时候如果对性能有更高的要求就需要把线程与协程结合起来用把可能阻塞的操作放在线程中执行通过生产者 / 消费者模型与协程配合工作。实际上面对多核系统也需要协程与线程配合工作。因为协程的载体是线程而一个线程同一时间只能使用一颗 CPU所以通过开启更多的线程将所有协程分布在这些线程中就能充分使用 CPU 资源。 除此之外为了让协程获得更多的 CPU 时间还可以设置所在线程的优先级比如 Linux 下把线程的优先级设置到 -20就可以每次获得更长的时间片。另外为了减少 CPU 缓存失效的比例还可以把线程绑定到某个 CPU 上增加协程执行时命中 CPU 缓存的机率。虽然这一讲中谈到协程框架在调度协程然而你会发现很多协程库只提供了创建、挂起、恢复执行等基本方法并没有协程框架的存在需要业务代码自行调度协程。这是因为这些通用的协程库并不是专为服务器设计的。服务器中可以由客户端网络连接的建立驱动着创建出协程同时伴随着请求的结束而终止。在协程的运行条件不满足时多路复用框架会将它挂起并根据优先级策略选择另一个协程执行。因此使用协程实现服务器端的高并发服务时并不只是选择协程库还要从其生态中找到结合 IO 多路复用的协程框架这样可以加快开发速度。 6.识别不同场景下最优容器 7.串行变并行 串行的逻辑在没有依赖限制的情况下可以并行执行。后端的逻辑如果需 要执行多项操作那么如果没有依赖或者依赖项满足的情况下可以立即执行而不必一 个一个挨个等待依次完成。Spring 的Async 注解可以比较方便地将普通的 Java 方 法调用变成异步进行的用这种方法可以同时执行数个互不依赖的方法。 8.避免使用多线程 业务中使用多线程有别于Tomcat这种容器中间件是为了提高并发能力或者是异步化业务能力。而这两种都有其他的方案来替代。比如高并发我们可能会进行一些拆分操作比如异步化消息队列会使用消息队列等。尽量不使用多线程除非是必须 四、BitSet和BitMap 1.BitSet简介 (1)什么是BitSet 可以减少大数据量的内存的消耗 BitSet类实现了一个按需增长的位向量。位Set的每一个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作可以使用一个 BitSet修改另一个 BitSet的内容。  默认情况下set 中所有位的初始值都是false。  每个位 set 都有一个当前大小也就是该位 set 当前所用空间的位数。注意这个大小与位 set 的实现有关所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关并且是与实现无关而定义的。  (2)BitSet是如何存储的 比如有一堆数字需要存储source[3,5,6,9] 用int就需要4*4个字节。 java.util.BitSet可以存true/false。 如果用java.util.BitSet则会少很多其原理是 先找出数据中最大值maxvalue9 声明一个BitSet bs,它的size是maxvalue110 遍历数据sourcebs[source[i]]设置成true. 最后的值是 (0为false;1为true) bs [0,0,0,1,0,1,1,0,0,1] 3,   5,6,       9 这样一个本来要int型需要占4字节共32位的数字现在只用了1位 比例32:1  这样就省下了很大空间。 (3)应用场景 ①统计一组大数据中没有出现过的数 将这组数据映射到BitSet然后遍历BitSet对应位为0的数表示没有出现过的数据。 ②对大数据进行排序 将数据映射到BitSet遍历BitSet得到的就是有序数据。 ③在内存对大数据进行压缩存储等等。 一个GB的内存空间可以存储85亿多个数可以有效实现数据的压缩存储节省内存空间开销。 2.方法详解 (1)构造函数 BitSet()  创建一个新的位 set。 BitSet(int nbits)    创建一个位 set它的初始大小足以显式表示索引范围在 0 到 nbits-1 的位。 注意 (1)如果指定了bitset的初始化大小那么会把他规整到一个大于或者等于这个数字的64的整倍数。比如64位bitset的大小是1个long而65位时bitset大小是2个long即128位。做这么一个规定主要是为了内存对齐同时避免考虑到不要处理特殊情况简化程序。 (2)常用方法案例演示 ①大数据中没出现过的数 有1千万个随机数随机数的范围在1到1亿之间。现在要求写出一种算法将1到1亿之间没有在随机数中的数求出来 public class Alibaba{ public static void main(String[] args) { Random randomnew Random(); ListInteger listnew ArrayList(); for(int i0;i10000000;i) { int randomResultrandom.nextInt(100000000); list.add(randomResult); } System.out.println(产生的随机数有); for(int i0;ilist.size();i) { System.out.println(list.get(i)); } BitSet bitSetnew BitSet(100000000); for(int i0;i10000000;i) { bitSet.set(list.get(i)); } System.out.println(0~1亿不在上述随机数中有bitSet.size()); for (int i 0; i 100000000; i) { if(!bitSet.get(i)) { System.out.println(i); } } }} ②大数量计算素数 step1:开辟指定大小的位集空间 step2:将所有位置true; step3:将已知素数的倍数所对应的位置false。 public class ComputeSieve { public static void main(String[] args) { int n 100; BitSet b new BitSet(n 1); int i; for(i 2; i n; i){ b.set(i); } i 2; while(i * i n){ if(b.get(i)){ int k 2 * i; while(k n){ b.clear(k); k i; } } i; } i 0; while(i n){ if(b.get(i)){ System.out.println(i); } i; } } } 五、让linux变得更快的代码 1.让CPU执行得更快的代码 任何代码的执行都依赖 CPU通常使用好 CPU 是操作系统内核的工作。然而当我们编 写计算密集型的程序时CPU 的执行效率就开始变得至关重要。由于 CPU 缓存由更快的 SRAM 构成内存是由 DRAM 构成的而且离 CPU 核心更近如果运算时需要的输入 数据是从 CPU 缓存而不是内存中读取时运算速度就会快很多。 (1)CPU 的多级缓存 CPU 缓存离 CPU 核心更近由于电子信号传输是需要时间的所以离 CPU 核心越近缓存的读写速度就越快。但 CPU 的空间很狭小离 CPU 越近缓存大小受 到的限制也越大。所以综合硬件布局、性能等因素CPU 缓存通常分为大小不等的三级 缓存。 CPU 缓存的材质 SRAM 比内存使用的 DRAM 贵许多所以不同于内存动辄以 GB 计算 它的大小是以 MB 来计算的。比如在 Linux 系统上离 CPU 最近的一级缓存是 32KB二级缓存是 256KB最大的三级缓存则是 20MBWindows 系统查看缓存大小可 以用 wmic cpu 指令或者用CPU-Z这个工具。 CPU 都是多核心 的每个核心都有自己的一、二级缓存但三级缓存却是一颗 CPU 上所有核心共享的。程序执行时会先将内存中的数据载入到共享的三级缓存中再进入每颗核心独有的二级缓 存最后进入最快的一级缓存之后才会被 CPU 使用。 缓存存要比内存快很多。CPU 访问一次内存通常需要 100 个时钟周期以上而访问一级缓存 只需要 4~5 个时钟周期二级缓存大约 12 个时钟周期三级缓存大约 30 个时钟周期 对于 2GHZ 主频的 CPU 来说一个时钟周期是 0.5 纳秒。 如果 CPU 所要操作的数据在缓存中则直接读取这称为缓存命中。命中缓存会带来很大 的性能提升因此代码优化目标是提升 CPU 缓存的命中率。 当然缓存命中率是很笼统的具体优化时还得一分为二。比如你在查看 CPU 缓存时会 发现有 2 个一级缓存比如 Linux 上就是上图中的 index0 和 index1这是因为CPU 会区别对待指令与数据。比如“112”这个运算“”就是指令会放在一级指令缓 存中而“1”这个输入数字则放在一级数据缓存中。虽然在冯诺依曼计算机体系结构 中代码指令与数据是放在一起的但执行时却是分开进入指令缓存与数据缓存的因此要分开来看二者的缓存命中率。 (2)提升数据缓存的命中率 for(i 0; i N; i1) { for(j 0; j N; j1) { array[i][j] 0; } } array[j][i]执行的时间是后者 array[i] [j]的 8 倍之多。因为二维数组 array 所占用的内存是连续的比如若长度 N 的值为 2那么内存中从前至后各元素的顺序是 array[0][0]array[0][1]array[1][0]array[1][1]。 如果用 array[i][j]访问数组元素则完全与上述内存中元素顺序一致因此访问 array[0][0] 时缓存已经把紧随其后的 3 个元素也载入了CPU 通过快速的缓存来读取后续 3 个元素 就可以。如果用 array[j][i]来访问访问的顺序就是 array[0][0]array[1][0]array[0][1]array[1][1] 此时内存是跳跃访问的如果 N 的数值很大那么操作 array[j][i]时是没有办法把 array[j1][i]也读入缓存的。遇到这种遍历访问数组的情况时按照内存布局顺序访问将会带来很大的性能提升。 (3)提升指令缓存的命中率 比如有一个元素为 0 到 255 之间随机数字组成的数组。接下来要对它做两个操作一是循环遍历数组判断每个数字是否小于 128如果小于则 把元素的值置为 0二是将数组排序。那么先排序再遍历速度快还是先遍历再排序速度 快呢 接下来要对它做两个操作一是循环遍历数组判断每个数字是否小于 128如果小于则 把元素的值置为 0二是将数组排序。那么先排序再遍历速度快还是先遍历再排序速度 快呢 当代码中出现 if、switch 等语句时意味着此时至少可以选择跳转到两段不同的指令去执 行。如果分支预测器可以预测接下来要在哪段代码执行比如 if 还是 else 中的指令就 可以提前把这些指令放在缓存中CPU 执行时就会很快。当数组中的元素完全随机时分 支预测器无法有效工作而当 array 数组有序时分支预测器会动态地根据历史命中数据对 未来进行预测命中率就会非常高。 (4)提升多核 CPU 下的缓存命中率 虽然三级缓存面向所有核心但一、二级缓存是每颗核心独享的。我们知道即使只有一个 CPU 核心现代分时操作系统都支持许多进程同时运行。这是因为操作系统把时间切成了 许多片微观上各进程按时间片交替地占用 CPU这造成宏观上看起来各程序同时在执 行。 因此若进程 A 在时间片 1 里使用 CPU 核心 1自然也填满了核心 1 的一、二级缓存 当时间片 1 结束后操作系统会让进程 A 让出 CPU基于效率并兼顾公平的策略重新调度 CPU 核心 1以防止某些进程饿死。如果此时 CPU 核心 1 繁忙而 CPU 核心 2 空闲则 进程 A 很可能会被调度到 CPU 核心 2 上运行这样即使我们对代码优化得再好也只能在一个时间片内高效地使用 CPU 一、二级缓存了下一个时间片便面临着缓存效率的问 题。 因此操作系统提供了将进程或者线程绑定到某一颗 CPU 上运行的能力。如 Linux 上提供 了 sched_setaffinity 方法实现这一功能其他操作系统也有类似功能的 API 可用。 当多线程同时执行密集计 算且 CPU 缓存命中率很高时如果将每个线程分别绑定在不同的 CPU 核心上性能便 会获得非常可观的提升。Perf 工具也提供了 cpu-migrations 事件它可以显示进程从不 同的 CPU 核心上迁移的次数。 2.内存池 当代码申请内存时首先会到达应用层内存池如果应用层内存池有足够的可用内存就会 直接返回给业务代码否则它会向更底层的 C 库内存池申请内存。比如如果你在 Apache、Nginx 等服务之上做模块开发这些服务中就有独立的内存池。当然Java 中 也有内存池当通过启动参数 Xmx 指定 JVM 的堆内存为 8GB 时就设定了 JVM 堆内存 池的大小。 你可能听说过 Google 的 TCMalloc 和 FaceBook 的 JEMalloc它们也是 C 库内存池。 当 C 库内存池无法满足内存申请时才会向操作系统内核申请分配内存。 Java 已经有了应用层内存池为什么还会受到 C 库内存池的影响 呢这是因为除了 JVM 负责管理的堆内存外Java 还拥有一些堆外内存由于它不使用 JVM 的垃圾回收机制所以更稳定、持久处理 IO 的速度也更快。这些堆外内存就会由 C 库内存池负责分配这是 Java 受到 C 库内存池影响的原因。 C 库内存池影响着系 统下依赖它的所有进程。我们就以 Linux 系统的默认 C 库内存池 Ptmalloc2 来具体分析 看看它到底对性能发挥着怎样的作用。 C 库内存池工作时会预分配比你申请的字节数更大的空间作为内存池。比如说当主进程 下申请 1 字节的内存时Ptmalloc2 会预分配 132K 字节的内存Ptmalloc2 中叫 Main Arena应用代码再申请内存时会从这已经申请到的 132KB 中继续分配。 当我们释放这 1 字节时Ptmalloc2 也不会把内存归还给操作系统。Ptmalloc2 认为与 其把这 1 字节释放给操作系统不如先缓存着放进内存池里仍然当作用户态内存留下 来进程再次申请 1 字节的内存时就可以直接复用这样速度快了很多。 你可能会想132KB 不多呀为什么这一讲开头提到的 Java 进程会被分配了几个 GB 的 内存池呢这是因为多线程与单线程的预分配策略并不相同。 当释放这 1 字节时Ptmalloc2 也不会把内存归还给操作系统。Ptmalloc2 认为与 其把这 1 字节释放给操作系统不如先缓存着放进内存池里仍然当作用户态内存留下 来进程再次申请 1 字节的内存时就可以直接复用这样速度快了很多。 你可能会想132KB 不多呀为什么这一讲开头提到的 Java 进程会被分配了几个 GB 的 内存池呢这是因为多线程与单线程的预分配策略并不相同。 Linux 下的 JVM 编译时默认使用了 Ptmalloc2 内存池因此每个线 程都预分配了 64MB 的内存这造成含有上百个 Java 线程的 JVM 多使用了 6GB 的内 存。在多数情况下这些预分配出来的内存池可以提升后续内存分配的性能。 然而Java 中的 JVM 内存池已经管理了绝大部分内存确实不能接受莫名多出来 6GB 的 内存那该怎么办呢有两种解决办法。 首先可以调整 Ptmalloc2 的工作方式。通过设置 MALLOC_ARENA_MAX 环境变量可 以限制线程内存池的最大数量当然线程内存池的数量减少后会影响 Ptmalloc2 分配 内存的速度。不过由于 Java 主要使用 JVM 内存池来管理对象这点影响并不重要。 其次可以更换掉 Ptmalloc2 内存池选择一个预分配内存更少的内存池比如 Google 的 TCMalloc。 六、JMH 1.JMH 简介 JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件主要是基于方法层面的基准测试精度可以达到纳秒级。当你定位到热点方法希望进一步优化方法性能的时候就可以使用 JMH 对优化的结果进行量化的分析。JMH 比较典型的应用场景如下 想准确地知道某个方法需要执行多长时间以及执行时间和输入之间的相关性 对比接口不同实现在给定条件下的吞吐量 查看多少百分比的请求在多长时间内完成 2.案例演示 下面以字符串拼接的两种方法为例子使用 JMH 做基准测试。 (1)引入依赖 加入依赖因为 JMH 是 JDK9 自带的如果是 JDK9 之前的版本需要加入如下依赖目前 JMH 的最新版本为  1.23 dependency groupIdorg.openjdk.jmh/groupId artifactIdjmh-core/artifactId version1.23/version /dependency dependency groupIdorg.openjdk.jmh/groupId artifactIdjmh-generator-annprocess/artifactId version1.23/version/dependency (2)测试类 编写基准测试接下来创建一个 JMH 测试类用来判断  和 StringBuilder.append() 两种字符串拼接哪个耗时更短具体代码如下所示 BenchmarkMode(Mode.AverageTime)Warmup(iterations 3, time 1)Measurement(iterations 5, time 5)Threads(4)Fork(1)State(value Scope.Benchmark)OutputTimeUnit(TimeUnit.NANOSECONDS)public class StringConnectTest {Param(value {10, 50, 100}) private int length;Benchmark public void testStringAdd(Blackhole blackhole) { String a ; for (int i 0; i length; i) { a i; } blackhole.consume(a); }Benchmark public void testStringBuilderAdd(Blackhole blackhole) { StringBuilder sb new StringBuilder(); for (int i 0; i length; i) { sb.append(i); } blackhole.consume(sb.toString()); }public static void main(String[] args) throws RunnerException { Options opt new OptionsBuilder() .include(StringConnectTest.class.getSimpleName()) .result(result.json) .resultFormat(ResultFormatType.JSON).build(); new Runner(opt).run(); }} 在 main() 函数中首先对测试用例进行配置使用 Builder 模式配置测试将配置参数存入 Options 对象并使用 Options 对象构造 Runner 启动测试。另外大家可以看下官方提供的 jmh 示例 demo http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ (3)执行基准测试 准备工作做好了接下来运行代码等待片刻测试结果就出来了下面对结果做下简单说明 # JMH version: 1.23# VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09# VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe# VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encodingUTF-8# Warmup: 3 iterations, 1 s each# Measurement: 5 iterations, 5 s each# Timeout: 10 min per iteration# Threads: 4 threads, will synchronize iterations# Benchmark mode: Average time, time/op# Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd# Parameters: (length 100) 该部分为测试的基本信息比如使用的 Java 路径预热代码的迭代次数测量代码的迭代次数使用的线程数量测试的统计单位等。 # Warmup Iteration 1: 1083.569 ±(99.9%) 393.884 ns/op# Warmup Iteration 2: 864.685 ±(99.9%) 174.120 ns/op# Warmup Iteration 3: 798.310 ±(99.9%) 121.161 ns/op 该部分为每一次热身中的性能指标预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化比如在预热后被测代码应该得到了充分的 JIT 编译和优化。 Iteration 1: 810.667 ±(99.9%) 51.505 ns/opIteration 2: 807.861 ±(99.9%) 13.163 ns/opIteration 3: 851.421 ±(99.9%) 33.564 ns/opIteration 4: 805.675 ±(99.9%) 33.038 ns/opIteration 5: 821.020 ±(99.9%) 66.943 ns/op Result com.wupx.jmh.StringConnectTest.testStringBuilderAdd: 819.329 ±(99.9%) 72.698 ns/op [Average] (min, avg, max) (805.675, 819.329, 851.421), stdev 18.879 CI (99.9%): [746.631, 892.027] (assumes normal distribution) Benchmark (length) Mode Cnt Score Error UnitsStringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op 该部分显示测量迭代的情况每一次迭代都显示了当前的执行速率即一个操作所花费的时间。在进行 5 次迭代后进行统计在本例中length 为 100 的情况下 testStringBuilderAdd 方法的平均执行花费时间为  819.329 ns误差为 72.698 ns。 最后的测试结果如下所示 Benchmark (length) Mode Cnt Score Error UnitsStringConnectTest.testStringAdd 10 avgt 5 161.496 ± 17.097 ns/opStringConnectTest.testStringAdd 50 avgt 5 1854.657 ± 227.902 ns/opStringConnectTest.testStringAdd 100 avgt 5 6490.062 ± 327.626 ns/opStringConnectTest.testStringBuilderAdd 10 avgt 5 68.769 ± 4.460 ns/opStringConnectTest.testStringBuilderAdd 50 avgt 5 413.021 ± 30.950 ns/opStringConnectTest.testStringBuilderAdd 100 avgt 5 819.329 ± 72.698 ns/op 结果表明在拼接字符次数越多的情况下 StringBuilder.append() 的性能就更好。 (4)生成 jar 包执行 对于一些小测试直接用上面的方式写一个 main 函数手动执行就好了。对于大型的测试需要测试的时间比较久、线程数比较多加上测试的服务器需要一般要放在 Linux 服务器里去执行。JMH 官方提供了生成 jar 包的方式来执行我们需要在 maven 里增加一个 plugin具体配置如下 plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version2.4.1/version executions execution phasepackage/phase goals goalshade/goal /goals configuration finalNamejmh-demo/finalName transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClassorg.openjdk.jmh.Main/mainClass /transformer /transformers /configuration /execution /executions /plugin/plugins 接着执行 maven 的命令生成可执行 jar 包并执行 mvn clean installjava -jar target/jmh-demo.jar StringConnectTest 3.JMH 基础 BenchmarkMode用来配置 Mode 选项可用于类或者方法上这个注解的 value 是一个数组可以把几种 Mode 集合在一起执行如BenchmarkMode({Mode.SampleTime, Mode.AverageTime})还可以设置为  Mode.All即全部执行一遍。 Throughput整体吞吐量每秒执行了多少次调用单位为 ops/time AverageTime用的平均时间每次操作的平均时间单位为 time/op SampleTime随机取样最后输出取样结果的分布 SingleShotTime只运行一次往往同时把 Warmup 次数设为 0用于测试冷启动时的性能 All上面的所有模式都执行一次 State 通过 State 可以指定一个对象的作用范围JMH 根据 scope 来进行实例化和共享操作。State 可以被继承使用如果父类定义了该注解子类则无需定义。由于 JMH 允许多线程同时执行测试不同的选项含义如下 Scope.Benchmark所有测试线程共享一个实例测试有状态实例在多线程共享下的性能 Scope.Group同一个线程在同一个 group 里共享实例 Scope.Thread默认的 State每个测试线程分配一个实例 OutputTimeUnit 为统计结果的时间单位可用于类或者方法注解 Warmup 预热所需要配置的一些基本测试参数可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢所以要让程序进行几轮预热保证测试的准确性。参数如下所示 iterations预热的次数 time每次预热的时间 timeUnit时间的单位默认秒 batchSize批处理大小每次操作调用几次方法 为什么需要预热因为 JVM 的 JIT 机制的存在如果某个函数被调用多次之后JVM 会尝试将其编译为机器码从而提高执行速度所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。 Measurement实际调用方法所需要配置的一些基本测试参数可用于类或者方法上参数和  Warmup 相同。 Threads 每个进程中的测试线程可用于类或者方法上。 Fork 进行 fork 的次数可用于类或者方法上。如果 fork 数是 2 的话则 JMH 会 fork 出两个进程来进行测试。 Param 指定某项参数的多种情况特别适合用来测试一个函数在不同的参数输入的情况下的性能只能作用在字段上使用该注解必须定义 State 注解。 在介绍完常用的注解后让我们来看下 JMH 有哪些陷阱。 4.JMH 陷阱 在使用 JMH 的过程中一定要避免一些陷阱。 比如 JIT 优化中的死码消除比如以下代码 Benchmarkpublic void testStringAdd(Blackhole blackhole) { String a ; for (int i 0; i length; i) { a i; }} JVM 可能会认为变量 a 从来没有使用过从而进行优化把整个方法内部代码移除掉这就会影响测试结果。JMH 提供了两种方式避免这种问题一种是将这个变量作为方法返回值 return a一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除。其他陷阱还有常量折叠与常量传播、永远不要在测试中写循环、使用 Fork 隔离多个测试方法、方法内联、伪共享与缓存行、分支预测、多线程测试等 5.插件 JMH 插件大家还可以通过 IDEA 安装 JMH 插件使 JMH 更容易实现基准测试在 IDEA 中点击  File-Settings...-Plugins然后搜索 jmh选择安装 JMH plugin 这个插件可以让我们能够以 JUnit 相同的方式使用 JMH主要功能如下 自动生成带有 Benchmark 的方法 像 JUnit 一样运行单独的 Benchmark 方法 运行类中所有的 Benchmark 方法比如可以通过右键点击  Generate...选择操作  Generate JMH benchmark 就可以生成一个带有  Benchmark 的方法。 还有将光标移动到方法声明并调用 Run 操作就运行一个单独的 Benchmark 方法。将光标移到类名所在行右键点击 Run 运行该类下的所有被  Benchmark 注解的方法都会被执行。 6.JMH 可视化 除此以外如果你想将测试结果以图表的形式可视化可以试下这些网站 JMH Visual Charthttp://deepoove.com/jmh-visual-chart/ JMH Visualizerhttps://jmh.morethan.io/ 比如将上面测试例子结果的 json 文件导入就可以实现可视化
http://www.zqtcl.cn/news/511975/

相关文章:

  • 河南郑州网站建设哪家公司好html5 网站正在建设中
  • 免费ppt模板下载医学类江门seo网站推广
  • 智慧软文网站群辉wordpress地址
  • 自己怎么做拼单网站外贸网站 源码
  • 做网站如何防止被黑网页无法访问如何解决360浏览器
  • 专门做设计的网站互联网运营培训班哪个好
  • 烟台网站建设网站推广做网站与数据库的关系
  • 深圳网站设计成功刻成全视频免费观看在线看第7季高清
  • 淮阳城乡建设局网站seo技术团队
  • 建设博客网站游戏交易类网站seo怎么做
  • 做系统软件的网站wordpress网站会员太多
  • 上海门户网站怎么登录网站开发竞价单页
  • 东莞市外贸网站建设公司软件开发 系统开发 网站开发服务
  • 泉州制作网站设计南宁网站排名外包
  • 南通网站建设入门wordpress google seo
  • 怎么建立图片的网站吗网站响应式是什么意思
  • 网站建设买了服务器后怎么做WordPress多城市
  • 网站建设凭证成都网站设计公司
  • 创新创业营销策略网站建设等做钢材的都用什么网站
  • 英文免费网站模板大庆+网站建设
  • 品牌网站建设内容框架网站首页收录没了
  • 湖南城乡住房建设厅网站网站图片切换效果
  • 凡科做的网站可以在百度搜到吗阿里云nas做网站
  • 做企业销售分析的网站更改wordpress传文件尺寸
  • 网站建设策划书封面知名企业名称
  • 中小企业网站建设与管理课件百度云济南高端网站建设公司
  • 台州企业建站程序网页设计素材网站知乎
  • wordpress视频付费谷歌seo专员是指什么意思
  • 域名续费做网站wordpress模板淘宝客模板
  • 加强政协机关网站建设深圳教育软件app开发