Java 性能调优指南 – 高性能Java

这篇指南主要调优 java 代码而不是 JVM 设置。

一、JDK 类

Java 1.7.0_06 String 内部表示的改变

  • 从 Java 1.7.0_06 开始, String.substring 总是为它创建的新字符串创建一个新的底层 char[] 值。这意味着这个方法现在有线性的复杂度,之前是常量的复杂度。这个改变的好处是字符串需要更少的内存 footprint(比以前少8字节),也是避免 String.substring 导致的内存泄露的一个保证。

Java 里二进制序列化的不同方法

  • 写单个字节到直接字节缓存是非常慢的。对写记录 — 大多数情况是单个字节的字段,你应该避免使用直接字节缓存。
  • 如果你有原始数组字段,总是使用bulk方法来处理它们(一次处理一批的)。ByteBuffer 的bulk 方法的性能接近于Unsafe的这些方法。如果你需要存储/加载任何其它原始数组 – 除字节型的,用 ByteBuffer.to[YouType]Buffer.put(array) 方法调用,字节缓存的位置会自动更新。不要在循环里调用 ByteBuffer.put[YouType] 方法。
  • 总是尝试用带本地字节顺序的直接缓存序列化原始数组。直接字节缓存的性能接近于Unsafe且是可移植的。

Java 集合概览

单线程 并发
Lists ArrayList : 一般基于数组
LinkedList :不要使用
Vector:废弃
CopyOnWriteArrayList:很少更新,经常遍历。
Queues/deques ArrayDeque:一般基于数组。
Stack:废弃。
PriorityQueue:有序的检索操作。
ArrayBlockQueue:有界阻塞queue。
ConcurrentLinkedDeque/ConcurrentLinkedQueue:无界链接queue(CAS)。
DelayQueue:在每个元素上带延迟的queue。
LinkedBlockingDeque/LinkedBlockingQueue:可选的有界链接queue(锁)。
LinkedTransferQueue:may transfer elements w/o storing。
PriorityBlockingQueue:并发的PriorityQueue。
SynchronousQueue:实现了Queue接口的Exchanger。
Maps HashMap:一般的map。
EnumMap:enum作为key的。Hashtable:废弃。
IdentityHashMap:用 == 比较键。
LinkedHashMap:保留了插入顺序。
TreeMap:有序的键。
WeakHashMap:可用于缓存。
ConcurrentHashMap:一般的并发map。
ConcurrentSkipListMap:有序的并发map。
Sets HashSet:一般的set。
EnumSet:enum的集合。
BitSet:比特位或稀疏整数的集合。
LinkedHashSet:保留了插入顺序。
TreeSet:有序集合。
ConcurrentSkipListSet:有序并发集合。
CopyOnWriteArraySet:很少更新,经常遍历。

ArrayList 性能指南

使用ArrayList时遵循下面的规则:

  • 添加元素到列表的末尾。
  • 从末尾删除元素。
  • 避免 contains, indexOf, remove(object) 方法。
  • 甚至避免 removeAll, retainAll 方法。
  • 使用 subList(int, int).clear() 来快速清除列表的部分。

LinkedList 性能

如果你需要写一个快速的 LinkedList 代码,尝试坚持这些规则:
* 考虑用 ArrayDeque 用于基于 queue的算法。
* 与 LinkedList 一起使用 ListIterator。
* 避免任何接受或返回元素在列表里的下标的 LinkedList 方法。(基于链表的列表不适合随机检索)
* 如果你有理由使用 LinkedList.remove/removeFirst/removeLast 则检查,用 pollFirst/pollLast 替代。
* 尝试批处理 LinkedList。

Bit sets

  • 当你需要映射大量整数到boolean标记时不要忘记 bit sets。
  • 整数值的集合应该用 bit sets 替代,在大多数情况下可以节省很多内存。

