内存优化:Java堆内存优化

2,624 阅读21分钟

从这一章开始,我们就进入了内存优化的实战环节。这一环节分为三部分:Java 堆内存 (Java Heap) 优化、Native 内存优化和虚拟内存优化。通过前面对基础知识的学习我们也知道,这三个部分是 App 内存的主要组成,那对主要组成各个击破,便是一个完整的内存优化流程了。又因为Java 堆内存的优化是 App 内存优化中最重要的一个部分,所以我们先讲 Java 堆的内存优化。

我们知道,Java 堆内存可用空间有限,目前大部分机型只有 512M,小部分低端机甚至只有 256M。当 Java 堆内存不足时,虚拟机会不断进行 GC,影响体验流畅性,GC 后如果依然没有足够的可用空间,便会触发 OOM 导致程序 Crash。

为了能对 Java 堆进行全面且深入的优化,这一章节我们会先从基础知识出发,了解 Java 堆的组成,以及 Java 堆内存的申请和释放流程,然后进入优化实战,实战部分会基于基础知识建立的方法论,形成一套体系的优化方案。建立优化的方法论,我们才能以不变应万变,完成各种业务和各种环境下的内存优化。下面就开始这一章的学习吧!

Java 堆组成

当 Android 虚拟机启动时,便会创建 Java 堆,后续所有 Java 对象所需要的内存都会从这个堆中分配,所以我们先来说说 Java 堆的组成。Java 堆由 ImageSpace、ZygoteSpace、Non moving space、LargeObjectSpace、MainSpace 这五个部分组成,下面是对每个组成的说明。

  • ImageSpace:用来存放系统库的对象,大小不固定。

  • ZygoteSpace:存放 Zygote 进程在启动过程中预加载和创建的各种对象,应用进程为 2M 左右,Zygote 进程为 64M,挨着 Image Space。

  • Non moving space:如果非 zygote 或 Native 进程启动时,便会将 ZygoteSpace 切分出 62M 左右,当做 non moving space,用来存放一些生命周期较长的对象。

  • LargeObjectSpace:用来存放大对象,大对象是大于 12K 的基本类型数组和 String 对象。

  • MainSpace:大对象以外的大部分的 Java 对象都会存放在这块空间。

我们可以通过上一章中的 maps 文件,来看看 Java 堆的内存详情: image.png

通过 maps 文件可以看到,12c00000 到 32c00000 的地址范围刚好是 512M 大小,属于 MainSpace。从 6f5ac000 到 717f5000 属于 ImageSpace,共 30 多 M,存放了各个系统相关的库。紧跟着 ImageSpace 的便是 ZygoteSpace、Non Moving Space 和 Large Object Space。

image.png

虽然 MainSpace 和 Large Object Space 都分配了 512M 的虚拟内存,但你千万不要被迷惑了。512M 只是这两个空间理论上可申请的最大内存,而在真正申请内存时,虚拟机会用 num_bytes_allocated_ 这个标志位来记录已经分配的内存,不管是在 MainSpace 还是 Large Object Space 中分配的空间,都会通过这个标志位累加记录下,如果这个值超过了阈值(标志位为 growth_limit_),就会抛出 OOM,虚拟机抛出 OOM 的代码如下。

inline bool Heap::IsOutOfMemoryOnAllocation(AllocatorType allocator_type,
                                            size_t alloc_size,
                                            bool grow) {
  size_t old_target = target_footprint_.load(std::memory_order_relaxed);
  while (true) {
    size_t old_allocated = num_bytes_allocated_.load(std::memory_order_relaxed);
    //new_footprint = 已经申请的内存大小+需要申请的内存大小
    size_t new_footprint = old_allocated + alloc_size;
    //new_footprint大于growth_limit_就会认为是OOM
    if (UNLIKELY(new_footprint <= old_target)) {
      return false;
    } else if (UNLIKELY(new_footprint > growth_limit_)) {
      //growth_limit_就是Java Heap的大小,超过了这个限制,就认为了是OOM
      return true;
    }
    ……
  }
}

这里的 growth_limit_ 就是我们所说的 Java 堆内存的大小,目前大部分机型都是 512M,所以后文中再说到 Java 堆的大小都统一默认为 512M。

了解了 Java 堆的组成,我们再通过源码了解一下 Java 堆的创建流程,我们通过精简后的 Java 堆构造函数实现:(源码地址:heap.cc)

