运算符 new 不会在 Android 上抛出 bad_alloc

Operator new does not throw bad_alloc on Android

我正在 Android NDK (android-ndk-r9b) 上开发 C++ 游戏。如果我这样写:

class Test
{
    char c[1024*1024*1024];
};

//in main
try {

    Test* p;
    while (1) {
        p = new Test();
    }
} catch (bad_alloc) {
    cout << "bad_alloc\n";
}

它不会抛出。如果我试试这个:

void no_memory_by_new() {
    cout << "no_memory_by_new\n";
    throw bad_alloc();
}

//in main
set_new_handler(no_memory_by_new);
Test* p;
while (1) {
    p = new Test();
}

也不行。最后,如果我试试这个:

//in main
set_new_handler(no_memory_by_new);
int* p;
while (1) {
    p = new int[1024*1024*1024];
}

然后调用 no_memory_by_new。我真的很困惑。谁能帮帮我?

为了产生工作异常,您需要使用默认库以外的其他 C++ 标准库之一进行构建。添加例如APP_STL := gnustl_staticjni/Application.mk。此外,您需要启用例外,将 LOCAL_CPP_FEATURES += exceptions 添加到 Android.mk,或将 APP_CPPFLAGS += -fexceptions 添加到 jni/Application.mk

这是 Android GNU libstdc++ 构建中的错误。如果您查看 _GLIBCXX_THROW_OR_ABORToperator new implementation, you'll see it call _GLIBCXX_THROW_OR_ABORT if malloc return NULL. Next, if you look on definition,您会看到它仅在定义了 __EXCEPTIONS 时才会抛出 bad_alloc;否则,它只是调用 abort。由于某种原因,__EXCEPTIONS 宏未在 GNU libstdc++ 的 Android 版本中定义,因此它调用 abort - 正是您在您的案例中看到的。

我已经用 Android NDK r10d 和 CrystaX NDK 10.1 检查了这种行为 - 在这两种情况下都是一样的。我已经提交 ticket to fix this in CrystaX NDK. For fixing that in Google's NDK, you should also file ticket in Google's NDK bug tracker

UPD:看来情况并没有那么简单...进一步调查后,我发现了更多细节,表明事情比我上面描述的要复杂一些。看得更远;当有严格的结果时将更新答案。

UPD2:经过深入调查,我发现我之前的回答是完全错误的。事实上,__EXCEPTIONS 是在构建 GNU libstdc++ 时定义的,所以 operator new 实际上会抛出 bad_alloc if malloc return NULL。问题实际上出在您的代码中,但要弄清楚有点棘手。请参阅下面的解释。

TL;DR: operator new return 指向 "allocated" 内存的指针(所以从它的角度来看,没有理由抛出 std::bad_alloc),但首先访问该内存会导致崩溃,因为实际上这些页面不可用。

更详细的解释:

这是我用于测试的完整代码:

#include <new>
#include <stdio.h>
#include <string.h>

class Test
{
public:
    Test() {
        ::fprintf(stderr, "ctor start\n");
        //memset(c, 0, sizeof(c));
        ::fprintf(stderr, "ctor finish\n");
    }

private:
    char c[1024*1024*1024];
};

int main()
{
    try {
        while (1) {
            Test *p = new Test();
            if (!p)
                return 1;
        }
        return 1;
    } catch (std::bad_alloc) {
        return 0;
    }
}

如果您编译此测试并在设备上 运行,您将在某些迭代中得到 std::bad_alloc(我在第三次迭代中得到它)。但是,如果您在 Test 的构造函数中取消注释 memset,应用程序将在第一个 memset 调用时崩溃。如果你完全删除 Test 的构造函数,它也会崩溃 - 只是因为在这种情况下编译器将生成构造函数,它对所有成员进行零初始化 - 即与我们对 memset.

所做的相同

这里的区别在于malloc(在operator new内部使用)return指向"allocated"内存的指针,但实际上没有分配;该区域仅标记为 "need to be allocated in future, when application will actually refer it"。出于性能原因,这就是 Linux 内核处理它的方式。在下一步中,当您(或编译器)用零填充数组时,应用程序实际上会访问这些页面,但不幸的是,系统中没有可用内存,因此 Linux 内核调用 OOM 杀手,结果终止进程。

这不是 Android 特有的。事实上,在 GNU/Linux 系统上也会发生同样的情况;唯一的区别是系统可用的内存量(在 Android 上它比在服务器上低得多,原因很明显)。