在上一章中,咱们介绍了经过线程和多进程来优化虚拟内存的两种计划,它们现已能处理大部分问题了。但有的时分,这两种计划仍然无法完全优化虚拟内存,处理运用序溃散,那就需求用到一些非常规的“黑科技”手法了。今天,咱们就来介绍两种。第一种是开释 Android 体系为 WebView 保存的虚拟内存,第二种是开释虚拟机作为后台 GC 而运用的备份空间的虚拟内存。

在具体讲解这两种计划之前,我想先讲讲咱们是怎样发现这些非常规优化计划的,由于你或许会忧虑:这些手法只要技能大牛才干想出来,普通开发者很难想到。实践上,“非常规”计划和咱们常用的通用计划相同,同样是根据底层原理寻觅办法论,并衍生而出一系列的优化计划,然后对这些计划进行可行性验证的。

那么这一章说到的两种计划是怎么发现的呢?在《把握 App 运行时的内存模型》这一章中咱们就讲过,运用进程中一切已分配的虚拟内存都会记录在 maps 文件中,那这一点就是底层原理。根据这一原理,假如咱们一行一行地去剖析运用的 maps 文件,寻觅到运用中现已分配但或许用不上的虚拟内存空间,然后将这些空间想办法开释掉,是不是就能优化不少虚拟内存呢? 假如咱们从 maps 文件中找到的可开释的点越多,咱们的计划也就越多,然后咱们将这些计划一一进行可行性验证即可。这一章说到的两个优化计划,都是经过剖析 maps 文件后,发现占用了较多虚拟内存,然后尝试是否能够优化,最终验证优化是可行的。

这样的思路不只适用于内存相关的优化,也适用于其他方面的优化。在后面的章节中还有很多关于优化思路的讲解,咱们也都会遵循先原理,后计划,再总结办法论这样的讲解思路,期望你能把这样的思路记下来,尝试运用在工作中,终究形成你自己的优化办法论。

了解了非常规优化计划的寻觅思路,咱们马上进入实践,看看这两个计划的具体施行进程吧!

开释为 WebView 保存的虚拟内存

经过剖析体系“设置”这个运用的 maps 文件,能够发现有一块 anno:libwebview reservation 的虚拟内存空间的分配,经过核算(722d0a6000 – 71ed0a6000 = 1024 M ),这一块内存的巨细有 1G,关于设置来说,是没有 WebView 页面的,所以这一块虚拟内存实践上是用不上的。

虚拟内存优化:“黑科技”优化手段

libwebview reservation 的内存空间不只“设置”这个运用有,安装在 Android 体系上的每一个运用都会有,即便咱们新建一个空 Android 项目,将这个空运用跑起来之后,也能看到这一块虚拟内存的请求。实践上这部分虚拟内存是为了翻开 WebView 页面预留的空间,64 位机上是 1G 巨细,32 位机上是 130M 巨细,其他非 ARM 机器是 190M。经过源码 WebViewLibraryLoader.java 咱们能够看到它的逻辑。

虚拟内存优化:“黑科技”优化手段

这一块空间实践是在 Zygote 进程就现已请求了,Zygote 进行中会加载 webviewchromiun_loader 这个 so 库并请求一定巨细的虚拟空间,一切运用的进程都是经过 Zygote 进程 fork 的,所以也都会保存这一块区域。假如咱们的运用不需求运用体系 WebView ,或许咱们现已把 WebView 的运用场景放到了子进程中,那咱们完全能够在主进程上开释这一块空间,节约出 130M 的虚拟内存。

经过前面的学习咱们知道,虚拟内存都是经过 mmap 函数来请求的,那要开释虚拟内存,只需求调用 munmap()即可。

int munmap(void *start, size_t length)

所以假如咱们想要开释这部分内存,只需求在 Native 层调用如下代码:

// 722d0a6000 是起始地址,1073741824是1G转换成字节的巨细
munmap(0x722d0a6000, 1073741824)

