After introducing the in-depth study of Android: Virtual Machine & Runtime, many friends asked me, the knowledge structure you described seems difficult and lofty, how can it be used in practical work? I’ll give you a quick example today.

As we all know, our Android App runs on a Java virtual machine, and Java is a language with GC. When virtual machine garbage collection, to do a very visual thing called STW (Stop the world); That is, in order to reclaim objects that are no longer in use, the virtual machine must stop all threads to do the necessary work. While this is a significant improvement on the ART runtime, the presence of GC has a subtle impact on the performance of the App runtime. If you’ve ever looked at your phone’s log, you’ll see something like this:

12-23 18:46:07. 300, 28643-28658 /? I/art: Background sticky concurrent mark sweep GC freed 15442(1400KB) AllocSpace objects, 8(128KB) LOS objects, 4% free, Paused 10.356ms total 53.023ms at GCDaemon Thread CareAboutPauseTimes 1 12-23 18:46:12.250 28643-28658/? I/art: Background partial concurrent mark sweep GC freed 28723(1856KB) AllocSpace objects, 6(92KB) LOS objects, 11% free, Paused 2.380ms total 108.502ms at GCDaemon Thread CareAboutPauseTimes 1

The above log reflects the fact that GC comes at a cost. There are a lot of performance optimization articles about GC, and there are a lot of articles about how garbage collection works and how garbage collection works, but the strategies are “don’t create unnecessary objects”, “Avoid memory leaks” and finally the use of MAT, LeakCanary and other tools. All I can say is that it’s pretty weak — writing code like this and learning how to use the tools should be a basic requirement.

Android also supports NDK development, but we can’t rewrite everything in C++, can we? So, is there any way we can influence the GC strategy to minimize GC? The answer is yes. The principle lies in the Android process mechanism — each App has a separate virtual machine instance, and we have considerable control over the App’s own process space.

Let me give you a simple example. (The following content is based on Android 5.1, all principles and code are not guaranteed to work on other system versions or even ROM)

All App processes on Android are fork from Zygote process. App sub-processes share Zygote process space with copy on write mechanism. The Creation of the Android virtual machine and runtime is completed when the Android system is started and the Zygote process is created. Garbage collection is part of a virtual machine, so let’s start with the Zygote process.

As we know, the Android system is based on the Linux kernel, and in Linux, all processes are descendants of the Init process. Zygote process is no exception. It is created by the init process during the system startup process. At system startup script system/core/rootdir/init. Rc file, we can see the start of the Zygote process script commands:

Service zygote /system/bin/app_process -Xzygote /system/bin – zygote – start-system-server

That is, init creates zygote by executing the /system/bin/app_process executable. The source code for app_process is available here; At the end of the main function there’s this:

