记一个 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 { |