JVM理论:(二/1)内存分配策略

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

对象的分配可能有以下几种方式:

1、JIT编译后被拆散为标量类型并间接地栈上分配

2、对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配

3、少数情况下也会直接分配在老年代

参考下图:

  

5种内存分配策略

1、对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  先来看看两种GC类型的定义

  新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

  老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(Parallel Scavenge里可设置直接进行Major GC,所以并非绝对),Major GC的速度一般会比Minor GC慢10倍以上。 

经典案例代码:private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
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];  // 出现一次Minor GC
}

运行结果:
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  先看看设置的虚拟机参数,-Xms20M -Xmx20M -Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,所以Eden有8MB的空间,一个Survivor区有1MB的空间,-XX:+PrintGCDetails表示虚拟机会在垃圾回收时打印内存回收日志。

  testAllocation()方法中尝试分配3个2MB大小和1个4MB大小的对象,在执行分配allocation4对象的语句时,会发现Eden区已经被占用了6MB,剩余2M空间,已不足以分配allocation4所需的4MB内存,因此会发生一次Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入只有1MB大小的Survivor空间,所以根据分配担保机制会提前转移到老年代去。

  根据打印的GC日志,6651K->148K(9216K), 6651K->6292K(19456K)也可以看出,新生代从6651K变为148K,但总内存占用量几乎没有减少,也证实allocation1、allocation2、allocation3三个对象都是存活的,只是被转移到了老年代,虚拟机几乎没有找到可回收的对象。这次GC后,程序执行完的结果是,Eden被allocation4占用4MB,Survivor空闲,老年代被allocation1、allocation2、allocation3三个对象占用6MB。

  所以,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

*//堆空间分布日志
Heap
  def new generation   total 9216K, used 4326K    //年轻代分布
  eden space 8192K,  51% used ≈ 4MB
  from space 1024K,  14% used ≈ 148KB,应该是之前的垃圾,忽略
  to space 1024K,   0% used
  tenured generation   total 10240K, used 6144K    //老年代分布
  the space 10240K,  60% used ≈ 2048KB*3
  compacting perm gen  total 12288K, used 2114K    //永久代(方法区)分布,本例不考虑
  the space 12288K,  17% used

  再从上例中的内存对分布的角度来重新推测过程:当分配到allocation4时,发现eden空间不足,这时进行GC,但由于Survivor只有1MB,存不下allocation1、allocation2、allocation3中的任意对象,所以这三个对象都直接进入老年代,最后allocation4分配在Eden,占4MB。

2、大对象直接进入老年代

  大对象是指需要大量连续内存空间的Java对象,典型大对象有长字符串和数组,大对象对虚拟机的内存分配来说是一个坏消息,写程序时还要避免创建一些朝生夕死的短命大对象,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置他们。

  虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样避免再Eden及两个Survivor区间发生大量的内存复制,这个参数只对Serial和ParNew两款收集器有效,如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的组合。

3、根据对象年龄判定进入老年代

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

代码示例:
private static final int _1MB = 1024 * 1024;
/**
 * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
 * -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
 * -XX:+PrintTenuringDistribution
 */
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
    byte[] allocation1, allocation2, allocation3;
    allocation1 = new byte[_1MB / 4];
    //什么时候进入老年代决定于XX:MaxTenuringThreshold设置
    allocation2 = new byte[4 * _1MB];
    allocation3 = new byte[4 * _1MB];
    allocation3 = null;
    allocation3 = new byte[4 * _1MB];
}
堆空间分布日志
以MaxTenuringThreshold=1参数来运行的结果:
Heap
def new generation   total 9216K, used 4178K   //年轻代分布
eden space 8192K,  51% used ≈ 4MB
from space 1024K,   0% used
to   space 1024K,   0% used
tenured generation   total 10240K, used 4500K  //老年代分布
the space 10240K,  43% used ≈ 4MB+256KB
compacting perm gen  total 12288K, used 2114K  //永久代分布先忽略
the space 12288K,  17% used
 以MaxTenuringThreshold=1的情况来分析,当要分配allocation3时,Eden空间不足,准备开始GC,照理Survivor(1MB)是能容纳下allocation1(256KB)的,但因为MaxTenuringThreshold=1,allocation1对象在第二次GC发生时进入老年代,所以Survivor区没有对象,老年代存放了allocation1和allocation2,eden中存的是allocation3。