if (zygote) {
    runtime.start("com.android.internal.os.ZygoteInit", args);
} else if (className) {
Copy the code

CPP calls androidRuntime.cpp’s start function, and the most important step in this function is to start the VM:

JNIEnv* env; if (startVm(&mJavaVM, &env) ! = 0) { return; }Copy the code

This function is quite long, but it resolves the parameters of the virtual machine startup, such as heap size, etc. Exploring largeHeap illustrates some of the important parameters that are important to a virtual machine, as we’ll see later. After parsing the parameters, JNI_CreateJavaVM is finally called to actually create the Java virtual machine. This interface is one of the three interfaces defined by Android VIRTUAL machine. Dalvik can switch to ART largely due to this. Its concrete is now jni_internal.cc; The JNI_CreateJavaVM function creates the Android runtime directly after taking the vm parameters:

if (! Runtime::Create(options, ignore_unrecognized)) { return JNI_ERR; }Copy the code

The creation of Runtime is very complicated, in which, related to GC, the heap space for App is created; The Heap constructor takes a number of parameters that have a significant impact on the GC, and this is a good place to start if you want to adjust the GC strategy.

heap_ = new gc::Heap(options->heap_initial_size_,
                     options->heap_growth_limit_,
                     options->heap_min_free_,
                     options->heap_max_free_,
                     options->heap_target_utilization_,
                     options->foreground_heap_growth_multiplier_,
                     options->heap_maximum_size_,
// ...
Copy the code

Heap_initialsize is the initial heap size, heap_growthlimit is the maximum heap growthlimit, heap_minfree and heap_maxfree are what? To put it simply, the Android system reduces memory fragmentation in the heap to ensure heap utilization efficiency. The heap size is adjusted each time GC is performed to reclaim some memory. For example, if you enter a page with a lot of images and apply for 100 MB of memory, when you exit the page, the 100 MB of memory will be reclaimed as free memory. But instead of giving you 100 MB of free memory, the system makes an adjustment to prevent waste. The specific size is related to heAP_MIN_FREE_, HEAP_MAX_free_ and heap_target_UTILIzation_.

At this point, the original part is explained; There is no difficulty except that the process is slightly complicated. So what does this heap have to do with our startup performance optimization?

During the startup of an Android App, the memory occupied by the process continues to rise for a period of time. Assume that the initial size of the heap is 8M and the peak memory usage during startup is 30M. During the startup process, a large number of temporary objects are created that die and are soon recycled:

As shown in the figure above, this is the memory usage of an App during a startup. We see a lot of little broken lines, the technical term is memory jitter; The reason, too, is obvious — a large number of temporary objects are created. How to solve it? Don’t create a lot of temporary objects, they say. I know all about it, but I can’t do it. For many large apps, the startup process is quite complicated, and many operations cannot be simply removed. So the question comes, 30M is not a very large number, why is the system so panicked, still need to keep drip reclaim memory?

There is a kind of cold, called your mother thinks you are cold. Garbage collection is not about recycling when there is garbage, it’s about recycling whenever the system feels you need it.

So, is it possible to keep the heap growing during startup without GC? After all, 30M doesn’t make OOM. What is the reason why the system is not doing this? The answer is free memory. For example, the heap started with 8M and grew to 24M as the boot process progressed. At this point, a GC is performed, reclaiming 8M memory and the heap is back to 16M; We also have 8M free memory. And the system says, boy, what are you doing with all this free memory? Come to mom and keep it for you, so you only have 2M free memory left. But it became clear that the App would soon be using more than 18M of heap memory, triggering a series of GC’s and heap resizing that would continue until the memory stabilized after startup. So far, our conclusions are obvious:

If we can adjust heAP_minfree and heAP_maxfree, we can greatly affect the GC process

How do I resize these two parameters? Get the pointer to the Heap object, find the offsets of these two parameters, and modify the memory. This requires a little bit of C++ memory layout; As for how to get the pointer to the Heap object, only to find the answer to the source code. Here I give the final implementation code:

Void modifyHeap(unsigned size) {// JavaVMExt * vmExt = (JavaVMExt *); if (vmExt->runtime == NULL) { return; } char* runtime_ptr = (char*) vmExt->runtime; void** heap_pp = (void**)(runtime_ptr + 188); char* c_heap = (char*) (*heap_pp); char* min_free_offset = c_heap + 532; char* max_free_offset = min_free_offset + 4; char* target_utilization_offset = max_free_offset + 4; size_t* min_free_ = (size_t*) min_free_offset; size_t* max_free_ = (size_t*) max_free_offset; *min_free_ = 1024 * 1024 * 2; *max_free_ = 1024 * 1024 * 8; }Copy the code

After the modification, the memory footprint during the startup process is as follows, you can see that our purpose has been achieved:

By the way, the above code does not consider any portability or adaptation, but is for demonstration purposes only. Getting to work is a physical task: first, we rely on the memory layout of a class in a particular Android version, where the offsets of member variables may vary from version to version. Second, the specific size of minfree and MaxFree is closely related to the physical memory of the phone, the memory used by the App, the initial heap size of the phone configuration and other factors. Adjusting the right parameters can take some time, and with so many Android models out there, there are a few tricks.

I don’t know if the above example makes you feel deep into the bottom of the system, the kind of omnipotent pleasure of controlling wind and rain? Maybe a lot of people think we are just writing if else, adjust the surface change animation write business is enough; But I want to make the point that it is very beneficial to learn the principles of systems in depth, and it gives you capabilities that you would never have on the application layer.

We mentioned above that the number of GC observations can be detected by code other than by tools in debug mode. This article focuses on the importance of the native layer, such as virtual machine runtime. The answer can be found in the Java Framework