Java虚拟机7:内存分配原则

http://www.cnblogs.com/xrq730/p/4841177.html

前言

对象的内存分配,往大的方向上讲,就是在堆上分配,少数情况下也可能会直接分配在 老年代中,分配的规则并不是百分之百固定的,其细节决定于当前使用的是哪种垃圾收集器组合,当然还有虚拟机中与内存相关的参数。垃圾收集器组合一般就是 Serial+Serial Old和Parallel+Serial Old,前者是Client模式下的默认垃圾收集器组合,后者是Server模式下的默认垃圾收集器组合,文章使用对比学习法对比Client模式下和 Server模式下同一条对象分配原则有什么区别。

TLAB

首先讲讲 什么是TLAB。内存分配的动作,可以按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲 (Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来 设定。这么做的目的之一,也是为了并发创建一个对象时,保证创建对象的线程安全性。TLAB比较小,直接在TLAB上分配内存的方式称为快速分配方式,而 TLAB大小不够,导致内存被分配在Eden区的内存分配方式称为慢速分配方式。

对象优先分配在Eden区上

上面讲了不同的垃圾收集器组合对于内存分配规则是有影响的,看下影响在什么地方并 解释一下原因,虚拟机参数为“-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8”,即10M新生代,10M老年代,10M新生代中8M的Eden区,两个Survivor区各1M。代码都是同 一段

public class EdenAllocationTest
{
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[4 * _1MB];
    }
}

Client模式下

[GC [DefNew: 6487K->194K(9216K), 0.0042856 secs] 6487K->6338K(19456K), 0.0043281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 4454K [0x0000000005180000, 0x0000000005b80000, 0x0000000005b80000)
  eden space 8192K,  52% used [0x0000000005180000, 0x00000000055a9018, 0x0000000005980000)
  from space 1024K,  18% used [0x0000000005a80000, 0x0000000005ab0810, 0x0000000005b80000)
  to   space 1024K,   0% used [0x0000000005980000, 0x0000000005980000, 0x0000000005a80000)
 tenured generation   total 10240K, used 6144K [0x0000000005b80000, 0x0000000006580000, 0x0000000006580000)
   the space 10240K,  60% used [0x0000000005b80000, 0x0000000006180048, 0x0000000006180200, 0x0000000006580000)
 compacting perm gen  total 21248K, used 2982K [0x0000000006580000, 0x0000000007a40000, 0x000000000b980000)
   the space 21248K,  14% used [0x0000000006580000, 0x0000000006869890, 0x0000000006869a00, 0x0000000007a40000)
No shared spaces configured.

Server模式下

Heap
 PSYoungGen      total 9216K, used 6651K [0x000000000af20000, 0x000000000b920000, 0x000000000b920000)
  eden space 8192K, 81% used [0x000000000af20000,0x000000000b59ef70,0x000000000b720000)
  from space 1024K, 0% used [0x000000000b820000,0x000000000b820000,0x000000000b920000)
  to   space 1024K, 0% used [0x000000000b720000,0x000000000b720000,0x000000000b820000)
 PSOldGen        total 10240K, used 4096K [0x000000000a520000, 0x000000000af20000, 0x000000000af20000)
  object space 10240K, 40% used [0x000000000a520000,0x000000000a920018,0x000000000af20000)
 PSPermGen       total 21248K, used 2972K [0x0000000005120000, 0x00000000065e0000, 0x000000000a520000)
  object space 21248K, 13% used [0x0000000005120000,0x0000000005407388,0x00000000065e0000)

看到在Client模式下,最后分配的4M在新生代中,先分配的6M在老年代中;在Server模式下,最后分配的4M在老年代中,先分配的6M在新生代中。说明不同的垃圾收集器组合对于对象的分配是有影响的。讲下两者差别的原因:

1、Client模式下,新生代分配了6M,虚拟机在GC前有6487K,比6M 也就是6144K多,多主要是因为TLAB和EdenAllocationTest这个对象占的空间,TLAB可以通过“-XX:+PrintTLAB” 这个虚拟机参数来查看大小。OK,6M多了,然后来了一个4M的,Eden+一个Survivor总共就9M不够分配了,这时候就会触发一次Minor GC。但是触发Minor GC也没用,因为allocation1、allocation2、allocation3三个引用还存在,另一块1M的Survivor也不够放下这 6M,那么这次Minor GC的效果其实是通过分配担保机制将这6M的内容转入老年代中。然后再来一个4M的,由于此时Minor GC之后新生代只剩下了194K了,够分配了,所以4M顺利进入新生代。

2、Server模式下,前面都一样,但是在GC的时候有一点区别。在GC前还会 进行一次判断,如果要分配的内存>=Eden区大小的一半,那么会直接把要分配的内存放入老年代中。要分配4M,Eden区8M,刚好一半,而且老 年代10M,够分配,所以4M就直接进入老年代去了。为了验证一下结论,我们把3个2M之后分配的4M改为3M看一下

public class EdenAllocationTest
{
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[3 * _1MB];
    }
}