但由于 libwebview reservation 这块空间的地址并不是固定的,所以咱们并不能将地址写死成 722d0a6000。并且 1G 是 64 位机的巨细,64 位上咱们并不需求忧虑虚拟内存不足,所以咱们只需求判别是否是 32 位机就能够了。假如是,则读取 maps 文件,解析 libwebview reservation,取出首地址和尾地址,核算 size,然后调用 munmap 函数。maps 文件的读取和解析逻辑,咱们在《把握 App 运行时的内存模型》中其实也见过,不过这儿仍是再演示一遍:

  1. 解析 maps,并寻觅 libwebview reservation 区域的首尾地址。
static bool FindReservedSpaceByParseMaps(void** start_out, size_t* size_out) {
    bool found = false;
    IterateMaps([&](uintptr_t start, uintptr_t end, char perms[4], const char* path, void* args) -> bool {
        if (perms[0] != '-' || perms[1] != '-' || perms[2] != '-' || perms[3] != 'p') {
            return false;
        }
        //判别是否为anon:libwebview reservation
        if (std::strcmp(path, "[anon:libwebview reservation]") == 0) {
            *start_out = reinterpret_cast<void*>(start);
            *size_out = static_cast<size_t>(static_cast<uint64_t>(end) - static_cast<uint64_t>(start));
            found = true;
            return true;
        }
        return false;
    });
    return found;
}
bool IterateMaps(const MapsEntryCallback& cb, void* args) {
    if (cb == nullptr) {
        return false;
    }
    FILE* fp = nullptr;
    char line[PATH_MAX] = {};
    // 读取maps文件
    if ((fp = std::fopen("/proc/self/maps", "r")) == nullptr) {     
        return false;
    }
    // 经过行读取和解析maps
    while(std::fgets(line, sizeof(line), fp) != nullptr) {
        uintptr_t start = 0;
        uintptr_t end = 0;
        char perm[4] = {};
        int pathnamePos = 0;
        if (std::sscanf(line, "%" PRIxPTR "-%" PRIxPTR " %4s %*x %*x:%*x %*d%n", &start, &end, perm, &pathnamePos) != 3) {
            continue;
        }
        if (pathnamePos <= 0) {
            continue;
        }
        while (std::isspace(line[pathnamePos]) && pathnamePos <= static_cast<int>(sizeof(line) - 1)) {
            ++pathnamePos;
        }
        if (pathnamePos > static_cast<int>(sizeof(line) - 1)) {
            continue;
        }
        size_t pathLen = std::strlen(line + pathnamePos);
        if (pathLen == 0 || pathLen > static_cast<int>(sizeof(line) - 1)) {
            continue;
        }
        char* pathname = line + pathnamePos;
        while (pathLen >= 0 && pathname[pathLen - 1] == '\n') {
            pathname[pathLen - 1] = '\0';
            --pathLen;
        }
        // 将读取到的maps行信息回调
        if (cb(start, end, perm, pathname, args)) {
            return true;
        }
    }
    return false;
}
  1. 开释 libwebview reservation 区域的虚拟内存。
void* reservedSpaceStart = nullptr;
size_t reservedSpaceSize = 0;
if (LocateReservedSpaceByParsingMaps(&reservedSpaceStart, &reservedSpaceSize)) {
    result = true;
}
if (result) {
    //开释虚拟内存
    munmap(reservedSpaceStart , reservedSpaceSize)
}

看到这,你或许想说,这很简单嘛,哪称得上黑科技呢!假如你觉得这个计划很简单,那可就错了,上面仅仅完成了这个优化计划的一半而已。实践上,Android 10 的体系才开端将这一块区域命名为 libwebview reservation,经过比照 9.0 和 10.0 的源码能够看到,10 开端才会调用 prctl 函数将这块区域进行命名。

虚拟内存优化:“黑科技”优化手段
虚拟内存优化:“黑科技”优化手段

关于 Android 10 以下的机型,这块区域在 maps 文件中是匿名的,咱们无法根据 libwebview reservation 这个字段来找出这块区域,所以解析 maps 的计划便不生效了,怎么找到这块区域的地址,就是整个计划的难点

