专栏首页QQ音乐技术团队的专栏谁创建谁销毁,谁分配谁释放——JNI调用时的内存管理

谁创建谁销毁,谁分配谁释放——JNI调用时的内存管理

在QQ音乐AndroidTV端的Cocos版本的开发过程中,我们希望尽量多的复用现有的业务逻辑,避免重复制造轮子。因此,我们使用了大量的JNI调用,来实现Java层和Native层(主要是C++)的代码通信。一个重要的问题是JVM不会帮我们管理Native Memory所分配的内存空间的,本文就主要介绍如何在JNI调用时,对于Java层和Native层映射对象的内存管理策略。

1. 在Java层利用JNI调用Native层代码

如果有Java层尝试调用Native层的代码,我们通常用Java对象来封装C++的对象。举个例子,在Java层的一个监听播放状态的类:MusicPlayListener,作用是将播放状态发送给位于Native层的Cocos,通知Cocos在界面上修改显示图标,例如“播放”,“暂停”等等。

第一种做法,是在Java类的构造函数中,调用Native层的构造函数,分配Native Heap的内存空间,之后,在Java类的finalize方法中调用Native层的析构函数,回收Native Heap的内存空间。

// in Java:
public class MusicPlayListener {
    // 指向底层对象的指针,伪装成Java的long
    private final long ptr; 

    public MusicPlayListener() {
        ptr = ccCreate();
    }

    // 在finalize里释放
    public void finalize() { 
        ccFree(ptr);
    }

    // 是否正在播放
    public void setPlayState(boolean isPlaying){ 
        ccSetPlayState(ptr,isPlaying);
    }

    private static native long ccCreate();
    private static native void ccFree(long ptr);
    private native void ccSetPlayState(long ptr,boolean isPlaying);
}

// in C:
jlong Java_MusicPlayListener_ccCreate(JNIEnv* env, jclass unused) {
    // 调用构造函数分配内存空间
    CCMusicPlayListener* musicPlayListener = 
        new CCMusicPlayListener(); 
    return (jlong) musicPlayListener;
}

void Java_MusicPlayListener_ccFree(
    JNIEnv* env,
    jclass unused,
    jlong ptr) {
        // 释放内存空间   
        delete ptr; 
}

void Java_MusicPlayListener_ccSetPlayState(
    JNIEnv* env,
    jclass unused,
    jlong ptr,
    jboolean isPlaying) {
        //将播放状态通知给UI线程
        (reinterpret_cast<CCMusicPlayListener*>(ptr))->setPlayState(isPlaying);    
}

这种做法会让Java对象和Native对象的生命周期保持一致,当Java对象在Java Heap中,被GC判定为回收时,同时会将Native Heap中的对象回收。

不通过finalize的话,也可以用其他类似的机制适用于上述场景。比如Java标准库提供的DirectByteBuffer的实现,用基于PhantomReference的sun.misc.Cleaner来清理,本质上跟finalize方式一样,只是比finalize稍微安全一点,他可以避免”悬空指针“的问题。

这种方式的一个重要缺点,就是不管是finalize还是其他类似的方法,都依赖于JVM的GC来处理的。换句话说,如果不触发GC,那么finalize方法就不会及时调用,这可能会导致Native Heap资源耗尽,而导致程序出错。当Native层需要申请一个很大空间的内存时,有一定几率出现Native OutOfMemoryError的问题,然后找了半天也发现不了问题在哪里...

第二种方法是对Api的一些简单调整,以解决上述问题。不在JNI的包装类的构造函数中初始化Native层对象,尽量写成open/close的形式,在open的时候初始化Native资源,close的时候释放,finalize作为最后的保险再检查释放一次。

虽然没有本质上的变化,但open/close这种Api设计,一般来说,对90%的开发人员还是能够提醒他们使用close的,至于剩下的10%...好像除了开除也没啥好办法了...

2. 在Native层利用JNI调用Java层代码

上一种情况,是以Java层为主导,Native层对象的生命周期受Java层对象的控制。下面要介绍的是另一种情况,即Native层对象为主导,由他控制Java层对象的生命周期。

2.1 Native层操作Java层对象

想要在native层操作Java Heap中的对象,需要位于Native层的引用(Reference)以指向Java Heap中的内存空间。JNI中为我们提供了三种引用:本地引用(Local Reference),全局引用(Global Reference)和弱全局引用(Weak Global Reference)。

Local Reference的生命周期持续到一个Native Method的结束,当Native Method返回时Java Heap中的对象不再被持有,等待GC回收。一定要注意不要在Native Method中申请过多的Local Reference,每个Local Reference都会占用一定的JVM资源,过多的Local Reference会导致JVM内存溢出而导致Native Method的Crash。但是有些情况下我们必然会创建多个Local Reference,比如在一个对列表进行遍历的循环体内,这时候开发人员有必要调用DeleteLocalRef手动清除不再使用的Local Reference。