以MaxTenuringThreshold=15参数来运行的结果:
Heap
def new generation   total 9216K, used 4582K   //年轻代分布
eden space 8192K,  51% used ≈ 4MB
from space 1024K,  39% used ≈ 256KB
to   space 1024K,   0% used
tenured generation   total 10240K, used 4096K  //老年代分布
the space 10240K,  40% used = 4MB
 以MaxTenuringThreshold=15的情况来分析,当要分配allocation3时,Eden空间不足,准备开始GC,这次MaxTenuringThreshold=15,所以allocation1不会直接进入老年代,会到Survivor区域内,Survivor存不了allocation2,所以allocation2被移动到老年代,最后在Eden分配allocation3。

4、Survivor空间中相同年龄的对象过半直接进入老年代

  上一点讲到MaxTenuringThreshold参数,但虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

private static final int _1MB = 1024 * 1024;
/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
 * -XX:+PrintTenuringDistribution
 */
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
  byte[] allocation1, allocation2, allocation3, allocation4;
  allocation1 = new byte[_1MB / 4];
  // allocation1+allocation2大于survivo空间一半
  allocation2 = new byte[_1MB / 4];
  allocation3 = new byte[4 * _1MB];
  allocation4 = new byte[4 * _1MB];
  allocation4 = null;
  allocation4 = new byte[4 * _1MB];
}

Heap
def new generation   total 9216K, used 4178K      //年轻代
eden space 8192K,  51% used ≈ 4MB
from space 1024K,   0% used
to   space 1024K,   0% used
tenured generation   total 10240K, used 4756K     //老年代
the space 10240K,  46% used ≈ 4MB+256KB*2

  根据堆内存分布日志分析,当要分配allocation4时,发现Eden空间不够进行GC,由于-XX:MaxTenuringThreshold设置为15,且Survivor区是可以容纳下allocation1和allocation2的,照理说这两个对象应该是要进入Survivor区的,但这两个对象对没有进入Survivor而是直接进入了老年代,这就是因为allocation1和allocation2两对象相加为512KB,达到了Survivor的一半,且它们同年龄,所以会直接进入老年代。Survivor区不够存allocation3也直接进入老年代,最后在Eden上分配allocation4。

5、空间分配担保

  关于分配担保机制JDK6前后有一点差别。  

  JDK6之前,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  如果检查老年代最大可用的连续空间大于新生代所有对象总空间,那么Minor GC可以确保是安全的。

  如果检查不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

      如果允许担保失败,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,这里取平均值是因为在实际完成内存回收前无法明确知道有多少对象会存活下来,所以也存在一定风险。

      如果大于历次平均大小,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,Minor GC若执行失败也会进行执行一次Full GC,这样的失败绕的圈子是最大的;

      如果小于历次平均大小,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

  对以上的步骤归纳一下,先看老年代的可用空间能否容下新生代的所有对象,不能的话看是否开启了分配担保机制,允许就先执行Minor GC,否则直接进行Full GC。大部分情况下还是会将HandlePromotionFailure开启分配担保,避免频繁Full GC。

  JDK6后,HandlePromotionFailure不再影响到虚拟机的空间分配担保策略,变为只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

参考链接:

  https://www.jianshu.com/p/fa3569127416

  https://segmentfault.com/a/1190000004606059

原文地址:https://www.cnblogs.com/zjxiang/p/9218202.html

时间: 2024-11-08 13:40:31

JVM理论:(二/1)内存分配策略的相关文章

jvm系列(二)jvm垃圾收集器与内存分配策略

