在这篇文章中我们将尝试搞清楚Java内存模型以及垃圾收集器是如何工作的。在这篇文章中我使用的是JDK8 Oracle Hot Spot 64 bit版本的JVM。首先我描述一下Java进程使用的不同内存区域。
一旦我们启动了JVM,操作系统就会为Java进程分配内存。这里JVM自身就是一个进程,操作系统分配给它的内存包括Heap(堆)
、Meta Space(元空间)
、JIT code cache(JIT代码缓存)
、
thread stacks(线程栈)
、shared libraries(共享类库)
。我们称这部分内存为Native Memory
,Native Memory
指的是由操作系统分配给Java进程的内存。
操作系统给Java进程分配多少内存取决于操作系统本身、处理器和JRE。
我们来阐述一下对于JVM可用的各部分内存块。
-
Heap Memory(堆): JVM使用这部分内存存储对象,这部分内存划分为两块不同的内存区域:
Young Generation Space(年轻代)
和Tenured Space(老年代)
-
Young Generation(年轻代): Young Generation(或称为the New Space)被划分为两部分:
Eden Space
和Survivor Space
-
Eden Space(伊甸区):当创建对象的时候, 内存会分配在Eden Space.
-
Survivor Space(幸存区): 这里容纳从Minor GC(或称为the Young garbage collection)中幸存下来的对象,我们把幸存区划分为两块相同大小的区域:S0和S1.
-
Tenured Space(老年代): 在minor GC或young GC过程中,如对象达到最大年龄阈值(max tenured threshold),将会被移动到
Tenured Space
(或称为Old Generation Space
).
当我们稍后讨论垃圾收集过程的时候就会知道上述的内存区域是如何使用的。
-
Meta Space(元空间): 这块内存是堆外内存、本地内存的一部分。根据文档默认情况下Meta Space没有上限。在早期的JDK版本中称为
Perm Gen Space
,这部分内存用于 存放由类加载器加载的类,这个设计是防止出现OOM错误的出现。然而,如果占用的内存超过剩余的物理内存,操作系统将会使用虚拟内存。物理内存和虚拟内存之间的数据交换是一个巨大的开销,会影响到应用的性能。我们可以设置JVM的参数来限制Meta Space的大小,在这种情况下就可能出现OOM错误。 -
Code Cache(代码缓存): JVM使用解释器来解释字节码,并将其转换为硬件相关的机器码。作为JVM优化的一部分,the Just In Time(JIT)compiler被引入进来。JIT将频繁访问的代码块编译成本地代码(native code),并将其存储在Code Cache中。JIT编译过的代码不会被解释。
现在我们来讨论垃圾收集过程(the garbage collection process)。JVM单独使用一个守护线程来进行垃圾收集。正如上文所述,当应用程序创建对象的时候,JVM将尝试从Eden Space(伊甸区)获取所需要的内存。JVM执行GC比如minor GC和major GC。
我们来理解一下minor GC。
初始时Survivor Space(幸存区)
和Tenured Space(老年代)
是空的。当JVM不能从Eden Space(伊甸区)获得内存时就会启动minor GC。在minor GC过程中,那些不可达的对象被标记为待回收的。
JVM选择Survivor Space(幸存区)中的一块作为"To Space
",可能是S0,也可能是S1。JVM将所有可达的对象复制到"To Space
"(假设S0)并将对象年龄增加1,那些无法存放在Survivor Space中的对象将被移动到Tenured Space,这个过程称为"premature promotion"(过早提升)。下图中我故意让“To Space
” 比已经分配的空间大,但是请记住,Survivor Space大小不会增长。
在上图中,被标记为红色的对象表示他们是不可达的。所有可达的对象都是GC roots,垃圾收集器不会移除GC roots,而是移除不可达的对象并清空Eden Space。
再一次进行minor GC时,垃圾收集器标记Eden Space和To Survivor Space(S0)中不可达的对象,将GC roots复制到另外一块幸存区空间S1,与此同时,可达的对象的年龄将增加1。
在上图中,被标记为红色的对象有资格被GC,Eden Space和survivor spaces空间中其他的对象将被复制到另一块幸存区S1,同时对象年龄加1。
对于每一次minor GC重复上述过程,当对象的年龄到达最大年龄阈值(the max age threshold)就会被复制到Tenured Space。
这里有一个叫做MaxTenuringThreshold
的JVM选项,它用于指定对象提升到Tenured Space的年龄阈值,默认值是15。
因此,很清楚地是minor GC从Young Generation Space
(年轻代)回收内存,这是一个"stop the world"的过程,有时候应用程序的停顿是可以忽略不计的。minor GC根据所使用的收集器可以单线程或多线程执行垃圾收集。
经过多次minor GC,最终Tenured Space会被填满,这时需要进行更多垃圾收集。此时JVM触发major GC
,也称为Full GC
。但是作为Full GC的一部分,JVM会从Meta Space中回收内存。如果堆中没有对象的话,MetaSpace(元空间)中已经加加载的类会被移除。
现在我们来看看JVM触发minor GC的时机:
- 如果开发人员调用
System.gc()
, 或Runtime.getRunTime().gc()
暗示JVM启动GC - 如果JVM判定没有足够的老年代空间了
- 在minor GC中,如果JVM不能从Eden Space或Survivor Space中回收足够的内存,就会触发magor GC
- 如果设置了JVM参数MaxMetaspaceSize,并且没有足够的空间来加载新的类时,JVM就会触发magor GC
翻译自Understanding the Java Memory Model and Garbage Collection , 原文作者Siva Prasad Rao Janapati