//C++代码
class Coo{
public:
   void Foo(){
     //获得局部引用对象ret
     jobject ret = env->CallObjectMethod();  

    for(int i =0;i<10;i++){
        //获得局部引用对象cret
        jobject cret = env->CallObjectMethod();  

        //...

        //手动回收局部引用对象cret 
        env->DeleteLocalRef(cret);        
    }
  }  //native method 返回,局部引用对象ret被自动回收
};

Global Reference的生命周期完全由程序员控制,你可以调用NewGlobalRef方法将一个Local Reference转变为Global Reference,Global Reference的生命周期会一直持续到你显式的调用DeleteGlobalRef,这有点像C++的动态内存分配,你需要记住new/delete永远是成对出现的。

//C++代码
class Coo{
public:
    void Foo(){
     //获得局部引用对象ret
     jobject ret = env->CallObjectMethod(); 
     //获的全局引用对象gret 
     jobject gret = env->NewGlobalRef(ret);  
 }//native method 返回,局部引用对象ret被自动回收
 //gret不会回收,造成内存溢出
};

Weak Global Reference是一种特殊的Global Reference,它允许JVM在Java Heap运行GC时回收Native层所持有的Java对象,前提是这个对象除了Weak Reference以外,没有被其他引用持有。我们在使用Weak Global Reference之前,可以使用IsSameObject来判断位于Java Heap中的对象是否被释放。

2.2 Native层释放的同时释放Java层对象

C++中的对象总会在其生命周期结束时,调用自身的析构函数,释放动态分配的内存空间,Cocos利用资源释放池(其本质是一种引用计数机制)来管理所有继承自cocos2d::CCObject(3.2版本之后变为cocos::Ref)的对象。换言之,对象的生命周期交给Cocos管理,我们需要关心对象的析构过程。

一种简单有效的做法,是在C++的构造函数中,实例化Java层的对象,在C++的析构函数中释放Java层对象。举个例子,主界面需要拉取Java层代码来解析后台协议,获取到主界面的几个图片的URL信息。

先来看显示效果:

再看代码:

//C++代码
class CCMainDeskListener
{
public:
    CCMainDeskListener();
    ~CCMainDeskListener();
private:
    //Java层对象的全局引用
    jobject retGlobal;                   
};

CCMainDeskListener::CCMainDeskListener()
{
    //获得本地引用
    jobject ret = CallStaticObjectMethod();   
    //创建全局引用    
    retGlobal = NewGlobalRef(ret); 
    //清除本地引用  
    DeleteLocalRef(ret);             

}

CCMainDeskListener::~CCMainDeskListener()
{
    //清除全局引用
    DeleteGlobalRef(retGlobal);   
}

在C++的构造函数中,调用Java层的方法初始化了Java对象,这个引用分配的内存空间位于Java Heap。之后我们创建全局引用,避免Local Reference在Native Method结束之后被回收,而全局引用在析构函数中被删除,这样就保证了Java Heap中的对象被释放,保持Native层和Java层的释放做到同步。

上述方法中,Java层对象的生命周期是跟随Native层对象的生命周期的,Native层对象的生命周期结束时会释放对于Java层对象的持有,让GC去回收资源。我们想进一步了解Native层对象的什么时候被回收,接下来介绍一下Cocos的内存管理策略。

3.Cocos的内存管理

C++中,在堆上分配和释放动态内存的方法是new和delete,程序员要小心的使用它们,确保每次调用了new之后,都有delete与之对应。为了避免因为遗漏delete而造成的内存泄露,C++标准库(STL)提供了auto_ptr和shared_ptr,本质上都是用来确保当对象的生命周期结束时,堆上分配的内存被释放。

Cocos采用的是引用计数的内存管理方式,这已经是一种十分古老的管理方式了,不过这种方式简单易实现,当对象的引用次数减为0时,就调用delete方法将对象清除掉。具体实现上来说,Cocos会为每个进程创建一个全局的CCAutoreleasePool类,开发人员不能自己创建释放池,仅仅需要关注release和retain方法,不过前提是你的对象必须要继承自cocos2d::CCObject类(3.0版本之后变为cocos2d::Ref类),这个类是Cocos所有对象继承的基类,有点类似于Java的Object类。

当你调用object->autorelease()方法时,对象就被放到了自动释放池中,自动释放池会帮助你保持这个obejct的生命周期,直到当前消息循环的结束。在这个消息循环的最后,假如这个object没有被其他类或容器retain过,那么它将自动释放掉。例如,layer->addChild(sprite),这个sprite增加到这个layer的子节点列表中,他的声明周期就会持续到这个layer释放的时候,而不会在当前消息循环的最后被释放掉。