static const char* kMemMapSpaceName[2] = {"main space", "main space 1"};
static const char* kRegionSpaceName = "main space (region space)"

Heap::Heap(……){

  ……
  std::vector<std::unique_ptr<space::ImageSpace>> boot_image_spaces;
  // 创建ImageSpace,用来加载boot.oat
  if (space::ImageSpace::LoadBootImage(……,&boot_image_spaces,……)) {
    ……
  } else {
    ……
  }

  MemMap main_mem_map_1;
  MemMap main_mem_map_2;

  std::string error_str;
  MemMap non_moving_space_mem_map;
  if (separate_non_moving_space) {
    // 创建ZygoteSpace虚拟内存,大小为64M
    const char* space_name = is_zygote ? kZygoteSpaceName : kNonMovingSpaceName;
    if (heap_reservation.IsValid()) {
      non_moving_space_mem_map = heap_reservation.RemapAtEnd(
          heap_reservation.Begin(), space_name, PROT_READ | PROT_WRITE, &error_str);
    } else {
      non_moving_space_mem_map = MapAnonymousPreferredAddress(
          space_name, request_begin, non_moving_space_capacity, &error_str);
    }
    request_begin = kPreferredAllocSpaceBegin + non_moving_space_capacity;
  }
 
  // 前台gc不是并发复制回收时,会创建两个space,5.x~7.x的系统采用这种gc算法
  if (foreground_collector_type_ != kCollectorTypeCC) {
    if (separate_non_moving_space || !is_zygote) {
    //3. 创建name为“main space”的space的虚拟内存
      main_mem_map_1 = MapAnonymousPreferredAddress(
          kMemMapSpaceName[0], request_begin, capacity_, &error_str);
    } else {
     ……
    }
  
  }
  //同样是5.x~7.x的系统采用这种gc算法
  if (support_homogeneous_space_compaction ||
      background_collector_type_ == kCollectorTypeSS ||
      foreground_collector_type_ == kCollectorTypeSS) {
    //4. 创建name为“main space 1”的space的虚拟内存
    main_mem_map_2 = MapAnonymousPreferredAddress(
        kMemMapSpaceName[1], main_mem_map_1.End(), capacity_, &error_str);
  }

  if (separate_non_moving_space) {
    const size_t size = non_moving_space_mem_map.Size();
    const void* non_moving_space_mem_map_begin = non_moving_space_mem_map.Begin();
    //通过DlMallocSpace来管理ZygoteSpze
    non_moving_space_ = space::DlMallocSpace::CreateFromMemMap(std::move(non_moving_space_mem_map),
                                                               "zygote / non moving space",
                                                               kDefaultStartingSize,
                                                               initial_size,
                                                               size,
     ……
  }
  // 前台gc为并发复制回收,8.0及以上系统采用的gc算法
  if (foreground_collector_type_ == kCollectorTypeCC) {
    //创建一个容量为capacity_ * 2,即1g的space,虽然这里创建了1g,但是可用的只有512,另外一半是GC时,用于对象移动的
    MemMap region_space_mem_map =
        space::RegionSpace::CreateMemMap(kRegionSpaceName, capacity_ * 2, request_begin);
   
    ……
  } else if (IsMovingGc(foreground_collector_type_)) {
    // 通过BumpPointerSpace管理前面创建的main space和main space
    bump_pointer_space_ = space::BumpPointerSpace::CreateFromMemMap("Bump pointer space 1",
                                                                    std::move(main_mem_map_1));
   
    temp_space_ = space::BumpPointerSpace::CreateFromMemMap("Bump pointer space 2",
                                                            std::move(main_mem_map_2));
   
  } else {
    //通过MainMallocSpace来管理前面创建的main space和main space
    CreateMainMallocSpace(std::move(main_mem_map_1), initial_size, growth_limit_, capacity_);
    if (main_mem_map_2.IsValid()) {
      const char* name = kUseRosAlloc ? kRosAllocSpaceName[1] : kDlMallocSpaceName[1];
      main_space_backup_.reset(CreateMallocSpaceFromMemMap(std::move(main_mem_map_2),
                                                           initial_size,
                                                           growth_limit_,
                                                           capacity_,
                                                           name,
                                                           /* can_move_objects= */ true));
        ……
    }
  }

  // 申请并创建LargeObjectSpace
  if (large_object_space_type == space::LargeObjectSpaceType::kFreeList) {
    large_object_space_ = space::FreeListSpace::Create("free list large object space", capacity_);
  } else if (large_object_space_type == space::LargeObjectSpaceType::kMap) {
    large_object_space_ = space::LargeObjectMapSpace::Create("mem map large object space");
  } else {
    ……
  }
  ……
  
}

在 Java 堆的创建流程中出现了很多 Space 的创建,但我们不要被绕进去了,只需要记住:所有的 Space 创建,都是先通过 mmap 申请一块匿名内存,然后将这块内存放入对应的 Space 空间中进行管理。比如 ZygoteSpace 的创建,会先通过 CreateFromMemMap 函数创建一个名字为 zygote,大小为 64M 的匿名内存,然后将这一块内存放入 DlMallocSpace 管理。下面简单介绍一下用来管理申请内存的 Space:

  1. DlMallocSpace:通过 dlmalloc 内存分配器来申请和释放内存,这是一个很出名的内存分配器,网上有大量的资料介绍,这里就不详细介绍了。

  2. MainMallocSpace:通过谷歌开发的 rosalloc 内存分配管理器来申请和释放内存。rosalloc 的用法比 dlmalloc 要复杂得多,而且还需要 ART 虚拟机中其他模块进行配合。但是分配的效果要比 dlmalloc 更好,并且多线程下表现更好。

  3. BumpPointerSpace:很简单的内存分配算法,按照顺序分配,类似于链表,容易出现内存碎片,所以只用在线程本地存储或者存活周期很长的对象空间上。

  4. RegionSpace:RegionSpace 的内存分配算法比 BumpPointerSpace 稍微高级一点。它先将内存资源划分成一个个固定大小(由 kRegionSize 指定,默认为 1MB)的内存块,每一个内存块由一个 Region 对象表示,进行内存分配时,先找到满足要求的 Region,然后从这个 Region 中分配资源。

  5. FreeListSpace/LargeObjectMapSpace:通过 list 或者 map 来分配和释放内存,比 BumpPointerSpace 更简单。

这里介绍的 Space 有点多,记不住也没关系,有个大致印象即可。在源码中也可以看到 MainSpace 会根据 GC 回收器类型这个条件判断,有不同的创建方式,并且选择是放入 RegionSpace、BumpPointerSpace 还是 MainMallocSpace 中,这里判断规则如下:

  1. Android5.x~7.x:会创建名字为 "main space" 和 "main space 1",大小都为 512M 的空间,并且 main space 和 main space 1 会通过 MainMallocSpace 来维护和管理,实际只会使用其中的一个空间,只有当执行 GC 的时候,另一个空间才派上用场。此时,GC 回收器会将前面所使用的空间中的存活对象全部移动到另一个空间来。
  1. Android8.0 及以上:创建 main space (region space),并且通过 RegionSpace 来维护和管理。

Java 堆划分了多个 Space,每个 Space 存放对象的性质都不一样,比如系统对象的存活周期非常长,而有些应用对象的存活周期非常短,并且不同的 GC 算法对空间的要求也不一样,标记清楚只需要一个空间,但是复制回收就需要两个空间,所以在创建 Java 堆的过程中,才会出现了那么多 Space,不同的 Space 对内存的申请和释放都不一样,适用的场景也不一样。

Java 对象申请及释放

虽然 Java 堆的组成很多,但实际上应用代码中的 Java 对象几乎只会存放 MainSpace 和 LargeObjectSpace 这两个空间中,其他的空间都是给系统库或者 Zygote 使用的,所以下面我们就来看看 Java 对象所需的内存是如何在 MainSpace 和 LargeObjectSpace 进行申请和释放的。

申请流程

在 Java 中创建并加载一个对象有 2 种方式。

  1. 显示加载:使用 Class.forName() 或者 ClassLoader.loadClass 方式加载对象。

  2. 隐式加载:使用 new,反射或者访问静态变量或者函数加载对象。

这 2 种方式到最后都会调用 AllocObjectWithAllocator 接口到 Java 堆中申请内存,我们直接看这个接口申请内存的代码逻辑(完整的代码可以看:heap-inl.h)。

inline mirror::Object* Heap::AllocObjectWithAllocator(Thread* self,
                                                      ObjPtr<mirror::Class> klass,
                                                      size_t byte_count,
                                                      AllocatorType allocator,
                                                      const PreFenceVisitor& pre_fence_visitor) {
                                                      
  ……

  //1.检测是否是LargeObject,如果是则在LargeObjectSpace申请内存
  if (kCheckLargeObject && UNLIKELY(ShouldAllocLargeObject(klass, byte_count))) {
    obj = AllocLargeObject<kInstrumented, PreFenceVisitor>(self, &klass, byte_count,
                                                           pre_fence_visitor);
    if (obj != nullptr) {
      return obj.Ptr();
    }
    ……
  }

  ……
  //2. 非LargeObject,则调用TryToAllocate在mainspace申请内存
  obj = TryToAllocate<kInstrumented, false>(self, allocator, byte_count, &bytes_allocated,
                                          &usable_size, &bytes_tl_bulk_allocated);
  if (UNLIKELY(obj == nullptr)) {
    //3. 申请失败的情况下则调用gc后再次申请
    obj = AllocateInternalWithGc(self,
                                 allocator,
                                 kInstrumented,
                                 byte_count,
                                 &bytes_allocated,
                                 &usable_size,
                                 &bytes_tl_bulk_allocated,
                                 &klass);
    ……        
  }
  ……
  return obj.Ptr();
}

上面代码只保留了主逻辑,通过注释可以看到,虚拟机为 Java 对象申请内存时,会先检测是否是大对象:如果是大对象,则会调用 AllocLargeObject 在 LargeObjectSpace 中申请;如果不是,则调用 TryToAllocate 在 MainSpace 中申请。如果申请失败,就会执行 GC 后继续申请。

什么是大对象呢?通过 ShouldAllocLargeObject 判断接口可以看到,申请的内存大小大于 3页,且是基本类型数组或者字符串便认为是大对象。

inline bool Heap::ShouldAllocLargeObject(ObjPtr<mirror::Class> c, size_t byte_count) const {
  return byte_count >= large_object_threshold_ && (c->IsPrimitiveArray() || c->IsStringClass());
}

释放流程

了解了对象的申请流程,我们再来看对象的释放流程。在 Java 堆中申请内存时,如果申请失败,或者申请完毕后超过了阈值,就会执行 GC,在上面申请流程中我们可以看到申请内存失败后,会调用 AllocateInternalWithGc 接口去重新申请,这个接口会调用 CollectGarbageInternal 接口进行 GC。(源码连接:heap.cc)

collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type,
                                               GcCause gc_cause,
                                               bool clear_soft_references,
                                               uint32_t requested_gc_num) {
  ……

  collector::GarbageCollector* collector = nullptr;
  //1. 选择对应的垃圾回收器
  if (compacting_gc) {
    switch (collector_type_) {
      case kCollectorTypeSS:
        semi_space_collector_->SetFromSpace(bump_pointer_space_);
        semi_space_collector_->SetToSpace(temp_space_);
        semi_space_collector_->SetSwapSemiSpaces(true);
        collector = semi_space_collector_;
        break;
      case kCollectorTypeCC:
        collector::ConcurrentCopying* active_cc_collector;
        if (use_generational_cc_) 
          active_cc_collector = (gc_type == collector::kGcTypeSticky) ?
                  young_concurrent_copying_collector_ : concurrent_copying_collector_;
          active_concurrent_copying_collector_.store(active_cc_collector,
                                                     std::memory_order_relaxed);
          collector = active_cc_collector;
        } else {
          collector = active_concurrent_copying_collector_.load(std::memory_order_relaxed);
        }
        break;
      default:
        
    }
    if (collector != active_concurrent_copying_collector_.load(std::memory_order_relaxed)) {
      temp_space_->GetMemMap()->Protect(PROT_READ | PROT_WRITE);
      if (kIsDebugBuild) {
        // Try to read each page of the memory map in case mprotect didn't work properly b/19894268.
        temp_space_->GetMemMap()->TryReadable();
      }
    }
  } else if (current_allocator_ == kAllocatorTypeRosAlloc ||
      current_allocator_ == kAllocatorTypeDlMalloc) {
    collector = FindCollectorByGcType(gc_type);
  } else {
    LOG(FATAL) << "Invalid current allocator " << current_allocator_;
  }

  //2. 执行GC
  collector->Run(gc_cause, clear_soft_references || runtime->IsZygote());
  ……
  return gc_type;
}