IdentityHashMap

  • IdentityHashMap 使用 System.identityHashCode 方法来获取对象的标识哈希码。如果你的对象有主键字段则避免使用 IdentityHashMap
  • 不要尝试去迭代 IdentityHashMap 里的内容,因为迭代顺序在你的程序的每次运行都是不同的,因而导致你的程序结果不一致。
  • 访问对象的标识哈希码是非常便宜的 Java 内部操作。
  • 注意,已计算了标识哈希码的对象不能用于 偏向锁

String 里正则有关的方法

  • 总是使用 MatcherPattern 替换 String.matches, split, replaceAll, replaceFirst 方法。
  • 在Java 7里,按单个非正则特殊字符的字符串分割字符串已在 String.split 方法里优化了。
  • 在所有其他简单情况下,在时间关键的代码里对简单的情况考虑亲手写解析方法。通过手工制作的方法来替换 Pattern 方法,你可以很容易获得10倍提速。

java.util.Date, java.util.Calendar and java.text.SimpleDateFormat 性能

  • 不要使用 java.util.Date,除非必须。用一个普通的 long 来替代。
  • java.util.Calendar 对于日期计算和国际化很有用,但避免大量存储这些对象或广泛地创建它们,它们消耗大量内存并且创建的开销很大。
  • java.text.SimpleDateFormat 对于一般情况下的日期时间解析很有用,但最好避免用它,如果你需要以同样的格式解析大量的date。手工实现一个解析器更好。

Joda 时间库的性能

Java 8 里的日期时间库就是基于 Joda 时间库的。

  • 所有 Joda 时间 date/time 对象都是构建在 long 时间戳之上,所以从 long 构建这些对象很便宜。
  • Joda 不在内部保存人类时间 – year/month/day/hour/min/second。这意味着在Joda对象上获取人类时间更昂贵,如果你需要获取多个字段。
  • Joda里 date/time 的解析比 JDK SimpleDateFormat 略微快。Joda 解析的优势是构建一个解析器 — DateTimeFormatter — 非常便宜,不像昂贵的 SimpleDateFormat,所以你不需要缓存解析器。

JSR 310 – Java 8 Date/Time library performance (as well as Joda Time 2.3 and j.u.Calendar)

  • Java 8 里的 date/time 是构建在人类时间之上的 — year/month/day/hour/minute/second/nanos

java.io.ByteArrayOutputStream

不要在性能关键代码里使用 ByteArrayOutputStream 。

ByteArrayOutputStream 主要用于写未知长度的消息到输出流的情况。它是基于字节数组实现的,当底层数组不够时就新建一个更大的,把原有数据拷贝到新数组,再用新数组替代原有数组;所有方法都是同步的。

  • 在性能关键代码,尝试用 ByteBuffer 替代 ByteArrayOutputStream,如果一定要用,去除它的同步。
  • 如果你的方法写一些消息到未知的 OutputStream,总是先写消息到 ByteArrayOutputStream ,然后使用它的 writeTo(OutputStream) 方法。在一些稀有的情况下从字节表示里构建字符串,可以使用 ByteArrayOutputStream.toString 方法。
  • 通常情况下避免 ByteArrayOutputStream.toByteArray 方法,它创建一个内部字节数组的拷贝。如果你的应用程序使用了GB内存,GC这些拷贝的时间是显著的。
  • 如果你预先知道你的消息大小(或至少知道它的上限),用 ByteBuffer 替代(或重用之前分配的),再写消息到它。
  • 如果是出于缓存的考虑,可以使用 BufferedOutputStream 来替代。
  • ByteArrayOutputStream.writeTo(OutputStream) 方法使用内部数组而不是创建拷贝。
  • ByteArrayOutputStream.toString(String charsetName)ByteArrayOutputStream.toString(String charsetName) 允许直接从内部数组创建一个字符串。

