`

Java虚拟机(3)自动内存管理机制

 
阅读更多
概要

在Java技术体系中,自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存

本文主要探讨给对象分配内存这点事。对象的内存分配,往大的方向上讲,就是在堆上分配。但是也有一些情况会在栈上分配和在TLAB上分配等。所以分配的规则并不是百分百固定的,其细节取决于当前使用的是哪一种GC组合和虚拟机中与内存相关的参数的设置



本文主要介绍几条最普遍的内存分配规则,并结合代码来验证这些规则。常见的内存分配规则有以下几条:
  • 对象优先分配在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代
  • 动态对象年龄判定
先来看一副图


现代商业虚拟机都将Java堆分为新生代和老年代,在新生代中,一般采用复制算法。在老年代中,一般采用标记-整理算法。

新生代的堆空间又划分为一块较大的Eden空间和两块较小的Survivor空间如上图所示,每次只使用Eden和其中的一块Survivor(因为采用复制算法,所以另外一块Survivor拿来做保留区域)。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,就是说每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存是会被“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)来进行分配担保,一般情况下的GC策略如下图:


废话少说,下面开始正文

对象优先分配在Eden分配

先运行下面代码,然后结合日志来分析。以下代码的开头都注释了执行时所需要设置的虚拟机启动参数,这些参数对实验结果有直接影响,请调试代码的时候不要忽略掉.本例子介绍的比较详细,后面几个例子中重复的内容就不在细讲。

public class TestGC1 {

       private static final int _1MB = 1024*1024;

       /**

       * VM ARGS: -verbose:gc - Xms20m -Xmx20m -Xmn10m - XX:SurvivorRatio=8 -XX:+PrintGCDetails

       */

       public static void testAllocation(){

             byte[] allocation1, allocation2,allocation3 ,allocation4;

            allocation1 = new byte [2 * _1MB];

            allocation2 = new byte [2 * _1MB];

            allocation3 = new byte [2 * _1MB];

            allocation4 = new byte [4 * _1MB];

      }

       public static void main(String[] args) {

             testAllocation();

      }

}


查看输出日志


先解释一下各个参数的意义:
-verbose:gc 表示输出虚拟机中的GC的情况 (红色方框部分)
-Xms20m 表示初始堆初始值
-Xmx20m 表示初始堆最大值 (-Xms和-Xmx的值相等时,表明堆空间大小为20m且不可扩展)
-Xmn10m 表示老年代的堆大小
-XX:SurvivorRatio=8 表明新生代中Eden区和Survivor区的空间比例为8:1
-XX:+PrintGCDetails 打印GC详细情况(蓝色方框部分)


testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象。当分配allocation4对象时候发生一次Minor GC.详细情况看日志(红色方框部分),关于各个数字的解释参考http://blog.csdn.net/alivetime/article/details/6895537 。这次GC的结果是新生代从6487KB变为149KB,而总内存占用量则几乎没有减少(因为allocation1,allocation2,allocation3三个对象都依旧存活)。这次MinorGC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩下的2MB不足已分配,因此发生Minor GC.
GC期间虚拟机又发现已有的3个2MB大小的对象无法存入Survior空间,所以只好通过分配担保机制提前转移到老年代中去。
此时,老年代的空间为10MB足够分配给3个2MB大小的对象,因此不会执行Full GC
接下来,虚拟机又会查看HandlePromotionFailure设置是否允许担保失败,默认设置为ture。所以最终执行Minor GC。执行过程如下图红色线头所示


这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB,Survivor空间,老年代被占用6MB。

大对象直接进入老年代

先运行代码并输出日志
public class TestGC2 {

       private static final int _1MB = 1024*1024;

       /**

       * VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintGCDetails

       * - XX:PretenureSizeThreshold=3145728

       */

       public static void testPretenureSizeThreshold(){

             byte [] allocation;

            allocation = new byte [4 * _1MB];

      }

       public static void main(String[] args) {

             testPretenureSizeThreshold();

      }

}


输出GC日志如下
Heap
def new generation total 9216K, used 507K [0x315e0000, 0x31fe0000, 0x31fe0000)
eden space 8192K, 6% used [0x315e0000, 0x3165ef18, 0x31de0000)
from space 1024K, 0% used [0x31de0000, 0x31de0000, 0x31ee0000)
to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000)
tenured generation total 10240K, used 4096K [0x31fe0000, 0x329e0000, 0x329e0000)
the space 10240K, 40% used[0x31fe0000, 0x323e0010, 0x323e0200, 0x329e0000)

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置的对象直接分配到老年代中。所以在testPretenureSizeThreshold()方法中 4MB的allocation对象被之间分配到老年代中。日志红色部分证明这一点

长期存活的对象进入老年代

为了能够识别哪些对象应当放在新生代,哪些对象应该放在老年代,虚拟机给每个对象定义了一个对象年龄技术器。
如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

下面分别以 MaxTenuringThreshold=1和 MaxTenuringThreshold=15运行下面代码并输出GC日志
public class TestGC3 {

       private static final int _1MB = 1024*1024;

       /**

       * VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintTenuringDistribution

       * - XX:MaxTenuringThreshold=1 - XX:+PrintGCDetails

       */

       public static void testMaxTenuringThreshold(){

             byte [] allocation1, allocation2,allocation3 ;

            allocation1 = new byte [ _1MB / 4];

            allocation2 = new byte [ _1MB * 4];

            allocation3 = new byte [ _1MB * 4];

            allocation3 = null ;

            allocation3 = new byte [ _1MB * 4];

      }

       public static void main(String[] args) {

             testMaxTenuringThreshold();

      }


MaxTenuringThreshold=1运行输出日志

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 415288 bytes, 415288 total
: 4695K->405K(9216K), 0.0064338 secs] 4695K->4501K(19456K), 0.0064885 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 136 bytes, 136 total
: 4665K->0K(9216K), 0.0013172 secs] 8761K->4501K(19456K), 0.0013602 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
Heap
def new generation total 9216K, used 4260K [0x315e0000, 0x31fe0000, 0x31fe0000)
eden space 8192K, 52% used [0x315e0000, 0x31a08fe0, 0x31de0000)
from space 1024K, 0% used[0x31de0000, 0x31de0088, 0x31ee0000)
to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000)
tenured generation total 10240K, used 4501K [0x31fe0000, 0x329e0000, 0x329e0000)
the space 10240K, 43% used[0x31fe0000, 0x32445578, 0x32445600, 0x329e0000)

MaxTenuringThreshold=15运行输出日志

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 415288 bytes, 415288 total
: 4695K->405K(9216K), 0.0064799 secs] 4695K->4501K(19456K), 0.0065357 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 136 bytes, 136 total
- age 2: 415080 bytes, 415216 total
: 4665K->405K(9216K), 0.0011127 secs] 8761K->4501K(19456K), 0.0011722 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
Heap
def new generation total 9216K, used 4665K [0x315e0000, 0x31fe0000, 0x31fe0000)
eden space 8192K, 52% used [0x315e0000, 0x31a08fe0, 0x31de0000)
from space 1024K, 39% used[0x31de0000, 0x31e455f0, 0x31ee0000)
to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000)
tenured generation total 10240K, used 4096K [0x31fe0000, 0x329e0000, 0x329e0000)
the space 10240K, 40% used[0x31fe0000, 0x323e0010, 0x323e0200, 0x329e0000)

在方法testMaxTenuringThreshold()中,对象allocation1需要256KB的内存空间。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年,新生代的Survivor空间变为0KB。
MaxTenuringThreshold=15时,allocation1对象在第二次GC发生时仍然留在新生代,新生代的Survivor空间变为404KB

动态对象年龄判定

为了能更好地适应不同程序的内存情况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold要求的年龄。
运行下面代码并输出GC日志

public class TestGC4 {

       private static final int _1MB = 1024*1024;

       /**

       * VM ARGS: - verbose:gc - Xms20m -Xmx20m - Xmn10m - XX:SurvivorRatio=8 - XX:+PrintTenuringDistribution

       * - XX:MaxTenuringThreshold=1 - XX:+PrintGCDetails

       */

       public static void testMaxTenuringThreshold(){

             byte [] allocation1, allocation2,allocation3 ,allocation4;

            allocation1 = new byte [ _1MB / 4];

            allocation2 = new byte [ _1MB / 4];

            allocation3 = new byte [ _1MB * 4];

            allocation4 = new byte [ _1MB * 4];

            allocation4 = null ;

            allocation4 = new byte [ _1MB * 4];

      }

       public static void main(String[] args) {

             testMaxTenuringThreshold();

      }

}


输出GC日志

[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 677448 bytes, 677448 total
: 4951K->661K(9216K), 0.0066603 secs] 4951K->4757K(19456K), 0.0067187 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 136 bytes, 136 total
: 4921K->0K(9216K), 0.0014790 secs] 9017K->4757K(19456K), 0.0015298 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x315e0000, 0x31fe0000, 0x31fe0000)
eden space 8192K, 52% used [0x315e0000, 0x31a08fe0, 0x31de0000)
from space 1024K, 0% used[0x31de0000, 0x31de0088, 0x31ee0000)
to space 1024K, 0% used [0x31ee0000, 0x31ee0000, 0x31fe0000)
tenured generation total 10240K, used 4757K [0x31fe0000, 0x329e0000, 0x329e0000)
the space 10240K, 46% used[0x31fe0000, 0x32485588, 0x32485600, 0x329e0000)

在日志中,我们发现Survivor空间的使用率为0,也就是说allocation1allocation2对象都进入到了老年代中,而没有等到15岁的年龄。


---------------------------------------------全文完----------------------------------------------------

分享到:
评论

相关推荐

    Java虚拟机中内存管理机制.pdf

    Java虚拟机(JVM)的内存管理机制是Java编程中至关重要的部分,它涉及到程序的性能和稳定性。本文主要分析了Java虚拟机的内存管理,特别是垃圾回收机制及其实现。 首先,Java虚拟机中的内存主要分为堆内存和栈内存...

    Java虚拟机内存管理总结

    Java虚拟机内存管理总结 Java虚拟机(JVM)中的内存管理是指Java语言中对象的分配和释放问题。Java中的内存管理可以分为两部分:对象的分配和释放。 对象的分配是由程序完成的,程序员需要通过关键字new为每个对象...

    JAVA虚拟机的内存管理

    本文档旨在对Sun J2SE 5.0版本中的Java HotSpot虚拟机(JVM)内存管理机制进行全面概述,包括不同类型的内存收集器及其配置方法、如何调整收集器内存区域的大小等。此外,还将提供一些影响内存收集器行为的常见选项,...

    深入理解Java 虚拟机内存模型.rar

    - **垃圾收集(Garbage Collection, GC)**:自动内存管理,清理不再使用的对象,防止内存泄漏。包括新生代GC(Minor GC)、老年代GC(Major GC)和全GC(Full GC)。 - **内存溢出与内存泄漏**:当内存耗尽或无法...

    Java虚拟机规范中文版.pdf

    例如,对JVM的垃圾回收算法的了解,有助于开发者编写出更适合自动内存管理的代码。了解JVM的线程模型,有助于编写高性能的多线程程序。 JVM规范是不断发展的,随着Java技术的发展,它也在不断地进行改进和扩展。...

    Java的内存管理机制分析

    - 堆内存是所有线程共享的,由Java虚拟机自动管理。 - 堆内存的大小可以通过JVM参数来设置,如`-Xms`和`-Xmx`来指定初始大小和最大限制。 3. **数据段(Data Segment)**: - 包括静态域和常量池两部分。 - **...

    java虚拟机

    3. **垃圾收集与内存管理**:JVM负责自动管理内存,包括对象的分配和回收。垃圾收集器是其重要组成部分,通过标记-清除、复制、标记-整理和分代收集等多种算法来释放不再使用的内存。 4. **类加载过程**:类的生命...

    Java虚拟机(Java VM) msjavax86 微软java虚拟机

    垃圾收集自动管理内存,避免程序员手动处理内存泄露问题;JIT编译则是在运行过程中将频繁执行的热点代码编译成本地机器码,提升运行效率。 总的来说,Java虚拟机是Java生态系统中的关键组件,它使得Java程序能够在...

    java虚拟机各种版本

    此外,JVM还实现了垃圾回收机制,自动管理内存,避免了程序员手动管理内存的麻烦。 JVM还有许多优化技术,如分代垃圾收集、并行/并发GC、压缩引用、逃逸分析等,这些都对提升Java应用的性能起到了关键作用。对于...

    深入java虚拟机 高清pdf 高清高清高清

    3. **垃圾收集与内存管理**:Java虚拟机中的垃圾收集机制是自动进行内存管理的关键。书里介绍了不同的垃圾收集算法(如标记-清除、复制、标记-整理、分代收集等)以及垃圾收集器(如Serial、ParNew、CMS、G1等),...

    JAVA虚拟机内存分配与回收机制[文].pdf

    JAVA虚拟机的内存分配与回收机制是JAVA语言的核心机制之一,栈和堆是JAVA用于在RAM中存放数据的地方,JAVA自动管理栈和堆,程序员不能直接地设置栈或堆。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小...

    JAVA虚拟机内存分配机制

    JAVA虚拟机内存分配机制是Java程序执行过程中内存管理的核心部分,它涉及到程序的性能和效率。Java虚拟机(JVM)内存主要分为两大部分:栈内存和堆内存。 栈内存主要存储程序运行过程中的局部变量,包括基本类型...

    安卓的JAVA虚拟机 简单实用

    同时,ART也提供了垃圾回收和内存管理机制,有助于提高应用的稳定性和效率。 3. **JAVA虚拟机的执行过程** 在Android中,Java源代码首先通过编译器生成.class文件,然后通过dx工具转换为.dex文件。在设备上,ART会...

    深入理解Java虚拟机(jvm性能调优+内存模型+虚拟机原理).zip

    此外,JVM的垃圾收集机制是其内存管理的关键,它自动回收不再使用的对象,避免内存泄漏。 为了更好地理解和分析JVM行为,开发者通常会借助各种工具,如VisualVM、JProfiler和JConsole等。这些工具可以实时监控JVM的...

    Java运行原理与Java虚拟机.pdf

    2. **内存保护**:通过内存管理机制,JVM可以防止程序访问非法内存区域,避免内存泄漏和其他内存相关的问题。 3. **数据流分析**:字节码验证器会进行数据流分析,确保所有变量在使用前已经被正确初始化。 4. **...

    Java虚拟机规范 深入java虚拟机

    它详细阐述了Java虚拟机(JVM)的工作原理,包括内存管理、类加载机制、字节码执行以及垃圾回收等核心概念。深入理解这些知识点对于提升程序性能、解决运行时问题以及设计高效的应用程序至关重要。 1. **JVM架构** ...

    JAVA虚拟机解读入门

    4. 内存管理:主要涉及垃圾收集机制,自动回收不再使用的对象,避免内存泄漏。现代JVM使用多种垃圾收集算法,如标记-清除、复制、标记-整理和分代收集等。 5. 本地方法接口:允许JVM调用原生的C/C++代码,实现与...

    Java虚拟机解释执行机制研究.pdf

    Java语言丰富的类库支持、自动内存管理和跨平台执行能力是其显著的优势,使得Java语言在互联网、企业网和嵌入式设备等领域广泛应用。JVM通过其解释执行机制和即时编译技术,使得Java程序具备了良好的平台无关性和...

Global site tag (gtag.js) - Google Analytics