能否用Kotlin内联类优化垃圾回收?Vector实现相关疑问
我正在开发一款基于Java Swing和Java AWT的游戏,每数毫秒就会重绘一次。查阅开发禁忌时,多次看到关于Garbage Collector (GC) 的警告:尤其在主循环结束时频繁生成并丢弃大量类实例(通常是Vector类),会导致GC频繁回收,引发恼人的微小卡顿。
我发现自己的代码需要大量Vector实例(实际是包装(x,y)整数对的类),其中很多仅用于中间计算。我用Kotlin开发,一开始想创建可重置x、y属性的MutVector类,但觉得这种方案违背直觉且容易出错。
后来想到Kotlin的内联类(现@JVMInline value类),它可作为特定类型的包装器,比如:
@JVMInline value class EmailAdress(val value: String) { fun getHost() { return value.split("@").last() } }
它是与String不同的独立类型,拥有自身方法,但运行时(大多数情况)以普通String形式存在,避免运行时开销。由于原生类型的优化特性,包装Int、Long等类型时能节省大量时间,而且原生类型无需GC回收。
于是我实现了如下代码:
private fun checkWontOverflow(x: Int): Int { // Value chosen because 2^31 - 1 misbehaves with shl, and for the usage I'm doing of it, it doesn't matter (it'll store coordinates smaller than 100,000) // much between 2^30 and 2^31 require(abs(x) < 2.0.pow(30)) return x } @JVMInline value class Vector private constructor(private val storage: Long) { constructor(x: Int, y: Int): this(checkWontOverflow(x).toLong() shl 32 + checkWontOverflow(y).toLong()) val x: Int get() = (storage shr 32).toInt() val y: Int get() = (storage - x shl 32).toInt() }
我的疑问
- 是否存在异常场景(比如溢出问题)?
- 它能否真正减轻GC负担、消除卡顿?
- 有没有更简便的实现方式?
- 我需要(x: Float, y: Float)类型的Vector,能否通过将两个Float合并为一个Double实现类似方案?
1. 异常场景与溢出问题
你的代码存在两个关键问题:
- 位运算优先级错误:
shl的优先级低于+,构造函数里的shl 32 + ...会被解析为shl (32 + ...),完全偏离了合并x、y到Long的逻辑,直接导致存储值错误甚至溢出。 - y值提取逻辑错误:
storage - x shl 32同样存在优先级问题,且计算效率低。
修正后的核心逻辑应该是:
// 构造时正确合并x和y (checkWontOverflow(x).toLong() shl 32) + checkWontOverflow(y).toLong() // 提取y时用位掩码直接取低32位 val y: Int get() = (storage and 0xFFFFFFFFL).toInt()
另外,你用2.0.pow(30)做范围检查会引入浮点数计算开销,换成整数常量0x3FFFFFFF更高效。
2. 对GC负担的影响
完全可以减轻GC负担甚至消除相关卡顿。@JVMInline value类在绝大多数场景下会被编译器内联为原始类型(这里就是Long),不会创建额外对象实例。所有中间计算产生的Vector都会以Long形式存在于栈上,不会进入堆内存,自然不会触发GC回收,完美解决频繁创建丢弃实例导致的卡顿问题。
3. 更简便的实现方式
可以简化为以下版本,兼顾正确性和简洁性:
@JVMInline value class Vector private constructor(private val storage: Long) { constructor(x: Int, y: Int) : this( (x.toLong() shl 32) or (y.toLong() and 0xFFFFFFFFL) ) { // 用整数常量替代浮点数计算,明确范围 require(x in -0x40000000..0x3FFFFFFF) { "x超出安全范围" } require(y in -0x40000000..0x3FFFFFFF) { "y超出安全范围" } } val x: Int get() = (storage shr 32).toInt() val y: Int get() = (storage and 0xFFFFFFFFL).toInt() }
如果你的坐标范围确实远小于100,000,甚至可以去掉范围检查(但建议保留,避免后续扩展时出错)。
4. Float类型Vector的实现方案
可以通过将两个Float合并为一个Double实现类似方案。每个Float是32位,Double是64位,正好可以把两个Float的二进制位分别存入Double的高32位和低32位,实现代码如下:
@JVMInline value class FloatVector private constructor(private val storage: Double) { constructor(x: Float, y: Float) : this( (java.lang.Double.doubleToLongBits(x.toDouble()) shl 32) or (java.lang.Double.doubleToLongBits(y.toDouble()) and 0xFFFFFFFFL) ) val x: Float get() = java.lang.Double.longBitsToDouble(storage.toLong() shr 32).toFloat() val y: Float get() = java.lang.Double.longBitsToDouble(storage.toLong() and 0xFFFFFFFFL).toFloat() }
这种方式同样不会产生额外对象,完全避免GC开销,和Int版Vector的优化逻辑一致。
内容的提问来源于stack exchange,提问作者Alphasaft