BufferedInputStream 和 GZIPInputStream

  • BufferedInputStream 和 GZIPInputStream 都有内部的缓存。默认大小:前者是 8192 字节,后者是 512 字节。通常情况下它们可以增长到最多 65536。
  • 不要用 BufferedInputStream 作为 GZIPInputStream 的输入,而是在构造器里显式设置 GZIPInputStream 的缓存大小。虽然,保留一个 BufferedInputStream 仍然是安全的。
  • 如果你有一个新的 BufferedInputStream( new FileInputStream( file ) ) 对象,你可以更频繁地调用它的可用方法,考虑重写 BufferedInputStream 的可访问方法,将极大提升文件读取速度。

Byte、Short、Integer、Long、Character(装箱和拆箱)

  • 绝不使用 Number 子类的 valueOf(String) 方法。如果需要一个原始类型的值,用 parse[Type] 。如果需要一个包装类的实例,仍然调用 parse[Type] 并依赖于 JVM 实现的装箱。它将支持缓存最常使用的值。绝不要使用包装类的构造器,它们总是返回一个新对象,因为绕过了缓存。下面是支持缓存的不同类型的范围:
Byte, Short, Long Character Integer Float, Double
-128 至 127 0 至 127 -128 至 Integer.IntegerCache.high 或 127 不缓存

Map.containsKey/Set.contains

  • 对于 sets, contains + add/remove 调用对应该用单个 add/remove 调用替代,即使有额外逻辑用于保证 contains 调用。
  • 对于 maps, contains + get 调用组合应该总是用 get 后接对 get 结果的非空检测替代。(这两个技巧都是减少了一次查找)
  • 对于 maps 键值对的遍历,应该通过 Map.entrySet 方法的结果进行遍历,而不是通过 Map.keySetMap.get(key) 的组合进行。

java.util.zip.CRC32 and java.util.zip.Adler32 performance

  • 如果你选择使用哪个checksum,首先尝试 Adler32。如果它的质量对你足够,用它而不是 CRC32。任何情况下,用 Checksum 接口来获取 Adler32/CRC32 的逻辑。
  • 尝试更新 checksum 至少要有 500 字节块。更短的块将要求显著的时间开销用于 JNI 调用。

hashCode 方法性能调优

  • 尝试提高 hashCode 方法的结果的分布性。这远比提高此方法的速度更重要。绝不要写一个返回常量的 hashCode 方法。
  • String.hashCode 结果的分布性几乎完美,所以有时你可以用String的哈希码来替代它们。如果你与字符串集合一起工作,尝试用 BitSet 来结束。(意思是 BitSet+哈希码可以完美替代字符串集合,并提升性能。)

在Java里抛出异常是很慢的

  • 绝不用异常作为返回码的替代 或 任何可能发生的事件。抛出异常是很昂贵的 — 对于简单的方法你将经历100倍减速。
  • 避免使用任何 Number 子类的 parse*/valueOf(这些方法会抛出 NumberFormatException),如果你对你的每个数据片段调用它们并且你预期有很多非数字的数据。为更高性能,手动解析这些值。

Java 日志性能陷阱

如何用最新的性能损失写日志消息:

  • 如果你在为日志消息准备数据时需要做昂贵的计算,要么使用 Logger.isLoggable 并在里面做所有的数据准备,要么写一个对象 — 它的 toString 完成所有的计算。
  • 绝不调用 Object.toString 方法来获取一个日志消息参数 — 仅仅传递原始对象。日志框架会调用你的对象的 toString 方法。(注:这是一个延迟计算的问题,日志框架只会在需要的时候调用,也就是在真正需要时才计算;而代码直接调用则会在调用日志框架的方法时完成计算。)
  • 不要混淆日志参数的字符串串联 — 恶意串联的字符串将允许你的应用程序用户突破你的 logging/access 数据,那是假定不用于用户访问的。

Base64 编码和解码性能

  • 如果你在找一个快速可靠的 Base64 编解码器 — 非JDK以外。在Java8里有一个新的编解码器: java.util.Base64,也有一个隐藏的: javax.xml.bind.DatatypeConverter。都是快速、可靠且不会遭受正数溢出。
  • 有些第三方开源的Base64编解码器在能处理的数据的大小上是有限制的。
  • 不要在巨大的字符串上调用 String.getBytes(Charset),如果你的字符集是多字节的 — 你可能得到正数溢出有关的异常。