这个接口的逻辑比较简单:

  1. 选择合适的 GarbageCollector(垃圾回收器),并设置好这个 collector 的环境,如 kCollectorTypeSS(半空间回收)就会设置好 FromSpace 和 ToSpace。

  2. 接着调用执行 collector->Run 接口,collector 会执行对象的回收策略。

不同的 GarbageCollector 对应了不同的 GC 算法,这一块的知识比较庞大,超出了该篇章的内容,不做详细的介绍了,只简单介绍一下 GarbageCollector 是如何判断一个对象是否可回收的。

对于 ART 虚拟机的垃圾回收器来说,是通过可达性分析来判断一个对象是否可以被回收GarbageCollector 会对 space 中的每一个对象的引用链进行分析,如果这个对象的引用链最终被 GC Root 持有,就说明这个对象不可回收。否则,就可以回收。如下图所示,对象 object 5、object 6、object 7 虽然互有关联,但是它们没有被 GC Roots 持有, 因此会被判定为可回收的对象。

image.png

GC Root 有下面几项:

  1. 栈中引⽤的对象:比如应用中主线程的 Handler,它是不会退出的,如果在 Handler 中持有了一个对象,那么这个对象就是被主线程栈所引用的对象,属于 GC Root 可达。这样一来,在 GarbageCollector 执行 GC 时就不会释放这个对象。

  2. 静态变量、常量引⽤的对象:被静态变量应用的对象也是属于 GC Root 可达,只有我们手动置为 null 才能释放这个对象。

  3. 本地⽅法栈 Native ⽅法引⽤的对象:通过 JNI 调用,传递到 Native 层并被 Native 的函数引用的对象。