经过上面的源码咱们能够发现,经过 mmap 请求这块内存后,将地址和巨细分别赋值给了 gReservedAddress 和 gReservedSize 这两个静态变量,所以咱们只需求想办法知道 gReservedAddress 的值就能处理这个难点了。获取 gReservedAddress 的值的计划有多种,这儿我主要介绍微信的计划,下面就具体介绍一下这个计划的完成思路以及细节。

咱们首要经过在 Android 源码中大局查找 gReservedAddress,发现它仅被 loader.cpp 这个目标中的办法运用到。

虚拟内存优化:“黑科技”优化手段

分别是 DoReserveAddressSpace 、DoCreateRelroFile 和 DoLoadWithRelroFile 这三个办法。

虚拟内存优化:“黑科技”优化手段
虚拟内存优化:“黑科技”优化手段
虚拟内存优化:“黑科技”优化手段

在 DoCreateRelroFile 和 DoLoadWithRelroFile 办法中,咱们可发现 gReservedAddress 和 gReservedSize 会被封装在 extinfo 结构体中,然后作为入参,调用 android_dlopen_ext 函数,这个函数是 libdl.so 库中的函数。

咱们现已了解了 plt hook 技能,它专门用来 hook 外部库的调用函数。关于 webviewchromiun_loader 这个库来说,android_dlopen_ext 刚好是一个外部函数,所以咱们只需求 经过 plt hook 技能 hook 住 webviewchromiun_loader 这个 so 中的 android_dlopen_ext 函数,就能拿到 extinfo 数据,然后拿到 gReservedAddress 和 gReservedSize 的值了。这儿仍是以 bhook 作为东西来演示具体的代码完成:

//1. 经过bhook,hook住webviewchromiun_loader 这个 so库中的android_dlopen_ext函数   
bytehook_stub_t bytehook_hook_single(
    null,
    "libwebviewchromium_loader.so",
    reinterpret_cast<void*>(android_dlopen_ext),
    reinterpret_cast<void*>(android_dlopen_ext_hook),
    bytehook_hooked_t hooked,
    void *hooked_arg);
/*2. extinfo实践是一个android_dlextinfo结构体,
 可是由于在咱们的hook函数中无法直接运用这个结构体,
 所以咱们按照原来结构体的数据结构结构一个*/
typedef struct {
    uint64_t flags;
    void* reserved_addr;
    size_t reserved_size;
    int relro_fd;
    int library_fd;
    off64_t library_fd_offset;
    struct android_namespace_t* library_namespace;
} android_dlextinfo;
//3. 在hook函数中获取gReservedAddress和gReservedSize的值
static void* android_dlopen_ext_hook(const char* filepath, int flags, void* extinfo) {
    //将extinfo强制转换成android_dlextinfo结构体
    auto android_extinfo = reinterpret_cast<android_dlextinfo*>(extinfo);
    //然后就能直接拿到reserved_addr和reserved_size的值了
    sReservedSpaceStart = android_extinfo->reserved_addr;
    sReservedSpaceSize = android_extinfo->reserved_size;
    //调用原函数
    BYTEHOOK_CALL_PREV();
}
//4. 开释关于的虚拟内存空间
munmap(sReservedSpaceStart ,sReservedSpaceSize )

当咱们做完上面一系列操作后,还有一个问题需求处理,那就是履行 DoCreateRelroFile 或许 DoLoadWithRelroFile 这两个办法,假如这两个办法都不履行的话,android_dlopen_ext 底子就不会被调用到,咱们天然也拿不到想要的数据。这两个办法是在 zygote 进程中被履行的,在咱们自己的运用中,假如不启动 webview 的话,这两个办法也不会被履行,所以咱们需求在运用中经过代码自动履行这两个函数中的一个。

怎么才干履行这两个函数呢?经过剖析源码后会发现,想要自动调用这两个函数其实很简单,由于这两个函数其实能够经过 CreateRelroFile 和 LoadWithRelroFile 这两个 JNI 办法来调用,所以咱们是否只需求直接在 Java 层调用 System.loadLibrary(“webviewchromiun_loader “),然后调用这其中一个 JNI 办法就能够完成意图了呢?

