Android System Optimization (33) - - Analysis of Grow ForUtilization of Android ART GC

Keywords: Android github Java

Analysis of Grow ForUtilization of Android ART GC


Reprinted from https://hello2mao.github.io

There are many situations that trigger garbage collection in Android running process. Taking android 5.0 as an example, we can find that the most common way to trigger GC in application running process is as follows:

This graph is a dynamic change of the memory occupancy of an application during the running process of an android application intercepted by android studio. The blue part is the memory occupied by the application, and the gray part is the current idle memory. As you can see, at that point in the white circle, when the application of idle memory reaches a certain threshold, the android system thinks that the current memory is not enough, so the system wakes up GC threads for garbage collection. Through logcat, you can see that the following logs are printed to indicate the effect of garbage collection.

In today's android 5.0 ART GC, every time the GC cleans up and plays with garbage, the system readjusts the heap size to control the remaining memory of the heap to meet the pre-set heap utilization constraints (in fact, the java heap is initialized and fixed to the memory address space at application startup, where adjusting the heap size means only adjusting the statistics of heap available memory). android does this by dynamically adjusting the statistics of heap available memory, making the distribution of objects in the heap more compact and eliminating slightly the fragmentation of heap memory caused by the tagged garbage collection algorithm. As shown in the following figure:

After GC triggers, the garbage objects that are no longer used by the application are reclaimed, so the available memory becomes larger, as shown in the collect garbage process on the right side of the figure above. But the android system will not give such a large amount of available memory to the application. It will adjust the size of the available memory according to the parameters such as the pre-set heap utilization of the system, and temporarily call the available memory after adjusting the size reserved. Free memory, this process is implemented in the code by calling GrowForUtilization. The next GC will be triggered when this reserved free memory is used almost. From the later analysis, we can see that the size of the reserved free memory is almost a fixed value in a sense.

Next, I will analyze the implementation of GrowForUtilization in detail.

GrowForUtilization is implemented in art/runtime/gc/heap.cc. The code is as follows:

void Heap::GrowForUtilization(collector::GarbageCollector* collector_ran) {
  // 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();
  last_gc_size_ = bytes_allocated;
  last_gc_time_ns_ = NanoTime();
  uint64_t target_size;
  collector::GcType gc_type = collector_ran->GetGcType();
  if (gc_type != collector::kGcTypeSticky) {
    // Grow the heap for non sticky GC.
    const float multiplier = HeapGrowthMultiplier();  // Use the multiplier to grow more for
    // foreground.
    intptr_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 + static_cast<uint64_t>(max_free_ * multiplier));
    target_size = std::max(target_size,
                         bytes_allocated + static_cast<uint64_t>(min_free_ * multiplier));
    native_need_to_run_finalization_ = true;
    next_gc_type_ = collector::kGcTypeSticky;
  } else {
    collector::GcType non_sticky_gc_type =
        have_zygote_space_ ? collector::kGcTypePartial : collector::kGcTypeFull;
    // 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 + max_free_ < max_allowed_footprint_) {
      target_size = bytes_allocated + max_free_;
    } else {
      target_size = std::max(bytes_allocated, static_cast<uint64_t>(max_allowed_footprint_));
    }
  }
  if (!ignore_max_footprint_) {
    SetIdealFootprint(target_size);
    if (IsGcConcurrent()) {
      // 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 = allocation_rate_ * 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<size_t>(bytes_allocated));
    }
  }
}

The code sets target_size according to the type of GC, and target-size-bytes-allocated is the reserved free memory mentioned above. Let max-free- = a, min-free- = b, utilization = c, multiplier = d, bytes-allocated = x, target-size = T.

When GC is not sticky gc,?
delta = x/c - x
t1 = x + d * delta = x + d(1-c)x/c
t2 = min(t1,x + da)
T = max(t2,x + db)
Reserve free memory = T - x
When GC is sticky gc,
Reserve free memory = a
If the development version is configured (in build.prop): max-free- = 8M, min-free- = 4M, utilization = 0.75, multiplier = 2.

dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=4m
dalvik.vm.heapmaxfree=8m

The relationship between the reserved free memory and the used memory can be obtained as shown in the following figure:

It can be seen that when GC is sticky gc, the reserved free memory is a fixed value of max-free-. When GC is non-sticky gc, the size of reserved free memory is related to the memory used by the application. For example, in this development version, when the memory occupied by the application exceeds 24M, reserved free memory becomes a fixed value of 16M, while for the applications frequently used by users, it is easy to exceed 24M. Over 24M, after each GC, 16M of free memory is reserved. When 16M is used up, the next GC will be triggered.

So when the application allocates a lot of objects in a short time, 8/16M memory will run out quickly, so the number of GC will be very large, as shown in the following figure:

As you can see from the code, if this GC is not sticky gc, then the next GC must be sticky gc. If the GC is sticky GC this time, it will decide whether sticky GC or partial gc or full gc next time according to the throughput of gc.


Posted by Adam on Tue, 11 Dec 2018 10:24:05 -0800