ART虚拟机中,堆(Heap)空间负责进行对象内存的分配。堆的大小也决定了一个应用可使用的内存大小。当应用内存量超过了Android系统给定的最大堆内存限制时,就会产生OOM。
我们接下来逐步分析,决定堆内存大小的因素;以及伴随GC过程中的堆大小的状态变化。
手机出厂时,会设置堆内存相关的几个系统参数(每个厂商和机型的设定可能不同):
# Provides overrides to configure the Dalvik heap for a 2G phone
# 192m of RAM gives enough space for 5 8 megapixel camera bitmaps in RAM.
PRODUCT_PROPERTY_OVERRIDES += \
dalvik.vm.heapstartsize=8m \ //表示进程启动后,堆得初始内存大小,它影响初始启动的流畅度。
dalvik.vm.heapgrowthlimit=192m \ //单个进程的最大可用的堆的内存。(不包含netive堆)
dalvik.vm.heapsize=512m \ //单个进程理论上可使用的内存的最大值。
dalvik.vm.heaptargetutilization=0.75 \ //内存利用率
dalvik.vm.heapminfree=512k \ //堆最小空闲内存heapminfree
dalvik.vm.heapmaxfree=8m //堆最大空闲内存heapmaxfree
这几个参数决定了Android中,堆内存的大小及堆变化的控制。
Android在每个应用启动时,都会给每个应用一个初始的可用内存空间,也就是设定一个初始的堆大小。这是因为如果应用刚启动,就给这个应用直接分配最大可用内存空间,那么一部手机也就运行不了几个应用就会消耗掉所有的内存空间了。Android为了,提高用户体验,让系统在有限的内存资源中,启动更多的应用,在应用启动时,都会给予应用一个很小的内存空间,随着应用内存需求的增加,系统会逐步增加该应用的可用内存空间(也就是堆空间的大小)。初始堆大小是通过系统参数dalvik.vm.heapstartsize设定的。
dalvik.vm.heapgrowthlimit表示堆得最大可用内存值。也就是说,应用启动后,随着堆空间的增加,占用的系统内存量也会逐步增加,但是增长是有限制的,堆得空间不可能无限增长。dalvik.vm.heapgrowthlimit就代表了Java堆内存的增长上限值。
dalvik.vm.heapsize表示应用最大可用的堆内存空间。当我们在应用程序Manifest中配置android:largeHeap="true"时,我们的最大堆内存大小就可以使用dalvik.vm.heapsize的值了。它代表了“大堆”空间,目的是给有特殊需求的,内存使用量非常大的应用使用的。
dalvik.vm.heaptargetutilization代表了内存的利用率。每次GC之后会评估当前堆内存的大小是否是适合的。dalvik.vm.heaptargetutilization = 已使用内存良/堆大小。过大或者过小都会影响GC的频繁程度及效率,它通常被设置为0.75。
dalvik.vm.heapminfree可允许的堆的最大空闲内存量。
dalvik.vm.heapmaxfree可允许的堆的最小空闲内存量。
方法一:
adb shell getprop
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]
方法二:
adb shell cat /system/build.prop #需要root权限
dalvik.vm.heapgrowthlimit=256m
dalvik.vm.heapstartsize=8m
dalvik.vm.heapsize=512m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m
代码位置:/frameworks/base/core/java/android/app/ActivityManager.java
public int getMemoryClass() {
return staticGetMemoryClass();
}
/** @hide */
static public int staticGetMemoryClass() {
// Really brain dead right now -- just take this from the configured
// vm heap size, and assume it is in megabytes and thus ends with "m".
String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
if (vmHeapSize != null && !"".equals(vmHeapSize)) {
return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
}
return staticGetLargeMemoryClass();
}
public int getLargeMemoryClass() {
return staticGetLargeMemoryClass();
}
/** @hide */
static public int staticGetLargeMemoryClass() {
// Really brain dead right now -- just take this from the configured
// vm heap size, and assume it is in megabytes and thus ends with "m".
String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}
每个进程的最大可用内存值可以通过ActivityManager的getMemoryClass()来获取,返回值也就是dalvik.vm.heapgrowthlimit值。
getLargeMemoryClass()返回的是dalvik.vm.heapsize的值,在应用程序Manifest中配置android:largeHeap=“true”,这样就可以使用“大堆”空间,也就是dalvik.vm.heapsize作为最大内存限制值了。
当应用(进程)启动后,会创建相关堆用于内存分配。
heap.h中保存了堆大小相关属性(/art/runtime/gc/heap.h):
// Maximum size that the heap can reach.
size_t capacity_;
// The size the heap is limited to. This is initially smaller than capacity, but for largeHeap
// programs it is "cleared" making it the same as capacity.
size_t growth_limit_;
// When the number of bytes allocated exceeds the footprint TryAllocate returns null indicating
// a GC should be triggered.
size_t max_allowed_footprint_;
// When num_bytes_allocated_ exceeds this amount then a concurrent GC should be requested so that
// it completes ahead of an allocation failing.
size_t concurrent_start_bytes_;
// Since the heap was created, how many bytes have been freed.
uint64_t total_bytes_freed_ever_;
// Since the heap was created, how many objects have been freed.
uint64_t total_objects_freed_ever_;
// Number of bytes allocated. Adjusted after each allocation and free.
Atomic num_bytes_allocated_;
// Minimum free guarantees that you always have at least min_free_ free bytes after growing for
// utilization, regardless of target utilization ratio.
size_t min_free_;
// The ideal maximum free size, when we grow the heap for utilization.
size_t max_free_;
// Target ideal heap utilization ratio
double target_utilization_;
capacity_:堆可以达到的最大大小。对应dalvik.vm.heapsize。
growth_limit_:堆的大小限制。对应dalvik.vm.heapgrowthlimit。
max_allowed_footprint_:当前最大可分配空间容量。
concurrent_start_bytes_:当num_bytes_allocated_超过此数量时,应该请求并发GC,以便在分配失败之前完成。
total_bytes_freed_ever_:自创建堆以来,已释放了多少字节。
min_free_:最小空闲内存。对应dalvik.vm.heapminfree。
max_free_:最大空闲内存。对应dalvik.vm.heapmaxfree。
以上相关参数,在进程启动后会有相对应的默认值。Android中可分配内存的堆也会随着程序的允许而动态调整。
堆内存空间的大小会随着应用程序的运行而动态改变。当应用程序创建了大量对象,需要更多内存空间时,可用的堆内存阈值(当前堆的最大可用内存值)就会增加;而当GC过后,回收了大量内存空间之后,可用的堆内存阈值就会相应变小,以释放给系统更多未使用的内存资源。
通常,堆内存的改变都是在GC之后发生的,在GC之后,系统会根据当前的已使用内存大小、未使用内存大小、内存使用率、最大内存空闲值、最小内存空闲值等参数,最终计算出当前堆内存的最优大小。那么在什么情况下会运行GC呢?比如,在对象内存分配尝试失败之后、堆上已分配内存达到一定阈值、堆空间切换等场景,都会触发GC过程。GC主要过程是通过CollectGarbageInternal方法实现的。
我们来看CollectGarbageInternal方法(/art/runtime/gc/heap.cc):
collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type,
GcCause gc_cause,
bool clear_soft_references) {
…… //一些错误处理逻辑
…… //压缩GC相关处理
if (gc_cause == kGcCauseForAlloc && runtime->HasStatsEnabled()) {//对象内存分配引发的GC。记录GC次数
++runtime->GetStats()->gc_for_alloc_count;
++self->GetStats()->gc_for_alloc_count;
}
const uint64_t bytes_allocated_before_gc = GetBytesAllocated();//当前已分配的内存量
if (gc_type == NonStickyGcType()) {
// Move all bytes from new_native_bytes_allocated_ to
// old_native_bytes_allocated_ now that GC has been triggered, resetting
// new_native_bytes_allocated_ to zero in the process.
old_native_bytes_allocated_.FetchAndAddRelaxed(new_native_bytes_allocated_.ExchangeRelaxed(0));
if (gc_cause == kGcCauseForNativeAllocBlocking) {
MutexLock mu(self, *native_blocking_gc_lock_);
native_blocking_gc_in_progress_ = true;
}
}
DCHECK_LT(gc_type, collector::kGcTypeMax);
DCHECK_NE(gc_type, collector::kGcTypeNone);
//以下设置,选择哪个垃圾回收器
collector::GarbageCollector* collector = nullptr;
// TODO: Clean this up.
if (compacting_gc) {
DCHECK(current_allocator_ == kAllocatorTypeBumpPointer ||
current_allocator_ == kAllocatorTypeTLAB ||
current_allocator_ == kAllocatorTypeRegion ||
current_allocator_ == kAllocatorTypeRegionTLAB);
switch (collector_type_) {
case kCollectorTypeSS:
// Fall-through.
case kCollectorTypeGSS:
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 = concurrent_copying_collector_;
break;
case kCollectorTypeMC:
mark_compact_collector_->SetSpace(bump_pointer_space_);
collector = mark_compact_collector_;
break;
default:
LOG(FATAL) << "Invalid collector type " << static_cast(collector_type_);
}
if (collector != mark_compact_collector_ && collector != concurrent_copying_collector_) {
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();
}
CHECK(temp_space_->IsEmpty());
}
gc_type = collector::kGcTypeFull; // TODO: Not hard code this in.
} else if (current_allocator_ == kAllocatorTypeRosAlloc ||
current_allocator_ == kAllocatorTypeDlMalloc) {
collector = FindCollectorByGcType(gc_type);
} else {
LOG(FATAL) << "Invalid current allocator " << current_allocator_;
}
if (IsGcConcurrent()) {
// Disable concurrent GC check so that we don't have spammy JNI requests.
// This gets recalculated in GrowForUtilization. It is important that it is disabled /
// calculated in the same thread so that there aren't any races that can cause it to become
// permanantly disabled. b/17942071
concurrent_start_bytes_ = std::numeric_limits::max();
}
CHECK(collector != nullptr)
<< "Could not find garbage collector with collector_type="
<< static_cast(collector_type_) << " and gc_type=" << gc_type;
collector->Run(gc_cause, clear_soft_references || runtime->IsZygote()); //执行GC
total_objects_freed_ever_ += GetCurrentGcIteration()->GetFreedObjects(); //对象释放数量
total_bytes_freed_ever_ += GetCurrentGcIteration()->GetFreedBytes(); //GC的内存回收量
RequestTrim(self);//裁剪堆
// Enqueue cleared references.
reference_processor_->EnqueueClearedReferences(self);//处理被回收了的引用对象
// Grow the heap so that we know when to perform the next GC.
GrowForUtilization(collector, bytes_allocated_before_gc);//调整堆的大小
LogGC(gc_cause, collector);
FinishGC(self, gc_type);
// Inform DDMS that a GC completed.
Dbg::GcDidFinish();
// Unload native libraries for class unloading. We do this after calling FinishGC to prevent
// deadlocks in case the JNI_OnUnload function does allocations.
{
ScopedObjectAccess soa(self);
soa.Vm()->UnloadNativeLibraries();
}
return gc_type;
}
主要过程:
错误处理逻辑、压缩GC等相关处理。
设置本次GC所使用的垃圾回收器。
调用collector->Run执行GC。
调用RequestTrim(self)执行堆剪切,将内存归还给内核。此操作会造成卡顿,Android设置了一个最小时间间隔来优化性能。
调用EnqueueClearedReferences()处理被回收了的引用对象。
调用GrowForUtilization()调整堆的大小。
GC完成之后,就会进行堆内存空间大小的调整,这个过程是通过GrowForUtilization方法实现的。
我们来看GrowForUtilization方法(/art/runtime/gc/heap.cc):
void Heap::GrowForUtilization(collector::GarbageCollector* collector_ran,
uint64_t bytes_allocated_before_gc) {
// We know what our utilization is at this moment.
// This doesn't actually resize any memory. It just lets the heap grow more when necessary.
const uint64_t bytes_allocated = GetBytesAllocated();//已分配内存大小
// Trace the new heap size after the GC is finished.
TraceHeapSize(bytes_allocated);
uint64_t target_size;
collector::GcType gc_type = collector_ran->GetGcType();//GC类型
const double multiplier = HeapGrowthMultiplier(); // 乘数因子,用于为前台应用设置更高的空闲内存
// 前台应用设置,最小、最大空闲内存均乘于乘数因子。
const uint64_t adjusted_min_free = static_cast(min_free_ * multiplier);
const uint64_t adjusted_max_free = static_cast(max_free_ * multiplier);
if (gc_type != collector::kGcTypeSticky) {//非粘性GC配置
// Grow the heap for non sticky GC.
ssize_t delta = bytes_allocated / GetTargetHeapUtilization() - bytes_allocated;
CHECK_GE(delta, 0);
target_size = bytes_allocated + delta * multiplier;
target_size = std::min(target_size, bytes_allocated + adjusted_max_free);
target_size = std::max(target_size, bytes_allocated + adjusted_min_free);
next_gc_type_ = collector::kGcTypeSticky;
} else {//粘性GC配置
collector::GcType non_sticky_gc_type = NonStickyGcType();
// Find what the next non sticky collector will be.
collector::GarbageCollector* non_sticky_collector = FindCollectorByGcType(non_sticky_gc_type);
// If the throughput of the current sticky GC >= throughput of the non sticky collector, then
// do another sticky collection next.
// We also check that the bytes allocated aren't over the footprint limit in order to prevent a
// pathological case where dead objects which aren't reclaimed by sticky could get accumulated
// if the sticky GC throughput always remained >= the full/partial throughput.
if (current_gc_iteration_.GetEstimatedThroughput() * kStickyGcThroughputAdjustment >=
non_sticky_collector->GetEstimatedMeanThroughput() &&
non_sticky_collector->NumberOfIterations() > 0 &&
bytes_allocated <= max_allowed_footprint_) {
next_gc_type_ = collector::kGcTypeSticky;
} else {
next_gc_type_ = non_sticky_gc_type;
}
// If we have freed enough memory, shrink the heap back down.
if (bytes_allocated + adjusted_max_free < max_allowed_footprint_) {
target_size = bytes_allocated + adjusted_max_free;
} else {
target_size = std::max(bytes_allocated, static_cast(max_allowed_footprint_));
}
}
if (!ignore_max_footprint_) {
SetIdealFootprint(target_size);
if (IsGcConcurrent()) {
const uint64_t freed_bytes = current_gc_iteration_.GetFreedBytes() +
current_gc_iteration_.GetFreedLargeObjectBytes() +
current_gc_iteration_.GetFreedRevokeBytes();
// Bytes allocated will shrink by freed_bytes after the GC runs, so if we want to figure out
// how many bytes were allocated during the GC we need to add freed_bytes back on.
CHECK_GE(bytes_allocated + freed_bytes, bytes_allocated_before_gc);
const uint64_t bytes_allocated_during_gc = bytes_allocated + freed_bytes -
bytes_allocated_before_gc;
// Calculate when to perform the next ConcurrentGC.
// Calculate the estimated GC duration.
const double gc_duration_seconds = NsToMs(current_gc_iteration_.GetDurationNs()) / 1000.0;
// Estimate how many remaining bytes we will have when we need to start the next GC.
size_t remaining_bytes = bytes_allocated_during_gc * gc_duration_seconds;
remaining_bytes = std::min(remaining_bytes, kMaxConcurrentRemainingBytes);
remaining_bytes = std::max(remaining_bytes, kMinConcurrentRemainingBytes);
if (UNLIKELY(remaining_bytes > max_allowed_footprint_)) {
// A never going to happen situation that from the estimated allocation rate we will exceed
// the applications entire footprint with the given estimated allocation rate. Schedule
// another GC nearly straight away.
remaining_bytes = kMinConcurrentRemainingBytes;
}
DCHECK_LE(remaining_bytes, max_allowed_footprint_);
DCHECK_LE(max_allowed_footprint_, GetMaxMemory());
// Start a concurrent GC when we get close to the estimated remaining bytes. When the
// allocation rate is very high, remaining_bytes could tell us that we should start a GC
// right away.
concurrent_start_bytes_ = std::max(max_allowed_footprint_ - remaining_bytes,
static_cast(bytes_allocated));
}
}
}
乘数因子,用于为前台应用设置更高的空闲内存。前台应用设置,最小、最大空闲内存均乘于乘数因子。
设置目标堆大小,设置下一次GC类型。这里分为了粘性GC(只回收上次垃圾回收到本次垃圾回收之间产生的垃圾)、非粘性GC(部分GC和full GC)。
非粘性GC的设置:
调用SetIdealFootprint(target_size)方法设置堆大小,也就是设置max_allowed_footprint_属性的值。
对于并发GC,需要处理并发过程中的对象分配情况。
修改当前允许分配的最大空间(/art/runtime/gc/heap.cc):
void Heap::SetIdealFootprint(size_t max_allowed_footprint) {
if (max_allowed_footprint > GetMaxMemory()) {
VLOG(gc) << "Clamp target GC heap from " << PrettySize(max_allowed_footprint) << " to "
<< PrettySize(GetMaxMemory());
max_allowed_footprint = GetMaxMemory();
}
max_allowed_footprint_ = max_allowed_footprint;
}
返回当前进程可使用的最大内存值(/art/runtime/gc/heap.h):
size_t GetMaxMemory() const {
// There is some race conditions in the allocation code that can cause bytes allocated to
// become larger than growth_limit_ in rare cases.
return std::max(GetBytesAllocated(), growth_limit_);//返回已分配内存量和堆内存大小限制值之间的最大值。
}
至此,ART堆空间大小的设置,以及堆空间大小的调整过程我们已经分析完了。下面我们来总结一下。
ART虚拟机中,堆(Heap)空间负责进行对象内存的分配。堆的大小也决定了一个应用可使用的内存大小。
手机出厂时,会设置堆内存相关的几个系统参数(每个厂商和机型的设定可能不同),这几个参数决定了Android中,堆内存的大小及堆变化的控制。
heap.h中保存了堆大小相关属性。
堆内存空间的大小会随着应用程序的运行而动态改变。
通常,堆内存的改变都是在GC之后发生的,在GC之后,系统会根据当前的已使用内存大小、未使用内存大小、内存使用率、最大内存空闲值、最小内存空闲值等参数,最终计算出当前堆内存的最优大小。
GC主要过程是通过CollectGarbageInternal方法实现的。
GC完成之后,就会进行堆内存空间大小的调整,这个过程是通过GrowForUtilization方法实现的。
GrowForUtilization方法会设置目标堆大小,设置下一次GC类型。并且会区分粘性GC(只回收上次垃圾回收到本次垃圾回收之间产生的垃圾)、非粘性GC(部分GC和full GC)。