Android | Bitmap的Java对象GC之后,对应的native内存会回收吗?
前言
- Bitmap 的内存分配分外两块:Java 堆和native 堆。我们都知道 JVM 有垃圾回收机制,那么当 Bitmap的Java对象GC之后,对应的 native 堆内存会回收吗?
带你理解 NativeAllocationRegistry 的原理与设计思想
NativeAllocationRegistry
是Android 8.0(API 27)
引入的一种辅助回收native
内存的机制,使用步骤并不复杂,但是关联的Java
原理知识却不少
- 这篇文章将带你理解
NativeAllocationRegistry
的原理,并分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
1. 使用步骤
从Android 8.0(API 27)
开始,Android
中很多地方可以看到NativeAllocationRegistry
的身影,我们以Bitmap
为例子介绍NativeAllocationRegistry
的使用步骤,涉及文件:Bitmap.java、Bitmap.h、Bitmap.cpp
步骤1:创建 NativeAllocationRegistry
首先,我们看看实例化NativeAllocationRegistry
的地方,具体在Bitmap
的构造函数中:
// # Android 8.0
// Bitmap.java
// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代码...
// 【分析点 1:native 层需要的内存大小】
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
// 【分析点 2:回收函数 nativeGetNativeFinalizer()】
// 【分析点 3:加载回收函数的类加载器:Bitmap.class.getClassLoader()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}
private static final long NATIVE_ALLOCATION_SIZE = 32;
private static native long nativeGetNativeFinalizer();
可以看到,Bitmap
的构造函数(在从JNI
中调用)中实例化了NativeAllocationRegistry
,并传递了三个参数:
参数 | 解释 |
---|---|
classLoader |
加载freeFunction 函数的类加载器 |
freeFunction |
回收native 内存的native 函数直接地址 |
size |
分配的native 内存大小(单位:字节) |
步骤2:注册对象
紧接着,调用了registerNativeAllocation(...)
,并传递两个参数:
参数 | 解释 |
---|---|
referent |
Java 层对象的引用 |
nativeBitmap |
native 层对象的地址 |
// Bitmap.java
// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代码...
// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}
// NativeAllocationRegistry.java
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
// 代码省略,下文补充...
}
步骤3:回收内存
完成前面两步后,当Java
层对象被垃圾回收后,NativeAllocationRegistry
会自动回收注册的native
内存。例如,我们加载几张图片,随后释放Bitmap
的引用,可以观察到GC
之后,native
层的内存也自动回收了:
tv.setOnClickListener{
val map = HashSet()
for(index in 0 .. 2){
map.add(BitmapFactory.decodeResource(resources,R.drawable.test))
}
- GC 前的内存分配情况 —— Android 8.0
- GC 后的内存分配情况 —— Android 8.0
2. 提出问题
掌握了NativeAllocationRegistry
的作用和使用步骤后,很自然地会有一些疑问:
- 为什么在
Java
层对象被垃圾回收后,native
内存会自动被回收呢? NativeAllocationRegistry
是从Android 8.0(API 27)
开始引入,那么在此之前,native
内存是如何回收的呢?
通过分析NativeAllocationRegistry
源码,我们将一步步解答这些问题,请继续往下看。
3. NativeAllocationRegistry 源码分析
现在我们将视野回到到NativeAllocationRegistry
的源码,涉及文件:NativeAllocationRegistry.java 、NativeAllocationRegistry_Delegate.java、libcore_util_NativeAllocationRegistry.cpp
3.1 构造函数
// NativeAllocationRegistry.java
public class NativeAllocationRegistry {
// 加载 freeFunction 函数的类加载器
private final ClassLoader classLoader;
// 回收 native 内存的 native 函数直接地址
private final long freeFunction;
// 分配的 native 内存大小(字节)
private final long size;
public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
if (size < 0) {
throw new IllegalArgumentException("Invalid native allocation size: " + size);
}
this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = size;
}
}
可以看到,NativeAllocationRegistry
的构造函数只是将三个参数保存下来,并没有执行额外操作。以Bitmap
为例,三个参数在Bitmap
的构造函数中获得,我们继续上一节未完成的分析过程:
- 分析点 1:native 层需要的内存大小
// Bitmap.java
// 【分析点 1:native 层需要的内存大小】
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
public final int getAllocationByteCount() {
if (mRecycled) {
Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
+ "This is undefined behavior!");
return 0;
}
// 调用 native 方法
return nativeGetAllocationByteCount(mNativePtr);
}
private static final long NATIVE_ALLOCATION_SIZE = 32;
可以看到,nativeSize
由固定的32
字节加上getAllocationByteCount()
,总之,NativeAllocationRegistry
需要一个native
层内存大小的参数,这里就不展开了。关于Bitmap
内存分配的详细分析请务必阅读文章:《Android | 各版本中 Bitmap 内存分配对比》
- 分析点 2:回收函数 nativeGetNativeFinalizer()
// Bitmap.java
// 【分析点 2:回收函数 nativeGetNativeFinalizer()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
private static native long nativeGetNativeFinalizer();
// Java 层
// ----------------------------------------------------------------------
// native 层
// Bitmap.cpp
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
// 转为long
return static_cast(reinterpret_cast(&Bitmap_destruct));
}
static void Bitmap_destruct(BitmapWrapper* bitmap) {
delete bitmap;
}
可以看到,nativeGetNativeFinalizer()
是一个native
函数,返回值是一个long
,这个值其实相当于Bitmap_destruct()
函数的直接地址。很明显,Bitmap_destruct()
就是用来回收native
层内存的。
那么,Bitmap_destruct()
是在哪里调用的呢?继续往下看!
- 分析点 3:加载回收函数的类加载器
// Bitmap.java
Bitmap.class.getClassLoader()
另外,NativeAllocationRegistry
还需要ClassLoader
参数,文档注释指出:classloader
是加载freeFunction
所在native
库的类加载器,但是NativeAllocationRegistry
内部并没有使用这个参数。这里笔者也不理解为什么需要传递这个参数,如果有知道答案的小伙伴请告诉我一下~
3.2 注册对象
// Bitmap.java
// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);
// NativeAllocationRegistry.java
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}
CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
// Other exceptions are impossible.
// Enable the cleaner only after we can no longer throw anything, including OOME.
thunk.setNativePtr(nativePtr);
return result;
}
可以看到,registerNativeAllocation (...)
方法参数是Java
层对象引用与native
层对象的地址。函数体乍一看是有点绕,笔者在这里也停留了好长一会。我们简化一下代码,try-catch
代码先省略,函数返回值Runnable
暂时用不到也先省略,瘦身后的代码如下:
// NativeAllocationRegistry.java
// (简化)
public void registerNativeAllocation(Object referent, long nativePtr) {
CleanerThunk thunk thunk = new CleanerThunk();
// Cleaner 绑定 Java 对象与回收函数
Cleaner cleaner = Cleaner.create(referent, thunk);
// 注册 native 内存
registerNativeAllocation(this.size);
thunk.setNativePtr(nativePtr);
}
private class CleanerThunk implements Runnable {
// 代码省略,下文补充...
}
看到这里,上文提出的第一个疑问就可以解释了,原来NativeAllocationRegistry
内部是利用了sun.misc.Cleaner.java
机制,简单来说:使用虚引用得知对象被GC的时机,在GC前执行额外的回收工作。若还不了解Java
的四种引用类型,请务必阅读:《Java | 引用类型》
# 举一反三 #
DirectByteBuffer
内部也是利用了Cleaner
实现堆外内存的释放的。若不了解,请务必阅读:《Java | 堆内存与堆外内存》
private class CleanerThunk implements Runnable {
// native 层对象的地址
private long nativePtr;
public CleanerThunk() {
this.nativePtr = 0;
}
public void run() {
if (nativePtr != 0) {
// 【分析点 4:执行内存回收方法】
applyFreeFunction(freeFunction, nativePtr);
// 【分析点 5:注销 native 内存】
registerNativeFree(size);
}
}
public void setNativePtr(long nativePtr) {
this.nativePtr = nativePtr;
}
}
继续往下看,CleanerThunk
其实是Runnable
的实现类,run()
在Java
层对象被垃圾回收时触发,主要做了两件事:
- 分析点 4:执行内存回收方法
public static native void applyFreeFunction(long freeFunction, long nativePtr);
// NativeAllocationRegistry.cpp
typedef void (*FreeFunction)(void*);
static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*,
jclass,
jlong freeFunction,
jlong ptr) {
void* nativePtr = reinterpret_cast(static_cast(ptr));
FreeFunction nativeFreeFunction = reinterpret_cast(static_cast(freeFunction));
// 调用回收函数
nativeFreeFunction(nativePtr);
}
可以看到,applyFreeFunction(...)
最终就是执行到了前面提到的内存回收函数,对于Bitmap
就是Bitmap_destruct()
- 分析点 5:注册 / 注销native内存
// NativeAllocationRegistry.java
// 注册 native 内存
registerNativeAllocation(this.size);
// 注销 native 内存
registerNativeFree(size);
// 提示:这一层函数其实就是为了将参数转为long
private static void registerNativeAllocation(long size) {
VMRuntime.getRuntime().registerNativeAllocation((int)Math.min(size, Integer.MAX_VALUE));
}
private static void registerNativeFree(long size) {
VMRuntime.getRuntime().registerNativeFree((int)Math.min(size, Integer.MAX_VALUE));
}
向VM
注册native
内存,比便在内存占用达到界限时触发GC
,在该native
内存回收时,需要向VM
注销该内存量
4. 对比 Android 8.0 之前回收 native 内存的方式
前面我们已经分析完NativeAllocationRegistry
的源码了,我们看一看在Android 8.0
之前,Bitmap
是用什么方法回收native
内存的,涉及文件:Bitmap.java (before Android 8.0)
// before Android 8.0
// Bitmap.java
private final long mNativePtr;
private final BitmapFinalizer mFinalizer;
// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代码...
mNativePtr = nativeBitmap;
mFinalizer = new BitmapFinalizer(nativeBitmap);
int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}
private static class BitmapFinalizer {
private long mNativeBitmap;
private int mNativeAllocationByteCount;
BitmapFinalizer(long nativeBitmap) {
mNativeBitmap = nativeBitmap;
}
public void setNativeAllocationByteCount(int nativeByteCount) {
if (mNativeAllocationByteCount != 0) {
// 注册 native 层内存
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
// 注销 native 层内存
VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
}
}
@Override
public void finalize() {
try {
super.finalize();
} catch (Throwable t) {
// Ignore
} finally {
setNativeAllocationByteCount(0);
// 执行内存回收函数
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
}
}
private static native void nativeDestructor(long nativeBitmap);
如果理解了NativeAllocationRegistry
的源码,上面这段代码就很好理解呀!
- 共同点:
- 分配的
native
层内存需要向VM
注册 / 注销 - 通过一个
native
层的内存回收函数来回收内存
- 分配的
- 不同点:
NativeAllocationRegistry
依赖于sun.misc.Cleaner.java
BitmapFinalizer
依赖于Object#finalize()
我们知道,finalize()
在Java
对象被垃圾回收时会调用,BitmapFinalizer
就是利用了这个机制来回收native
层内存的。若不了解,请务必阅读文章:《Java | 谈谈我对垃圾回收的理解》
再举几个常用的类在Android 8.0
之前的源码为例子,原理都大同小异:Matrix.java (before Android 8.0)、Canvas.java (before Android 8.0)
// Matrix.java
@Override
protected void finalize() throws Throwable {
try {
finalizer(native_instance);
} finally {
super.finalize();
}
}
private static native void finalizer(long native_instance);
// Canvas.java
private final CanvasFinalizer mFinalizer;
private static final class CanvasFinalizer {
private long mNativeCanvasWrapper;
public CanvasFinalizer(long nativeCanvas) {
mNativeCanvasWrapper = nativeCanvas;
}
@Override
protected void finalize() throws Throwable {
try {
dispose();
} finally {
super.finalize();
}
}
public void dispose() {
if (mNativeCanvasWrapper != 0) {
finalizer(mNativeCanvasWrapper);
mNativeCanvasWrapper = 0;
}
}
}
public Canvas() {
// 省略其他代码...
mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
}
5. 问题回归
NativeAllocationRegistry
利用虚引用感知Java
对象被回收的时机,来回收native
层内存- 在
Android 8.0 (API 27)
之前,Android
通常使用Object#finalize()
调用时机来回收native
层内存
推荐阅读
- Java | 带你理解 ServiceLoader 的原理与设计思想
- Android | 谈一谈 Matrix 与坐标变换
- Android | 一文带你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按钮快速点击
- Android | 这是一份详细的 EventBus 使用教程
- 开发者 | 浅析App社交分享的5种形式
- 计算机组成原理 | Unicode 和 UTF-8是什么关系?
- 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)
共有 0 条评论