运行结果为

[GC [PSYoungGen: 6487K->352K(9216K)] 6487K->6496K(19456K), 0.0035661 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 352K->0K(9216K)] [PSOldGen: 6144K->6338K(10240K)] 6496K->6338K(19456K) [PSPermGen: 2941K->2941K(21248K)], 0.0035258 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
 PSYoungGen      total 9216K, used 3236K [0x000000000af40000, 0x000000000b940000, 0x000000000b940000)
  eden space 8192K, 39% used [0x000000000af40000,0x000000000b269018,0x000000000b740000)
  from space 1024K, 0% used [0x000000000b740000,0x000000000b740000,0x000000000b840000)
  to   space 1024K, 0% used [0x000000000b840000,0x000000000b840000,0x000000000b940000)
 PSOldGen        total 10240K, used 6338K [0x000000000a540000, 0x000000000af40000, 0x000000000af40000)
  object space 10240K, 61% used [0x000000000a540000,0x000000000ab70858,0x000000000af40000)
 PSPermGen       total 21248K, used 2982K [0x0000000005140000, 0x0000000006600000, 0x000000000a540000)
  object space 21248K, 14% used [0x0000000005140000,0x0000000005429890,0x0000000006600000)

看到3M在新生代中,6M通过分配担保机制进入老年代了。

大对象直接进入老年代

虚拟机参数为“-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728”,最后那个参数表示大于这个设置值的对象直接在老年代中分配,这样做的目的是为 了避免在Eden区和两个Survivor区之间发生大量的内存复制。测试代码为

public class OldTest
{
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args)
    {
         byte[] allocation = new byte[4 * _1MB];
    }
}

Client模式下

Heap
 def new generation   total 9216K, used 507K [0x0000000005140000, 0x0000000005b40000, 0x0000000005b40000)
  eden space 8192K,   6% used [0x0000000005140000, 0x00000000051bef28, 0x0000000005940000)
  from space 1024K,   0% used [0x0000000005940000, 0x0000000005940000, 0x0000000005a40000)
  to   space 1024K,   0% used [0x0000000005a40000, 0x0000000005a40000, 0x0000000005b40000)
 tenured generation   total 10240K, used 4096K [0x0000000005b40000, 0x0000000006540000, 0x0000000006540000)
   the space 10240K,  40% used [0x0000000005b40000, 0x0000000005f40018, 0x0000000005f40200, 0x0000000006540000)
 compacting perm gen  total 21248K, used 2972K [0x0000000006540000, 0x0000000007a00000, 0x000000000b940000)
   the space 21248K,  13% used [0x0000000006540000, 0x00000000068272a0, 0x0000000006827400, 0x0000000007a00000)
No shared spaces configured.

Server模式下

Heap
 PSYoungGen      total 9216K, used 4603K [0x000000000afc0000, 0x000000000b9c0000, 0x000000000b9c0000)
  eden space 8192K, 56% used [0x000000000afc0000,0x000000000b43ef40,0x000000000b7c0000)
  from space 1024K, 0% used [0x000000000b8c0000,0x000000000b8c0000,0x000000000b9c0000)
  to   space 1024K, 0% used [0x000000000b7c0000,0x000000000b7c0000,0x000000000b8c0000)
 PSOldGen        total 10240K, used 0K [0x000000000a5c0000, 0x000000000afc0000, 0x000000000afc0000)
  object space 10240K, 0% used [0x000000000a5c0000,0x000000000a5c0000,0x000000000afc0000)
 PSPermGen       total 21248K, used 2972K [0x00000000051c0000, 0x0000000006680000, 0x000000000a5c0000)
  object space 21248K, 13% used [0x00000000051c0000,0x00000000054a72a0,0x0000000006680000)

看到Client模式下4M直接进入了老年代,Server模式下4M还在新生代中。产生这个差别的原因是“-XX:PretenureSizeThreshold”这个参数对Serial+Serial Old垃圾收集器组合有效而对Parallel+Serial Old垃圾收集器组合无效

其他几条原则

上面列举的原则其实不重要,只是演示罢了,也不需要记住,因为实际过程中我们可能 使用的并不是上面的垃圾收集器的组合,可能使用ParNew垃圾收集器,可能使用G1垃圾收集器。场景很多,重要的是要在实际使用的时候有办法知道使用的 垃圾收集器对于对象分配有哪些原则,因为理解这些原则才是调优的第一步。下面列举一下对象分配的另外两条原则:

1、长期存活的对象将进入老年代。Eden区中的对象在一次Minor GC后没有被回收,则对象年龄+1,当对象年龄达到“-XX:MaxTenuringThreshold”设置的值的时候,对象就会被晋升到老年代中