优化方案

通过上面对 Java 堆的原理的讲解,我们了解了这 2 个知识点:

  1. Java 堆的空间是有限的,加起来只有 512M;

  2. 只有在切断 Java 对象和 GC Root 的关联后,虚拟机的 GC 机制才会回收该对象。

基于这 2 个底层的知识点,我们就可以总结出 Java 堆内存优化的 3 条方法论:

  1. 减少加载进程 Java 堆的数据

  2. 及时清理加载进 Java 堆的数据

  3. 增加 Java 堆空间可用大小

Java 堆内存的所有优化方案,都是基于这 3 条,下面我们就来看看具体可以扩展出哪些优化方案吧!

减少加载进 Java 堆的数据

想要减少加载进 Java 堆的数据,我们可以通过减少缓存大小、按需加载数据和转移数据,这几种方式来实现。

方法 1:减少缓存大小

我们知道,业务开发不可避免的需要使用到很多缓存,缓存能通过空间换时间的方式提升业务的体验。因此,优化缓存就会和业务的体验产生冲突。这个时候,我们就需要综合评估业务的体验、OOM 率、业务使用频率等多方面因素,来尽量减少缓存的大小。具体该怎么操作呢?就拿 LruCache 来说,它是我们使用最多的缓存容器之一,要优化 LruCache 这类缓存,我们需要考虑这几点:

  1. 这个 Cache 的大小是多少?

  2. Cache 中的数据何时清理?