跟内存管理有关的方法,一共有三个:release(),retain()和autorelease()。release和retain的作用分别是将当前引用次数减一和加一,autorelease的作用则是将当前对象的管理交给PoolManager。当对象的引用次数减为0时,PoolManager就会调用delete,回收内存空间。

release和retain的作用分别是将当前引用次数减一和加一,autorelease的作用则是将当前对象的管理交给PoolManager。当对象的引用次数减为0时,PoolManager就会调用delete,回收内存空间。

一般情况下,我们需要记住的就是继承自Ref的对象,使用create方法创建实例后,是不需要我们手动delete的,因为create方法会自己调用autorelease方法。

4.总结

JNI调用时,即可能造成Native Heap的溢出,也可能造成Java Heap的溢出,作为JNI软件开发人员,应该注意以下几点:

  1. Native层(一般是C++)本身的内存管理。
  2. 不使用的Global Reference和Local Reference都要及时释放。
  3. Java层调用JNI时尽量使用open/close的格式替代构造函数/finalize的方式。

本文分享自微信公众号 - QQ音乐技术团队(gh_287053a877e6),作者:glensun

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2016-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • (iOS开发)之内存管理

    Objective-C的内存管理主要有三种方式ARC(自动内存计数)、MRC(手动内存计数)、内存池。

    ios-lan
  • OC中内存管理的一些问题

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

    用户1451823
  • 那些年Android黑科技①:只要活着,就有希望

    “黑科技什么的最喜欢了! 对,我们就是要搞事。 来呀。谁怕谁。三年血赚,死刑不亏。(๑´ڡ`๑) ” -- 来自暗世界android工程师

    陈宇明
  • iOS中的各种理论知识

    1. 你如何理解OC 的内存管理 OC 内存管理是基于引用计数。谁想使用某个对象B,就要把对象B 的计数器+1,如果不

    用户1451823
  • OC内存管理

    用户1941540
  • RAC(ReactiveCocoa)介绍(七)——信号销毁

    在RACSignal信号发送命令执行之后,本着谁创建谁销毁的原则,最后一步必须要进行销毁操作。而销毁操作的执行则由RACDisposable类来完成。 RAC...

    我只不过是出来写写代码
  • Objective-C 内存管理

    1) 手动引用计数 MRC (Mannul Reference Counting);

    meteoric
  • 五万字长文带你学会Spring

    Sping:Spring是分层的javaEE/SE应用full-stack轻量级开源框架,他以AOP( 面向切面编程 aspect oriented prog...

    一只胡说八道的猴子
  • 内存泄露从入门到精通三部曲之常见原因与用户实践

    常见原因 1.集合类 集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map...

    腾讯Bugly
  • 学 Java 开发怎么能不知道 Filter 与 Listener

      Filter 也称之为过滤器,它是 Servlet 技术中最实用的技术之一。通过 Filter 技术,可以对 web 服务器管理的所有 web 资源:例如 ...

    Demo_Null
  • 带你搞懂Java线程池

    RejectedExecutionHandler是一个接口,JDK提供了四种实现,如果都不合适,可以自己实现这个接口去处理。

    longzeqiu
  • Android 使用MediaRecorder录音调用stop()方法的时候报错

    这个问题在网上看到了太多的答案,一直提示说按照官网的api的顺序来,其实解决问题的方法不是这样的,那样没法解决问题,照着那个顺序来也米有用

    wust小吴
  • 用Python实现一个简单的线程池

    在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是 如此,虚拟机将试图跟踪每一个对象,以便能够在对象...

    py3study
  • 你会不会处理多线程中的对象管理?

    就那七个张伟,他们有一个共用属性,钱包里的钱。这天,张伟A在吃喝的时候,发现钱给没了,原因是张伟B拿去捐款了,那就很尴尬了。为了避免这种情况,怎么办?他们商量了...

    看、未来
  • 聊聊Objective-C内存管理

    内存管理的文章网上太多了,本文只是简单的聊聊内存管理 让你加深内存管理的理解。 了解内存管理首先你需要思考几个问题 1.为什么需要进行内存管理? 2.内管...

    赵哥窟
  • OC学习10——内存管理

    1、对于面向对象的语言,程序需要不断地创建对象。这些对象都是保存在堆内存中,而我们的指针变量中保存的是这些对象在堆内存中的地址,当该对象使用结束之后,指针变量指...

    mukekeheart
  • PHP储存和销毁session的实现

    PHP session ,用于存储关于用户会话(session)的信息,或者更改用户会话(session)的设置。Session 变量存储单一用户的信息,并且对...

    德顺
  • 第一节预解释、作用域、this原理

    河湾欢儿
  • OC知识--彻底理解内存管理(MRC、ARC)

    程序员充电站

扫码关注云+社区

领取腾讯云代金券