虚拟内存优化:“黑科技”优化手段

答案是不行的。上面的流程看起来似乎没啥问题,但当咱们实践去操作时却无法跑通。首要,webviewchromiun_loader 这个 so 实践现已被加载进咱们运用的进程了,由于这个 so 是被 zygote 进程加载的,而经过 zygote fork 出来的运用进程会同享父进程的虚拟内存中的数据,天然就加载了这个 so。并且,即便咱们想在 Java 层调用 System.loadLibrary 从头加载一次这个 so,也无法做到。这是由于从 Android7.0 版别开端,便不允许运用加载体系的 so 库了,而 webviewchromiun_loader 是一个体系的 so 库,天然无法正常加载。

其次,咱们直接在 Java层调用 nativeLoadWithRelroFile 或许 CreateRelroFile 这两个 native 函数也行不通,由于体系会自动将这个 native 办法加上包名前缀,导致咱们找不到这个 native 办法。

虚拟内存优化:“黑科技”优化手段

那么怎么才干履行这两个函数呢?幸运的是,咱们在 native 层就能调用这两个办法了,这儿以调用 nativeLoadWithRelroFile 函数为例,咱们只需求经过 env->FindClass 拿到这个函数对应的 Java 目标就能履行这个办法了。下面是代码完成流程,尽管很长,但大部分仅仅为了兼容不同操作体系版别而写的兼容性代码,所以完成起来其实并不难。

//1. 在 jni 层调用nativeLoadWithRelroFile函数
static bool LocateReservedSpaceByProbing(JNIEnv* env,
        jint sdk_ver, jobject class_loader) {
    jclass loaderClazz = env->FindClass("android/webkit/WebViewLibraryLoader");
    const char* probeMethodName = "nativeLoadWithRelroFile";
    const char* LS = "java/lang/String";
    const char* LC = "java/lang/ClassLoader";
    jstring jProbeTag = env->NewStringUTF(PROBE_REQ_TAG);
    jstring jFakeRelRoPath = env->NewStringUTF(FAKE_RELRO_PATH);
    jmethodID probeMethodID = nullptr;
    jint probeMethodRet = 0;
    //不同的Android体系版别中,nativeLoadWithRelroFile 函数的入参是不相同的,所以这儿经过遍历,把一切或许的入参状况都调一遍
    for (int i = 1; probeMethodID == nullptr && i <= 4; ++i) {
        switch (i) {
            case 1: {
                const char* typeNameList[] = {LS, LS, LC};
                // 拿到nativeLoadWithRelroFile办法的methodId
                probeMethodID = GetMethodIDByMetaReflect(env, loaderClazz, probeMethodName,
                        typeNameList, NELEM(typeNameList));
                if (probeMethodID != nullptr) {
                    // 履行nativeLoadWithRelroFile办法
                    probeMethodRet = env->CallStaticIntMethod(loaderClazz, probeMethodID,
                            jProbeTag, jFakeRelRoPath, class_loader);
                    env->ExceptionClear();
                }
                break;
            }
            case 2: {
                const char* typeNameList[] = {LS, LS, LS, LC};
                probeMethodID = GetMethodIDByMetaReflect(env, loaderClazz, probeMethodName,
                        typeNameList, NELEM(typeNameList));
                if (probeMethodID != nullptr) {
                    probeMethodRet = env->CallStaticIntMethod(loaderClazz, probeMethodID,
                            jProbeTag, jFakeRelRoPath, jFakeRelRoPath, class_loader);
                    env->ExceptionClear();
                }
                break;
            }
            case 3: {
                const char* typeNameList[] = {LS, LS, LS, LS, LC};
                probeMethodID = GetMethodIDByMetaReflect(env, loaderClazz, probeMethodName,
                        typeNameList, NELEM(typeNameList));
                if (probeMethodID != nullptr) {
                    probeMethodRet = env->CallStaticIntMethod(loaderClazz, probeMethodID,
                            jProbeTag, jProbeTag, jFakeRelRoPath, jFakeRelRoPath, class_loader);
                    env->ExceptionClear();
                }
                break;
            }
            case 4: {
                const char* typeNameList[] = {LS, LS, LS, LS};
                probeMethodID = GetMethodIDByMetaReflect(env, loaderClazz, probeMethodName,
                        typeNameList, NELEM(typeNameList));
                if (probeMethodID != nullptr) {
                    if (LIKELY(sdk_ver >= 23)) {
                        probeMethodRet = env->CallStaticIntMethod(loaderClazz, probeMethodID,
                                jProbeTag, jProbeTag, jFakeRelRoPath, jFakeRelRoPath);
                    } else {
                        probeMethodRet = env->CallStaticBooleanMethod(loaderClazz, probeMethodID,
                                jProbeTag, jProbeTag, jFakeRelRoPath, jFakeRelRoPath)
                                        ? LIBLOAD_SUCCESS : LIBLOAD_FAILURE;
                    }
                    env->ExceptionClear();
                }
                break;
            }
            default: {
                break;
            }
        }
    }
    if (probeMethodID == nullptr) {
        return false;
    }
    if (probeMethodRet == LIBLOAD_SUCCESS) {
        return true;
    } else {
        return false;
    }
}