先看第一个问题,LruCache 构造函数中需要传入这个 Cache 的大小,网上很多文章都默认传入最大可用堆内存的八分之一,这样设置 size 其实并不太准确。我们需要评估业务的重要性和业务使用频率,如果是重要并且使用频率高的业务缓存,这里的 size 多设置一些也能接受。同时,我们还需要评估当前的机型,如果是只有 256M 的可用堆内存的低端机,这里设置为八分之一的大小,也就是 32M 就有点多了,对整个应用的稳定性会产生较大的影响。那么到底应该设置多少呢?我建议综合机型、业务充分考虑后再设置,这里没有绝对正确的标准,需要应用的开发者自己去考虑清楚。

再来看第二个问题,Cache 中的数据何时清理呢?LruCache 自带了缓存清理的策略,这个缓存的容量满了之后,就会清理最后一个最近未被使用的数据。除了这个清理策略之外,我们可以再多增加一些策略,比如 Java 堆内存使用达到阈值(如 80%)就清理这个 LruCache 的数据。

除了 LruCache 之外,常用的集合容器还有 List、Map 等。在做内存优化时,我们也都需要考虑它们运行时所占用的内存会有多大,是否会出现过大导致的内存异常问题。

方法 2:按需加载数据

按需加载指的是,只有当我们真正需要用到的时候再去加载数据,Android 系统中用到了大量的按需加载策略。比如,我们在前面章节提到的 mmap 函数申请的其实是虚拟内存,只有真正需要存放数据时才会去分配并映射物理内存。在应用开发中,使用按需加载数据策略能节约不少 Java 堆。

