记一个 IllegalArgumentException
记一个诡异的 IllegalArgumentException: Comparison method violates its general contract!
问题说明
完整的异常日志如下:
1 | 09-25 14:17:14.253 7992-8182/com.tencent.ibg.joox E/AndroidRuntime: FATAL EXCEPTION: BitmapProfilerRefQueue |
BitmapTraceCollector.sortBySize() 的源码如下:
1 | fun sortBySize(): List<BitmapTrace> = ArrayList<BitmapTrace>(traces) |
而 BitmapTrace 类定义如下:
1 | class BitmapTrace constructor( |
写个测试来看一下:
1 | class ContractTest { |
这个测试在一个死循环中跑 runTest(),不过 10s 就出现 Comparison method violates its general contract。

问题原因
其实 Java 官方文档对这种行为有描述,见
- compareto - Comparison method violates its general contract! Java 7 only - Stack Overflow
- Java SE 7 and JDK 7 Compatibility
描述如下:
Area: API: Utilities
Synopsis: Updated sort behavior for Arrays and Collections may throw an IllegalArgumentException
Description: The sorting algorithm used by java.util.Arrays.sort and (indirectly) by java.util.Collections.sort has been replaced. The new sort implementation may throw an IllegalArgumentException if it detects a Comparable that violates the Comparable contract. The previous implementation silently ignored such a situation.
If the previous behavior is desired, you can use the new system property, java.util.Arrays.useLegacyMergeSort, to restore previous mergesort behavior.
Nature of Incompatibility: behavioral
简单来说,java.util.Arrays.sort() 和 java.util.Collections.sort() 两个排序方法底层使用的算法有更新。使用这两个方法对数组或集合排序时,如果检查到 Comparable 没有遵守 Comparable contract 时会抛出 IllegalArgumentException。而以前版本中会忽略该异常。可以设置 java.util.Arrays.useLegacyMergeSort 系统属性来切换到以前的不会抛出异常的 mergesort。
解决方法
对于标准的 JVM,可以使用以下方法
1 | java -Djava.util.Arrays.useLegacyMergeSort=true |
对于 Android,上述方法不可行。Android SDK java.util.Arrays.sort() 方法源码如下:
1 | public static void sort(Object[] a) { |
直接不支持 LegacyMergeSort !!
所以只能老老实实分析出问题的代码为什么没有正确的实现 Comparable contract。其实很容易缩小问题范围,是使用 BitmapTrace.currentBimapSize() 的返回值对 BitmapTrace 列表来排序的:
1 | sortedBy { it.currentBitmapSize() } |
BitmapTrace.currentBimapSize() 方法会不会有问题?
1 | fun currentBitmapSize(): Int = get()?.allocationByteCount ?: 0 |
不难理解,它的返回值是不稳定的。当前对象是个 WeakReference,这个对象未被回收时 currentBitmapSize() 返回 Bitmap 内存大小,这个对象被回收时返回 0。所以如果排序持续过程足够长,其中发生了 GC,很可能以下描述的这种局面。参考来源
一开始时有三个对象 A, B, C,大小分别如下:
1 | A: 100 |
这时 A > B, B > C。所以可以断定 A > C 必然成立。由于 GC 导致 A 对象被回收,所以 A, B, C 三个对象大小恰好变成了这样:
1 | A: 0 |
这里 A > C 不成立了。JVM 一旦检测到这一点,就立即抛出 java.lang.IllegalArgumentException: Comparison method violates its general contract!。(要不要这么严肃???)
所以解决办法是保证 currentBitmapSize() 返回值是稳定的、不受 GC 影响的。
测试代码修改如下,使用 oldBitmapSize() 代替原来的 currentBitmapSize()。这个测试跑一下午也没有出现 Comparison method violates its general contract! 异常,问题完美修复!
1 | class ContractTest { |