经过上面的流程,运用就多出了 130M 的可用虚拟内存。

除了上面说到的计划,咱们还有其他的计划能完成同样的意图。比如 gReservedAddress 作为一个未初始化的大局变量,会存放在 bss 段。所以我能够解析 maps 文件,找到 webviewchromiun_loader 这个 so 库的地址并转换成 ELF 格局之后,经过寻觅和遍历 bss 段就能获取到 gReservedAddress 的值了。并且 webviewchromiun_loader 这个 so 库只要 7 个大局变量,所以 bss 段中只要 7 条数据,很容易就能够找到。前面介绍 PLT Hook 的完成原理,其实也和这有点相似,经过拿到 so 的地址并转换成 ELF 格局,之后遍历 dynamic 段修正 got 表的值。

开释虚拟机备份栈空间的虚拟内存

了解了怎么开释预留的 webview 虚拟空间,咱们再来看一下开释虚拟机备份栈空间的虚拟内存这一计划。当咱们剖析 Android5 到 Android7 之间的运用的 maps 文件会发现,Main Space 占用了 1G 的虚拟内存,这 1G 的虚拟内存由两块空间组成:main space 和 main space 1。

// 12c00000-32c00000, 512M
12c00000-12e08000 rw-p 00000000 00:04 14381      /dev/ashmem/dalvik-main space
12e08000-32c00000 ---p 00208000 00:04 14381      /dev/ashmem/dalvik-main space
// 32c00000-52c00000, 512M
32c00000-32c01000 rw-p 00000000 00:04 14382      /dev/ashmem/dalvik-main space 1
32c01000-52c00000 ---p 00001000 00:04 14382      /dev/ashmem/dalvik-main space 1

在《物理内存优化实战》这一章剖析 Java 堆的组成时,咱们也从源码中知道,在 Android 5 到 7 体系中,确实会创立 main space 和 main space 1,创立两个空间的意图是为了用在复制收回这种 GC 算法中,虚拟机在履行复制收回 GC 时,需求将当时在运用的堆内存空间中存活的目标移到另一块干净的堆内存空间中去,流程如下图。

虚拟内存优化:“黑科技”优化手段

并且复制收回这一种 GC 算法,仅当运用处于后台或许 Java 堆内存不足时才会采用,由于这种 GC 算法耗费的时刻和性能都较多。当运用位于前台时,履行的是并发符号铲除这一种 GC 算法。假如咱们禁用复制收回这种 GC 算法,只用符号铲除这一种 GC 算法,就能够将备用的一块 512 M 巨细的虚拟内存开释,然后多出了 512M 可用的虚拟内存了。

