谁创建谁销毁,谁分配谁释放——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。但是有些情况下我们必然会创建多个LocalReference,比如在一个对列表进行遍历的循环体内,这时候开发人员有必要调用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的方式。
Image placeholder
IT头条
未设置
  82人点赞

没有讨论,发表一下自己的看法吧

推荐文章
简易图书管理系统——Java实现

推荐课程:JavaWeb项目实战全程实录#学完你就可以自己用Java做项目了 本人还是小白,闲来无事,跟着网络上大牛们学习了这段程序,本身难度并不大,但自己在很多细节地方考虑的还是不够周全,一些功能还

自动识别Android不合理的内存分配

写在前面Android开发中我们常常会遇到不合理的内存分配导致的问题,或是频繁GC,或是OOM。按照常规的套路我们需要打开AndroidStudio录制内存分配或者dump内存,然后人工分析,逐个排查

Go内存分配跟踪调优

今天小编为大家分享一篇关于Go内存分配跟踪调优的文章,文中涉及到一些压测及跟踪分析的工具,以及问题查找方法,希望能对大家有所帮助。Makeitwork,makeitright,makeitfast.–

多进程之间的线程利用XSI IPC共享内存分配互斥量进行同步

···#include#include#include#include#include#include#include#include#include#definehandle_error_en(en

Go语言高级编程_1.5 面向并发的内存模型

1.5面向并发的内存模型 在早期,CPU都是以单核的形式顺序执行机器指令。Go语言的祖先C语言正是这种顺序编程语言的代表。顺序编程语言中的顺序是指:所有的指令都是以串行的方式执行,在相同的时刻有且仅有

JavaScript常见的内存泄漏

课程推荐:Java开发工程师--学习猿地精品课程 前言1介绍2内存泄露的主要原因3常见的内存泄露3.1全局变量3.2计时器3.3多处引用3.4闭包4Chrome内存分析工具资料前言在阅读这篇博客之前,

ZILLIZ AI数据中台:打破数据处理瓶颈,释放AI效能

在最近结束的第十届中国数据库技术大会(DTCC2019)上,ZILLIZ得到了众多专业评委的一致认可,获选为“2019中国数据库技术年度评选——年度创新企业”。这家成立于2016年的企业,凭借对技术发

开源社区的技术债:写代码的“码农”VS 删代码的“清道夫”,谁更该被嘉奖?

大数据文摘出品编译:楚阳、橡树、钱天培对于开源项目来讲,写新代码的贡献者不一定是好程序员,但不会删代码的程序员一定不是合格的程序员——因为“删代码”才是使开源软件项目的代码简洁高效的关键所在。Mong

企业出了IT事故,谁该来背锅?

当企业内部出现IT事故时,舆论质疑、客户追责甚至诉讼等问题都会接踵而至,这些问题会令企业蒙受巨大的损失。那么,谁应该为这样的失误负责呢?有些企业由于未能及时更新系统补丁而导致IT事故,企业的声誉和估值

超大规模商用 K8s 场景下,阿里巴巴如何动态解决容器资源的按需分配问题?

导读:资源利用率一直是很多平台管理和研发人员关心的话题。本文作者通过阿里巴巴容器平台团队在这一领域的工作实践,整理出了一套资源利用提升的方案,希望能够带给大家带来一些讨论和思考。引言不知道大家有没有过

jquery如何获取html元素的内容?

jquery如何获取html元素的内容?一、text():设置或返回所选元素的文本内容文本信息 $('#p').text();//文本信息 $('#p').text('新的文本');二、html()

jquery判断浏览器的内核

jquery判断浏览器的内核判断浏览器内核可以使用$.browser属性。$.browser属性在jQuery1.9已经被移除。用于返回用户当前使用的浏览器的相关信息。不建议使用该属性来检测浏览器,因

[扩展推荐] Laravue —— 漂亮的 Laravel 管理界面

介绍几个月前我尝试为我的项目寻找新的解决方案,我已经使用Vue构建了一个单页应用(使用这个非常棒的框架,使用LaravelLumen作为API网关,使用LaravelPassport作为SSO服务器)

Laravel 第七章学习——会话管理

Laravel强大的用户认证机制(部分内容)一、Laravel提供的 Auth 的 attempt 方法可以让我们很方便的完成用户的身份认证操作:attempt 方法会接收一个数组来作为第一个参数,该

冬虫夏草之技术路线图之五【“图”——管理篇】

作为一名28年证券机构从业经历的老兵,杨松一直在观察和研究IT技术对金融机构的业务重构,以及证券业务变革相关的内容。今天,让我们来看看这位金融业内人士如何利用他28年的行业积累,通过“技”“术”“路”

项目管理最佳实践,企业如何进行有效的项目管理

前言:企业在划分项目时,可按照项目的复杂程度、管理范围等将项目分为三个级别,分别是企业级、部门级和小组级(与目标划分原则相同),然后将每一级的目标与项目对应起来。我们知道,企业制定的目标(OKR),一

记一次 vue 的异步更新队列导致内存泄漏

起因 由于项目是需要连续传输图片形成一个伪视频(没办法,客户钱给的不够)来观看。后端采用传输base64的图片到前端展示。 环境 php:7.2 workerman:3.X vue:2.X 过程 wo

Go语言高级编程_2.7 CGO内存模型

2.7CGO内存模型 CGO是架接Go语言和C语言的桥梁,它使二者在二进制接口层面实现了互通,但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的

基于内存和文件存储的 queue worker, 不用 Redis 适合单进程使用没有外部依赖

因为最近要做一个简单的并发任务系统,在github上面找了一圈并没有简单可依赖的库,所以自己写了一个。欢迎大家Review贡献代码。项目地址https://github.com/iflamed/mfw

共享内存在不同系统的应用与优劣详解

共享内存是一种使计算机程序能够同时共享内存资源以实现更高性能和更少冗余数据副本的技术。共享系统内存可以在单处理器系统、并行多处理器或集群微处理器上运行。对于分布式系统会有一些差异,但共享内存也可以其上

Java内存映射,上G大文件轻松处理

内存映射文件(Memory-mappedFile),指的是将一段虚拟内存逐字节映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作),

面试题:请解释一下什么是虚拟内存?

内存对于用户来说就是一个字节数组,我们可以根据地址来访问到某个字节或者某些字节:很久之前的内存很久很久之前,一台机器上只放置一个程序,操作系统仅仅作为一个函数库存在。对于内存来说,除去操作系统的代码和

Kafka 如何优化内存缓冲机制造成的频繁 GC 问题?

目录1、Kafka的客户端缓冲机制2、内存缓冲造成的频繁GC问题3、Kafka设计者实现的缓冲池机制4、总结一下“ 这篇文章,给大家聊一个硬核的技术知识,我们通过Kafka内核源码中的一些设计思想,来

打破边界 不是所有“内存与存储”都叫傲腾

人类正在向一个万物感知、万物互联、万物智能的世界进化。一方面海量的数据对数据基础设施带来了新的挑战;另一方面伴随着数据中心业务和应用的多样化以及智能化,企业对数据存储的需求越来越高。智能世界的特点是能

深入理解JVM - 内存溢出实战

Java堆溢出Java堆用于存储对象实例,只要不断地创建对象,当对象数量到达最大堆的容量限制后就会产生内存溢出异常。最常见的内存溢出就是存在大的容器,而没法回收,比如:Map,List等。出现下面信息