在一个中大型的项目中,我们会注册各种全局服务,通过服务的接口将各个业务的能力暴露出去,达到解耦的目的。这个场景下,我们完全可以在真正使用的时候,再进行服务的注册。同时,应用启动时的各种预加载,也需要思考是否有预加载的必要性。

方法 3:转移数据

我们知道,Java 堆的大小是有限制的,可用大小只有 512M。那如果我们将需要放入 Java 堆的数据转移到其它地方,是不是就可以突破 512M 的限制,使用整个手机的可用内存了呢?确实可以这样做,转移数据的方式有 2 种:

  1. 将 Java 堆的数据转移到 Native 中。

  2. 将当前进程中 Java 堆的数据转移到其他进程中。

我们先来看第 1 种:将 Java 堆的数据转移到 Native 中。 Android8.0 以前,Bitmap 是算入 Java 堆的空间的,8.0 及之后的版本,Bitmap 却被放入了 Native 中。这一策略极大地增加了 Android8.0 版本之后 Java 堆的可用空间。Fresco 这款图片加载在 Android5.0 以下的系统中,就是将 Bitmap 的创建,放在了 Ashmem 匿名共享内存中。Android 系统或者 Frsco 框架都是通过将原本存放 Java 堆的数据转移到 Native 中这一思想来优化 Java 堆内存的,而我们在做 Java 堆内存优化时也可以使用这样的思路。比如说,我们也可以将需要读取大数据的业务下沉到 Native 层去做,包括网络库、业务的数据处理等。即使是 Bitmap,在Android8.0 以下的版本中,也是可以通过“黑科技”手段转移到 Native 中的,但需要 Native Hook 技术,就不在这里展开讲了,后面会详细讲解的。

接着,我们再来看看怎么将当前进程中 Java 堆的数据转移到其他进程中。 每个进程的 Java 堆都是固定的,但是我们可以将应用设计成多进程模型,这样就有多个 Java 堆空间可用了。我们可以选择将比较独立的业务放在子进程中,如需要小程序、Flutter、RN、WebView 等容器承载的业务,当我们把这些业务放在独立的子进程后,不仅可以减轻主进程中 Java 堆的大小,还能降低主进程中因为这些业务导致的性能问题,如内存泄漏、Crash 等。

及时清理加载进 Java 堆的数据

那么,基于第二个方法论又可以扩展出哪些优化方案呢?虽然虚拟机会回收堆中不再使用的内存,但也需要我们将对象的 GC Root 的连接切断。那什么情况下我们需要切断对象和 GC Root 的联系呢?主要有两种情况:业务结束时和内存不足时

在大部分情况下,当一个 Activity 执行 destory 后,我们便认为这个业务结束了,这个时候, ActivityThread 这个 GC Root 便不会再持有这个 Activity,那当虚拟机执行 GC 时,这个 Activity 因为没有被 GC Root 持有,就会被回收释放掉。

但现实情况是,我们一般还是会在代码中持有这个 Activity 的 context,并且不会主动释放。这样一来, Activity 即使 destory了,Java 堆的 GC 机制也不会回收这个 Activity 以及这个 Activity 所持有的对象,因为虚拟机会认为这个 Activity 还在被使用,不能回收。因此,当 Activity 结束时,我们需要主动 Activity 的 GC Root。

在开发中,我们可以把持有 Activity context 的地方改成 Application context,如果不能持有 Application 的 context,也应该以弱引用持有该 Activity。我们可以通过 LeackCanary 或者 hprof 来分析 Activity 的 context 被哪些对象持有。LeackCanary 和 Hprof 的使用和分析在网上有很多详细的教程,就不在这儿详细讲了。