此时,你或许会忧虑,制止了复制收回,莫非不会导致 OOM 这些内存问题吗?这个计划其实在抖音经过了很多的线上用户考验,实践验证下来发现,因 Java 堆内存不足导致的 OOM 率并没有提高,并且由于虚拟内存不足导致的 OOM 率甚至有较大的下降。由于没有了复制收回这种 GC 算法,会导致内存碎片增多,但由于咱们的运用并不是常驻进程,用一段时刻就会被体系或许运用杀掉进程了,内存碎片增多带来的影响有限,所以你就放心运用吧。好,接下来咱们就看一下这个计划要怎么完成吧!完成该计划主要有 2 个过程:

  1. 制止虚拟机复制收回这种 GC 机制;

  2. 开释当时没被运用的那一块 main space 空间。

制止虚拟机复制收回这种 GC 机制

咱们先看看怎么制止虚拟机复制收回这种 GC 机制。关于这种需求关闭体系或底层某个功用或逻辑的操作,通用的思路都是剖析这个功用的流程,然后从流程代码中寻觅有什么标志位或许逻辑能够帮助咱们达到意图,然后再进行验证。

咱们直接剖析复制收回这种 GC 机制履行的代码逻辑。它的完成在 heap.cc 这个的目标的 PerformHomogeneousSpaceCompact 办法中。

HomogeneousSpaceCompactResult Heap::PerformHomogeneousSpaceCompact() {
  Thread* self = Thread::Current();
  count_requested_homogeneous_space_compaction_++;
  ScopedThreadStateChange tsc(self, ThreadState::kWaitingPerformingGc);
  Locks::mutator_lock_->AssertNotHeld(self);
  {
    ScopedThreadStateChange tsc2(self, ThreadState::kWaitingForGcToComplete);
    ……
    //从这一点下手,中止该 gc 机制
    if (disable_moving_gc_count_ != 0 || IsMovingGc (collector_type_) ||
!main_space_-> CanMoveObjects ()) {
 return kErrorReject;
}
    if (!SupportHomogeneousSpaceCompactAndCollectorTransitions()) {
      return kErrorUnsupported;
    }
    collector_type_running_ = kCollectorTypeHomogeneousSpaceCompact;
  }
  if (Runtime::Current()->IsShuttingDown(self)) {
    FinishGC(self, collector::kGcTypeNone);
    return HomogeneousSpaceCompactResult::kErrorVMShuttingDown;
  }
  ……
  return HomogeneousSpaceCompactResult::kSuccess;
}

经过剖析履行复制收回 GC 的流程代码,寻觅能够让流程中止的当地,咱们发现 disable_moving_gc_count_ 不为 0 或许 IsMovingGc 为 true 或许 CanMoveObjects 为 true,这三个条件满意一个就会中止复制 GC 的流程,那咱们就能够从这儿下手,看看能不能让条件满意走到 return 中,这样复制收回 GC 就无法正常履行了。由于经过 hook 修正标志位相对来说最容易,所以咱们先从 disable_moving_gc_count_ 这个标志位开端,大局查找这个标志位。

虚拟内存优化:“黑科技”优化手段

查找后咱们发现,只要 heap.cc 这个目标中的 IncrementDisableMovingGC 和 DecrementDisableMovingGC 这两个办法能够改动 disable_moving_gc_count_ 的值。

void Heap::IncrementDisableMovingGC(Thread* self) {
  // Need to do this holding the lock to prevent races where the GC is about to run / running when
  // we attempt to disable it.
  ScopedThreadStateChange tsc(self, ThreadState::kWaitingForGcToComplete);
  MutexLock mu(self, *gc_complete_lock_);
  ++disable_moving_gc_count_;
  if (IsMovingGc(collector_type_running_)) {
    WaitForGcToCompleteLocked(kGcCauseDisableMovingGc, self);
  }
}
void Heap::DecrementDisableMovingGC(Thread* self) {
  MutexLock mu(self, *gc_complete_lock_);
  CHECK_GT(disable_moving_gc_count_, 0U);
  --disable_moving_gc_count_;
}

咱们又接着大局查找 IncrementDisableMovingGC 办法,发现下面两对办法在调用 IncrementDisableMovingGC 和 DecrementDisableMovingGC:

  • GetStringCritical()/ReleaseStringCritical()

  • GetPrimitiveArrayCritical()/ReleasePrimitiveArrayCritical()