2、Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到“-XX:MaxTenuringThreshold”设置要求的年龄

时间: 2024-08-05 05:55:09

Java虚拟机7:内存分配原则的相关文章

深入理解Java虚拟机笔记---内存分配与回收策略

Java技术体系中的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存.对象的内存分配往大的方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲(-XX:+UseTLAB,默认已开启),将按线程优先在TLAB上分配.少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置. 下面是几条主要的最普遍的内存分配规则: 1.对象优先在Ed

java中的内存分配原则

问题:Java中这些类.变量.字符串.方法,在内存中是怎样分配的? 首先,Java中的内存区域如下: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中.当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用.实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针! ◆堆:存放用new产生的数据.在堆中分配的内存,由J

《深入理解Java虚拟机》内存分配策略

上节学习回顾 1.判断对象存活算法:引用计数法和可行性分析算法 2.垃圾收集算法:标记-清除算法.复制算法.标记-整理算法 3.垃圾收集器: Serial:新生代收集器,采用复制算法,单线程. ParNew:新生代收集器,采用复制算法,多线程. Parallel Scavenge:新生代收集器,采用复制算法,多线程,注重吞吐量. Serial Old:老年代收集器,采用标记-整理算法,单线程. Parallel Old:老年代收集器,采用标记-整理算法,多线程,与Parallel Scaveng

java虚拟机的内存分配

java程序在执行时,jvm的内存执行方案.

Java虚拟机一 内存管理机制

Java虚拟机的内存管理主要分两点:内存分配以及内存回收.· 一.内存分配图: 注: 所占区域的大小与实际的内存大小比例并无直接关系. 解读: 1.如图,分成两种颜色的内存区域,其中蓝色的是线程隔离的数据区,也就是说每一个线程都有自己的这么一个区域存放自己的数据,而青色区域则是线程共享的,里面的数据为所有线程共有,原则上都有权限访问. 2.程序计数器: 用途:用来给程序导航指路的.这个是一块较小的内存空间,可以看做是当前线程执行的字节码的行号指示器.理解就是虚拟机把java源代码编译成了字节码,

Java虚拟机的内存管理

众所周知,Java程序员写的代码是没有办法控制Java对象的内存释放的,完全有JVM暗箱操作. 虽然程序员把内存的释放的任务都交给了Java虚拟机,但是并不代表Java程序就不存在内存泄漏. 反而,某程度上,当出现内存泄漏,Debug会变得难度更大. 所以,Java程序员,有必要去了解Java虚拟机对于内存的管理以及垃圾回收的机制. Java虚拟机是如何判断一个对象可以回收? 当一个对象没有被任何其他所引用时,这个对象被Java虚拟机视为可回收. 早起的虚拟机,使用引用计数的方法判断对象是否可回

Java变量以及内存分配

Java变量以及内存分配(非常重要) 堆栈 静态存储区域 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. 2.堆区(heap)— 由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收.注意它与数据结构中的堆是两回事,分配方式倒是类似于链表. 3.全局区(静态区)(static)— 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的

Java虚拟机:内存模型详解

版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 我们都知道,当虚拟机执行Java代码的时候,首先要把字节码文件加载到内存,那么这些类的信息都存放在内存中的哪个区域呢?当我们创建一个对象实例的时候,虚拟机要为对象分配内存,Java虚拟机又是如何配分内存的呢?这些都涉及到Java虚拟机的内存划分机制,今天我们就来探究一下Java虚拟机的内存模型. Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间,有的区域随

如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

程序小白在写代码的过程中,经常会不经意间写出发生内存溢出异常的代码.很多时候这类异常如何产生的都傻傻弄不清楚,如果能故意写出让jvm发生内存溢出的代码,有时候看来也并非一件容易的事.最近通过学习<深入理解java虚拟机-JVM高级特性与最佳实践>这本书,终于初步了解了一下java虚拟机的内存模型.本文通过写出使jvm发生内存溢出异常的代码来对自己的学习结果进行总结,同时也提醒自己以后写代码时候不要再跳进这个坑啦. java的内存管理是由java虚拟机自动进行管理的,并不需要程序员过多的手动干预

Java当中的内存分配以及值传递问题内存解析

首先必须说明作为Java程序员对于内存只要有大致的了解就可以了,如果你对Java当中的某一个知识点在不需要分析内存分配过程的情况下可以掌握,那就大可不必去研究内存.如果你对知识点已经掌握,那么你应该把更多的精力放在对业务逻辑的分析与设计上,这样的话你才可能这一行业走的更远. 好了废话不多说了,下面我带着大家先来简单的看一下Java当中所涉及的内存分配,接着我会以讲解Java当中的值传递问题,分析在代码执行的过程当中内存的状态. 一.Java当中所涉及到的内存分类 Java当中你知道这5种内存就够