众所周知,在java语言中,内存分配和回收是由jvm自动管理的.因此内存的分配和回收也是jvm三大功能之一.垃圾收集器(GC)需要完成三件事情: 哪些内存需要回收? 什么时候进行回收? 如何回收? 本篇博客将解答jvm是如何处理以上三个问题的.值得注意的是,java运行时数据区中的程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行进栈和出栈的操作,每一个栈帧分配多少内存基本上是在类结构确定下来的时候就已知的.因此以上三个区域不需要过多考

JVM总结(二):JVM的内存分配策略

这节我们总结一下JVM中的内存分配策略.目录如下: 内存分配策略 对象优先在新生代Eden分配 大对象直接进入老年代 长期存活的对象将进入老年代 动态对象年龄判定 空间分配担保 内存分配策略 Java技术体系中所提倡的自动内存管理可以归结于两个部分:给对象分配内存以及回收分配给对象的内存. 我们都知道,Java对象分配,都是在Java堆上进行分配的,虽然存在JIT编译后被拆分为标量类型并简介地在栈上进行分配.如果采用分代算法,那么新生的对象是分配在新生代的Eden区上的.如果启动了本地线程分配缓

垃圾收集器与内存分配策略(二)之垃圾收集算法

垃圾收集器与内存分配策略(二)--垃圾收集算法 Java JVM 垃圾回收 简单了解算法的思想 1. 标记-清除算法 标记-清除算法分为标记和清除二个阶段:首先标记出需要回收的对象(详见上一节的可达性分析找出存活对象),在标记完成后统一回收所有被标记的对象. 缺点: 1.标记和清除二个过程的效率都不高 2.空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作. 2. 复制算法 复制算

JVM性能调优 第七章 内存分配策略

理解了jvm内存分配策略不仅是程序性能调优的重要知识,还能够给养成自己一种良好的代码思路,一个程序的代码差异往往都是在这里体现出来的. 一.对象优先分配到Eden区域   一般来说,新创建的对象都会直接分配到Eden区域,如果Eden区域内存不够,JVM就会触发GC(垃圾回收),一般来说在JVM中有3种GC: Minor GC:指发生在新生代的垃圾收集动作,非常频繁,速度较快. Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会

垃圾收集器与内存分配策略(二)

垃圾收集算法简介 1.标记-清除算法       标记-清除算法主要分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一进行回收.对象的标记过程在垃圾收集器与内存分配策略(一)中已经介绍过. 存在的问题:一是效率问题,标记和清除的效率都不高:二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的内存而不得不提前触发另一次垃圾收集动作. 2.复制算法       复制算法:它将内存按照容量划分为大小

jvm笔记2--垃圾收集器与内存分配策略

垃圾收集器与内存分配策略 Java运行时,内存的各个部分中,程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作.每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的.因此这几个区域不需要过多考虑回收的问题,因为线程结束时,内存自然就跟着回收了. Java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才知道会创建哪些对象,这部分内存的分配

JVM内存分配策略,及垃圾回收算法

本人免费整理了Java高级资料,一共30G,需要自己领取;传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q 说起垃圾收集(Garbage Collection, GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开发效率上获得了惊人的便利.理解GC,对于理解JVM和Java语言有着非常重要的作用.并且当我们需要排查各种内存溢

深入理解JVM内存分配策略

理解JVM内存分配策略 三大原则+担保机制 JVM分配内存机制有三大原则和担保机制 具体如下所示: 优先分配到eden区 大对象,直接进入到老年代 长期存活的对象分配到老年代 空间分配担保 对象优先在Eden上分配 如何验证对象优先在Eden上分配呢,我们进行如下实验. 打印内存分配信息 首先代码如下所示: public class A { public static void main(String[] args) { byte[] b1 = new byte[4*1024*1024]; }

java虚拟机学习-JVM内存管理:深入垃圾收集器与内存分配策略(4)

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项技术当做Java语言的伴生产物.事实上GC的历史远远比Java来得久远,在1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.当Lisp还在胚胎时期,人们就在思考GC需要完成的3件事情:哪些内存需要回收?什么时候回收?怎么样回收? 经过半个世纪的发展,目前的内存分配策略