这两个办法中 Get 接口能够将 disable_moving_gc_count_ 的值加一,Release 接口能够将 disable_moving_gc_count_ 的值减一。由于虚拟机正常运用时都是成对出现的,假如咱们自动触发 Get 接口而不触发 Release 接口,就能够将 disable_moving_gc_count_ 的值维持在 1,然后制止 PerformHomogeneousSpaceCompact 的履行了。GetxxxCritical 办法的作用是拿到某一个 Java 目标在虚拟机内部内存的指针,为了确保这个指针有用,此时需求禁用复制收回 GC,ReleasexxxCritical 是在运用完这个指针后,使复制收回 GC 恢复可用的状况。

static void* GetPrimitiveArrayCritical(JNIEnv* env, jarray java_array, jboolean* is_copy) {
    ...
    // 目标必须是可移动的
    if (heap->IsMovableObject(s)) {
    ...
    // disable_moving_gc_count_计数器自增
    heap->IncrementDisableMovingGC(soa.Self());
    ...
    return static_cast<jchar*>(s->GetValue());
  }
  static void ReleasePrimitiveArrayCritical(JNIEnv* env, jarray java_array, void* elements, jint mode) {
    ...
    // disable_moving_gc_count_计数器自减
    heap->DecrementDisableMovingGC(soa.Self());
    ...
  }

这样一来计划就出来了,自动调用 GetPrimitiveArrayCritical 办法就能关闭后台复制 GC。并且这两对办法是规范的 JNI 接口,咱们只需求拿到 JNIEnv 就能直接调用该办法,所以完成起来就很简单了。

//创立一个 1 byte的array,作为入参传给 GetPrimitiveArrayCritical 办法
jbyteArray myArr = env->NewByteArray(1);
// 履行 GetPrimitiveArrayCritical 办法            
void *arrPtr = env->GetPrimitiveArrayCritical(myArr, nullptr);

经过上面的流程能够看到,制止虚拟机复制收回完成起来并不难,咱们甚至都不需求知道 GetPrimitiveArrayCritical 或许 GetStringCritical 是干嘛的,也不需求进行 hook 的操作就能完成目标了。

开释当时没被运用的那一块 main space 空间

制止了复制收回 GC 后,咱们就能开释一块 main space 了,由于从这个时分开端,虚拟机只需求运用一块 main space。但运用有两块 main space,咱们需求开释哪一块呢?上面咱们不是经过 NewByteArray 请求了一个 byteArray 目标嘛,那咱们能够查找一个目标落在了哪一块 space 中,然后开释别的一块 space 即可,并且 GetPrimitiveArrayCritical 函数的意图就是回来这个目标的指针,所以咱们直接拿着 arrPtr 指针去查找即可。

auto arrPtrValue = reinterpret_cast<uintptr_t>(arrPtr);
const uintptr_t space1Start = 0x12c00000;
const uintptr_t space1End = 0x32c00000;
const uintptr_t space2Start = 0x32c00000;
const uintptr_t space2End = 0x52c00000;
if (arrPtrValue >= space1Start && arrPtrValue < space1End) {
    munmap(reinterpret_cast<void *>(space2Start), space2Size)
} else if (arrPtrValue >= space2Start && arrPtrValue < space2End) {
    munmap(reinterpret_cast<void *>(space1Start), space1Size)
}

这儿为了演示简单,我将两块 main space 的空间地址都写死了,尽管这两块空间地址根本都是固定的,但为了稳定性,真正在线上运用时,我仍是主张解析 maps 文件后再确定这两块 space 的地址。

小结

这一章中的咱们介绍了两种“黑科技”手法来优化虚拟内存,最终仍是用一张图简单总结一下:

虚拟内存优化:“黑科技”优化手段

一般只要常规的优化计划都运用完,仍然还有很多由于虚拟内存导致的程序溃散时,咱们才会运用这些非常规的计划,可是作为一名技能人员,对技能的追求应该是无止境的,所以即便咱们用上这些计划的机会不多,可是仍然需求知道这些计划的技能点及原理,并能从这些计划中吸收优化的思路,触类旁通,扩展出更多的用法。