在 Activity destory 时,除了需要清除其他地方对这个 Activity 的引用,还要清除全局变量或主线程的成员变量中,所持有的与该业务相关的数据:如全局的缓存、单例中的缓存等,这些清理操作都是在 onDestory 会调中进行。

除了业务结束时,内存不足时我们也需要切断非必要对象和 GC Root 的联系。 当 Java 堆内存不足时,我们需要对应用中的缓存进行一次清理,这样能减少 OOM。那如何才能知道 Java 堆不足呢?这就需要增加一个检测的机制了,我们可以开启一个独立的子线程,然后每隔一定的频率检测一次。我们在之前的章节中已经知道获取 Java 堆信息的方式,可以通过 meminfo 来获取,也可以通过 Runtime.getRuntime() 的接口来获取,在这个场景下,用 Runtime.getRuntime() 才是合适的,因为性能的损耗最小,并且我们也只需要知道 Java 堆的最大内存和已经使用的内存。

//获取当前虚拟机实例的内存使用上限
Runtime.getRuntime().maxMemory()

//获取当前已经申请的内存  
Runtime.getRuntime().totalMemory()

当我们拿到最大可使用内存和已经使用的 Java 堆内存后,把它们简单相除,如果超过我们设定的阈值,就通过回调通知各个业务、缓存、单例对象等进行缓存的清理工作。

增加 Java 堆的大小

至于如何增加 Java 堆的可用大小,我们似乎没有太多可落地的方案,毕竟 Java 堆的大小只有 Android 的系统才能调节,这是手机厂商的工作。从最开始的 256M,到现在普遍的 512M,未来可能会更大。但如果我们对 Art 虚拟机原理掌握得足够深入,完全可以通过 Hook Art 虚拟机这一黑科技手段来扩展 Java 堆的大小。

前面我们提到过,堆空间总的可用大小是 512M,每当我们在堆中申请内存后,就会将 num_bytes_allocated_ 这个变量加上申请的这块内存大小,当 num_bytes_allocated_ 大于 512M 的时候,就会发生 OOM,但我们在分析源码时,也发现了 MainSpace 和 LargeObjectSpace 这两个 space 的理论上限都是 512M,如果我们通过 hook,在申请 LargeObjectSpace 时,不将增加的内存记录在num_bytes_allocated_ 变量上,那么我们就可以使用 512M 的 MainSpace + 512M 的 LargeObjectSpace,总共 1G 的 Java 堆。

字节自研的 mSponse 就是采用了该方案,因为这部分的技术较复杂,所以我们简单了解下就可以了,感兴趣的可以通过《拯救OOM!字节自研 Android 虚拟机内存管理优化黑科技 mSponge》深入了解(想要看懂这篇文章,需要掌握 Native Hook 的相关技术知识,但这偏离了我们这一章的主题,就不在这儿展开说了,在后面 Native 内存优化中会详细讲解 Native Hook 这一知识点)。

同样,我们也可以通过 hook 是否 oom 这个判断方法,来达到 LargeObjectSpace 和 MainSpace 的可用上限。

小结

想要优化 Java 堆内存,熟悉一些具体的堆内存优化方案并不是最重要的,最重要的是我们能掌握底层的理论以及优化的方法论。

首先,我们要知道 Java 堆是什么,事实上 Java 层主要是在 MainSpace 和 LargeObjectSpace 中申请内存空间的。

其次,我们要知道 Java 对象的申请和释放的流程。

  • 申请流程:我们可以用显示加载或者隐式加载来创建一个对象

  • 释放流程:在 Java 堆中申请内存时,如果申请失败,或者申请完毕后超过了阈值,会调用 AllocateInternalWithGc 接口去重新申请,而这个接口会调用 CollectGarbageInternal 接口进行 GC。

最后,在这些原理性的知识加持下,我们总结出了对 Java 堆进行优化的三条通用的方法论:减少缓存大小、按需加载数据和转移数据。并且,基于这几条方法论,我们又延伸出了一系列优化方案。

优化方案非常多,一节课肯定介绍不完的,但只要我们能够充分理解基于 Java 堆内存优化的方法论,就可以扩展出更多的优化方案。这样一来,我们不仅可以优化基于 Android 虚拟机 Java 堆内存,也可以去优化 V8 虚拟机的堆内存,去优化 JVM 环境下优化堆内存,去优化 Python 虚拟机的堆内存……