从这一章开端,咱们就进入了内存优化的实战环节。这一环节分为三部分: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 堆的内存概况:
经过 maps 文件能够看到,12c00000 到 32c00000 的地址规模刚好是 512M 巨细,归于 MainSpace。从 6f5ac000 到 717f5000 归于 ImageSpace,共 30 多 M,寄存了各个体系相关的库。紧跟着 ImageSpace 的便是 ZygoteSpace、Non Moving Space 和 Large Object Space。
尽管 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:
-
DlMallocSpace:经过 dlmalloc 内存分配器来请求和开释内存,这是一个很出名的内存分配器,网上有许多的材料介绍,这儿就不具体介绍了。
-
MainMallocSpace:经过谷歌开发的 rosalloc 内存分配办理器来请求和开释内存。rosalloc 的用法比 dlmalloc 要复杂得多,而且还需求 ART 虚拟机中其他模块进行配合。可是分配的作用要比 dlmalloc 更好,而且多线程下体现更好。
-
BumpPointerSpace:很简略的内存分配算法,依照次序分配,类似于链表,简略呈现内存碎片,所以只用在线程本地存储或许存活周期很长的目标空间上。
-
RegionSpace:RegionSpace 的内存分配算法比 BumpPointerSpace 略微高级一点。它先将内存资源划分红一个个固定巨细(由 kRegionSize 指定,默以为 1MB)的内存块,每一个内存块由一个 Region 目标表示,进行内存分配时,先找到满足要求的 Region,然后从这个 Region 中分配资源。
-
FreeListSpace/LargeObjectMapSpace:经过 list 或许 map 来分配和开释内存,比 BumpPointerSpace 更简略。
这儿介绍的 Space 有点多,记不住也不要紧,有个大致印象即可。在源码中也能够看到 MainSpace 会依据 GC 收回器类型这个条件判别,有不同的创立办法,而且挑选是放入 RegionSpace、BumpPointerSpace 仍是 MainMallocSpace 中,这儿判别规矩如下:
- Android5.x~7.x:会创立姓名为 “main space” 和 “main space 1″,巨细都为 512M 的空间,而且 main space 和 main space 1 会经过 MainMallocSpace 来保护和办理,实践只会运用其间的一个空间,只需当履行 GC 的时分,另一个空间才派上用场。此刻,GC 收回器会将前面所运用的空间中的存活目标全部移动到另一个空间来。
- 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 种办法。
-
显现加载:运用 Class.forName() 或许 ClassLoader.loadClass 办法加载目标。
-
隐式加载:运用 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;
}
这个接口的逻辑比较简略:
-
挑选适宜的 GarbageCollector(垃圾收回器),并设置好这个 collector 的环境,如 kCollectorTypeSS(半空间收回)就会设置好 FromSpace 和 ToSpace。
-
接着调用履行 collector->Run 接口,collector 会履行目标的收回战略。
不同的 GarbageCollector 对应了不同的 GC 算法,这一块的知识比较庞大,超出了该华章的内容,不做具体的介绍了,只简略介绍一下 GarbageCollector 是怎样判别一个目标是否可收回的。
关于 ART 虚拟机的垃圾收回器来说,是经过可达性剖析来判别一个目标是否能够被收回。GarbageCollector 会对 space 中的每一个目标的引证链进行剖析,假如这个目标的引证链最终被 GC Root 持有,就阐明这个目标不可收回。不然,就能够收回。如下图所示,目标 object 5、object 6、object 7 尽管互有相关,可是它们没有被 GC Roots 持有, 因而会被判定为可收回的目标。
GC Root 有下面几项:
-
栈中引⽤的目标:比方运用中主线程的 Handler,它是不会退出的,假如在 Handler 中持有了一个目标,那么这个目标便是被主线程栈所引证的目标,归于 GC Root 可达。这样一来,在 GarbageCollector 履行 GC 时就不会开释这个目标。
-
静态变量、常量引⽤的目标:被静态变量运用的目标也是归于 GC Root 可达,只需咱们手动置为 null 才干开释这个目标。
-
本地⽅法栈 Native ⽅法引⽤的目标:经过 JNI 调用,传递到 Native 层并被 Native 的函数引证的目标。
优化计划
经过上面临 Java 堆的原理的讲解,咱们了解了这 2 个知识点:
-
Java 堆的空间是有限的,加起来只需 512M;
-
只需在堵截 Java 目标和 GC Root 的相关后,虚拟机的 GC 机制才会收回该目标。
依据这 2 个底层的知识点,咱们就能够总结出 Java 堆内存优化的 3 条办法论:
-
削减加载进程 Java 堆的数据
-
及时整理加载进 Java 堆的数据
-
添加 Java 堆空间可用巨细
Java 堆内存的一切优化计划,都是依据这 3 条,下面咱们就来看看具体能够扩展出哪些优化计划吧!
削减加载进 Java 堆的数据
想要削减加载进 Java 堆的数据,咱们能够经过削减缓存巨细、按需加载数据和搬运数据,这几种办法来实现。
办法 1:削减缓存巨细
咱们知道,事务开发不可避免的需求运用到许多缓存,缓存能经过空间换时间的办法提高事务的体会。因而,优化缓存就会和事务的体会产生冲突。这个时分,咱们就需求归纳评价事务的体会、OOM 率、事务运用频率等多方面因素,来尽量削减缓存的巨细。具体该怎样操作呢?就拿 LruCache 来说,它是咱们运用最多的缓存容器之一,要优化 LruCache 这类缓存,咱们需求考虑这几点:
-
这个 Cache 的巨细是多少?
-
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 种:
-
将 Java 堆的数据搬运到 Native 中。
-
将当时进程中 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 虚拟机的堆内存……