多线程环境下的 java.util.Random 和 java.util.concurrent.ThreadLocalRandom

  • 在任何情境下都不要在多个线程间使用 java.util.Random ,把它包装在 ThreadLocal 里。
  • 从 Java 7 开始,在所有情境下首选 java.util.concurrent.ThreadLocalRandom 而不是 java.util.Random — 它是向后兼容已有代码的,但内部使用更少的操作。

Java 7/8 里的字符串编码和解码

  • 总是倾向于国家字符集如 windows-1252 或 Shift_JIS 而不是 UTF-8:它们生成更紧凑的二进制表示(作为一个规则)并且它们编码/解码更快。
  • 在Java 7 和 8 里 ISO-8869-1 总是比 US-ASCII工作得更快。
  • 对于 US-ASCII/ISO-8859-1,你可以写一个快速的 String->byte[] 转换,但你不能打败 Java 解码器,它们直接访问它们创建的字符串输出。

二、内存优化

Java 里内存节约技术概要

这篇文章将给你java里内存优化的基本建议。大多数Java内存优化技术都是基于这些建议的。

  • 首选基本类型而不是它们的包装对象。
  • 尝试减少你的对象数量。例如,首选基于数组的结构(如 ArrayList/ArrayDeque)而不是基于指针的结构(如 LinkedList)。

流行Java数据类型的内存消耗

EnumSet, BitSet 1 bit per value
EnumMap 4 bytes (for value, nothing for key)
ArrayList 4 bytes (but may be more if ArrayList capacity is seriously more than its size)
LinkedList 24 bytes (fixed)
ArrayDeque 4 to 8 bytes, 6 bytes on average

Java 里的内存节约技术

这篇文章描述了静态内部类、字符串池、布尔标记集合和JDK里一些特殊类的微小集合的好处。

  • 默认让所有内部类是静态的。只有必须时才移除 static 修饰符。
  • 如果你有通常是小集合的集合,尝试用 java.util.Collections.empty*/singleton* 方法达到内存高效的存储微小集合。
  • 首选 BitSet 而不是 boolean 的数组或列表、或整数类型的稀疏集合:位集合是 内存和CPU友好的。

Java 6,7,8 里的 String.intern – 字符串池

这篇文章描述了 Java 6 里 String.intern() 是如何实现的, Java 7 和 8 里做了什么修改。

  • 在Java 6里远离 String.intern() 方法,由于用于JVM字符串池存储的固定大小内存区域(PermGen)。
  • Java 7 和 8 在堆内存里实现了字符串池。这意味着在 Java 7 和 8 里对于字符串池你受限于整个应用内存。
  • 在 Java 7 和 8 用 JVM 参数 -XX:StringTableSize 来设置字符串吃的map大小。它是固定的,因为它被实现为带链表位桶的 hash map。估算你的应用里不同字符串是数量,设置池的大小等于接近于池大小的 prime number(素数)。这将让 String.intern 运行在常量时间,并对每个interned的字符串要求更小的内存开销。(对于同样的任务,显式使用 Java WeakHashMap 将多消耗 4-5 倍的内存)
  • -XX:StringTableSize 的默认值在 Java 7 里是 1009,Java 8 里是 25-50K

Java 6,7,8 里的 String.intern – 多线程访问

这篇文章描述了多线程调用 String.intern() 的性能影响。

  • 随意地使用 String.intern()
  • JVM 字符串池不是线程本地的。

Java 6,7,8 里的 String.intern – 最佳实践

  • 虽然 Java 7+ 开始 String.intern() 实现有了重大优化,它仍然需要显著的时间来运行(对于CPU敏感的应用程序是显著的)。你不应该用 String.intern() 作为安全网络,把一切长期存活字符串丢给它。相反,应当只处理有限数量的不重复值。

Tagged:

Comments are closed.