1.  垃圾收集器与内存分配策略

  Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:

  • 给对象分配内存;
  • 回收分配给对象的内存。

  对象的内存分配,往大方向上讲就是在堆上的分配,对象主要分配在新生代的Eden区上。少数也可能分配在老年代,取决于哪一种垃圾收集器组合,还有虚拟机中的相关内存的参数设置。下面先介绍一下JVM中的年代划分:新生代、老年代、永久代(JDK1.8后称为元空间)。

1.1 JVM堆的结构分析(新生代、老年代、永久代)

  HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from(S1)和to(S2)),具体可参下面的JVM内存体系图。Eden和Survival的默认分配比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理,后面会说到),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

  因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

  在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

   在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  永久代主要用于存放静态文件,Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应
用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持永久代空间来存放这些运行过程中新增的类。永久代大小通过-XX: MaxPermSize = <N> 进行设置。

1.2 对象在Eden上分配

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

  下面做一个测试程序demo,详细说明,新生代对象在Eden区的内存分配情况。尝试分配3个2MB大小和一个4MB大小的对象,在运行时候通过VM参数设置(看代码注释),限制java堆大小为20MB,不可扩展,其中10M分配给新生代,10M分给老年代,需要注意的是Eden区与一个Survivor区的空间比例是8:1,从输出结果也可以看出"eden space 8192K,from space 1024K,to space 1024K"的信息,新生代的总空间为9216KB(endn区+1个survivor区的总容量)。测试代码如下:

public class Minor_GC {
private static final int _1MB = 1024 * 1024; /*
* VM 参数配置: -Xms20M
* -Xmx20M
* -Xmn10M
* -XX:+PrintGCDetails
*/ public static void main(String args[]){
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]; // 出现一次GC回收 }
}

  输出GC日志如下:

  上述参数可以看出: 执行main函数中,分配给allocation4对象时候发生了一次Minor GC(新生代回收),这次GC的结果是新生代内存7684k---->365k,然而堆上总内存的占用几乎没有改变,因为allocation1、allocation2、allocation3都存活,本次回收基本上没有找到可回收的对象。分析如下:

  1. 新生代一共被分配10M,其中Enden:8M,survivor:2M(From:1M,To:1M);
  2. 给allocation4分配内存时,Eden已经被占用6M(allocation1、2、3共6M,所以剩下2M),所以内存已经不够用了---->发生GC;
  3. 然而,6M放不进Survivor的From(只有1M),所以只能通过分配担保机制提前转移到老年代

  这次GC结束后,Eden中有4M的allocation4对象(一共8M,被占用50%左右),survivor为空闲,老年代为6M(被allocation1、2、3占用),日志中显示为6146k,其中老年代采用Mark-sweep(标志清除)回收的方法。

  [注意]区别新生代(Minor GC)和老年代(Full GC):

  1. 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特性,所以Minor GC非常的频繁,一般回收速度也比较快;
  2. 老年代GC(Major GC/Full GC):指发生在老年代的垃圾回收动作,出现Major GC,经常会有至少一次的MinorGC(因为对象大多数都是先在Eden分配空间的,但是并非绝对)。Major GC回收的速度会比Minor GC慢十倍以上(因为Minor GC回收一般都是大面积的回收采用复制算法;而Major GC没有额外空间为他担保,只能采用标记-清理方法),这两者的回收思路是相反的,是一个空间换时间和时间换空间的关系。

1.3 大对象直接进入老年代

  大对象是指需要大量内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组(byte[ ]就是典型的大对象)。出现达对象很容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

  虚拟机提供了一个-XX:pretenureSize Threshold()参数,令大于这个设置直的对象直接在老年代分配。这样做的目的是避免Eden和Survivor区之间发生大量的内存复制(新生带采用复制的方法完成GC)。下面做个测试demo说明问题:

public class Major_GC {
private static final int _1MB = 1024 * 1024;
/*
* VM 参数配置: -Xms20M
* -Xmx20M
* -Xmn10M
* -XX:+PrintGCDetails
* -XX:PretenureSizeThreshold=3145728(等于3M)
*/
public static void main(String args[]){
byte[] allocation;
allocation = new byte[4 * _1MB]; // 直接会分配到老年代
}
}

  运行后可以看到,内存会直接在老年代分配。[说明]:这里不给出运行结果,以免产生误导,因为在Parallel Scavenge收集器是不支持PretenureSizeThreshold这个参数的,得不到这样的结论。

1.4 长期存活对象将进入老年代

  Java虚拟机采用分代收集的思想来管理虚拟机内存。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并且经过第一次Minor GC后仍然存活,并且能被Survivor的话,将被移动到Survivor空间中,并且对象年龄增加到一定程度(默认15岁),就会被晋升到老年代。对晋升到老年代的对象的阈值可以通过-XX:MaxTenuringThreshold设置。

  下面给出测试demo:

public class LongTimeExistObj {
private static final int _1MB = 1024 * 1024; /*
* VM 参数配置: -Xms20M
* -Xmx20M
* -Xmn10M
* -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/ public static void main(String args[]){
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];
}
}

  

  测试结果如下所示:

1.5 动态对象年龄判定

  虚拟并不是永远都要求对象年龄必须达到MaxTenuringThreshold才能晋升为老年代的,如果在Survivor的空间相同年龄的所有对象大小总和大于Survivor空间的一半时,年龄大于或者等于该年龄的对象直接静如老年代,无需要等到MaxTenuringThreshold中要求的年龄。

  下面做一个动态年龄测试demo:

public class LongTimeExistObj {
private static final int _1MB = 1024 * 1024; /*
* VM 参数配置: -Xms20M
* -Xmx20M
* -Xmn10M
* -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void main(String args[]){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[_1MB/4]; // 使得allocation1 + allocation2 > survivor空间的一半(0.5M)
allocation2 = new byte[_1MB/4]; allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
}

  测试结果如下:

  执行代码结果中,可以看出:Survivor区占用空间仍然为0(from = 0,to = 0);而老年代的内存使用为5M,而其他对象都为4M,可以知道,alloccation1和allocation2都在没有达到15岁的时候就提前进入了老年代。验证了我们的结论---->在Survivor的空间相同年龄的所有对象大小总和大于Survivor空间的一半时,年龄大于或者等于该年龄的对象直接静如老年代

1.6 空间分配担保

  在发生Minor GC之前,虚拟机会先检查老年代可用的连续空间是否大于所有新生代的总空间,如果大于的话,那么这个GC就可以保证安全,如果不成立的,那么可能会造成晋升老年代的时候内存不足。在这样的情况下,虚拟机会先检查HandlePromotionFailure设置值是否允许担保失败,如果是允许的,那么说明虚拟机允许这样的风险存在并坚持运行,然后检查老年代的最大连续可用空间是否大于历次晋升老年代对象的平均大小,如果大于的话,就执行Minor GC,如果小于,或者HandlePromotionFailure设置不允许冒险,那么就会先进行一次Full GC将老年代的内存清理出来,然后再判断。

  上面提到的风险,是由于新生代因为存活对象采用复制算法,但为了内存利用率,只使用其中的一个Survivor空间,将存活的对象备份到Survivor空间上,一旦出现大量对象在一次Minor GC以后依然存活(最坏的计划就是没有发现有对象死亡需要清理),那么就需要老年代来分担一部分内存,把在Survivor上分配不下的对象直接进入老年代,因为我们不知道实际上具体需要多大内存,我们只能估算一个合理值,这个值采用的方法就是计算出每次晋升老年代的平均内存大小作为参考,如果需要的话,那就提前进行一次Full GC.

  取平均值在大多数情况下是可行的,但是因为内存分配的不确定性太多,保不定哪次运行突然出现某些大对象或者Minor GC以后多数对象依然存活,导致内存远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。这样的情况下,担保失败是要付出代价的,大部分情况下都还是会将HandlePromotionFailure开关打开,毕竟失败的几率比较小,这样的担保可以避免Full GC过于频繁,垃圾收集器频繁的启动肯定是不好的。

  上面很繁琐(详细),实在看不下去就看图吧:

文中关于新生代、老年代的概念部分内容参考了博文:https://www.cnblogs.com/E-star/p/5556188.html

本文参考书籍:《深入理解java虚拟机》

深入理解java虚拟机----->垃圾收集器与内存分配策略(下)的更多相关文章

  1. 深入理解JAVA虚拟机 垃圾收集器和内存分配策略

    引用计数算法 很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象就是不可能再被使用的 ...

  2. [深入理解Java虚拟机]<垃圾收集器与内存分配策略>

    Overview 垃圾收集考虑三件事: 哪些内存需要回收? 什么时候回收? 如何回收? 重点考虑Java堆中动态分配和回收的内存. Is Object alive? 引用计数法 给对象添加一个引用计数 ...

  3. Java虚拟机垃圾收集器与内存分配策略

    Java虚拟机垃圾收集器与内存分配策略 概述 那些内存须要回收,什么时候回收.怎样回收是GC须要完毕的3件事情. 程序计数器.虚拟机栈与本地方法栈这三个区域都是线程私有的,内存的分配与回收都具有确定性 ...

  4. Java虚拟机 垃圾收集器与内存分配策略

    说起GC,我们要思考的主要有三件事 哪些内存需要回收 那些已经“死去”的对象,那么哪些对象“死”,哪些对象“活”呢,有个简单的办法 引用计数法,但是没法解决循环依赖问题 所以Java虚拟机采用的是可达 ...

  5. Java虚拟机--垃圾收集器和内存分配

    垃圾收集器和内存分配 程序计数器.虚拟机栈.本地方法栈这三个区域和线程的生命周期一致,所以方法结束或者线程结束时,内存自然就跟着回收了.Java堆和方法区,只有在程序处于运行期间才能知道会创建哪些对象 ...

  6. 深入理解JVM(三)垃圾收集器和内存分配策略

    3.1 关于垃圾收集和内存分配 垃圾收集和内存分配主要针对的区域是Java虚拟机中的堆和方法区: 3.2 如何判断对象是否“存活”(存活判定算法) 垃圾收集器在回收对象前判断其是否“存活”的两个算法: ...

  7. 深入理解JVM:垃圾收集器与内存分配策略

    堆里面存放着Java世界差点儿全部的对象实例,垃圾收集器在对堆进行回收前.第一件事情就是要确定这些对象之中哪些还存活,哪些已经死去.推断对象的生命周期是否结束有下面几种方法 引用计数法 详细操作是给对 ...

  8. 《深入理解Java虚拟机》(三)垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 详解 3.1 概述 本文参考的是周志明的 <深入理解Java虚拟机>第三章 ,为了整理思路,简单记录一下,方便后期查阅. 3.2 对象已死吗 在垃圾收集器进行回收 ...

  9. 《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略

    第三章 垃圾收集器与内存分配策略 3.1 概述 哪些内存需要回收 何时回收 如何回收 程序计数器.虚拟机栈.本地方法栈3个区域随线程而生灭. java堆和方法区的内存需要回收.   3.2 对象已死吗 ...

随机推荐

  1. WinForm 菜单控件

    一:MenuStrip 菜单条 MenuStrip 是应用程序菜单条的容器. 二:ToolStripMenuItem 像上面图中, 文件 格式 等这些菜单当中的一级菜单以及文件中的 新建 打开 分割条 ...

  2. Linux 源码编译Python 3.6

    Linux 源码编译Python 3.6 1.操作系统以及版本显示 # uname -sr Linux 3.10.0-514.el7.x86_64 # uname -sr Linux 3.10.0-5 ...

  3. C++ stl 运用(深层)

    1.multiset(set差不多) (1)erase删除,删除指针和键值是不同的. 键值的话是删除所有,指针的话是那个位置的值. (2)统计单个键值个数. (3)对于q.begin(),q.end( ...

  4. 在你的网站实现qq登陆(php)

    这个qq的oauth2.1有个坑,坑了我半天,后来查了不少资料总算弄通了,现在把详细步骤记录下来. 步骤一.登陆http://connect.qq.com/     步骤二.创建应用.我创建的是wen ...

  5. MarkDown的快速入门

    简介 简单的去解释MarkDown就是html,但是将html中的元素用符号去代替使用.本文用的编译软件是Atom(神器),不多说直接上图看效果. 语法 文本 列表 区块 分割符 表格 链接 mark ...

  6. 记Javascript的编写方式的全新学习

    前言 这次有幸参与前端的工作,对于前端开发学习了不少新知识,在此记录一下相比之前,完全不同的Javascript编写方式. 原来的编写方式 之前也是写过Javascript,就是常见的.js 文件写函 ...

  7. 使用js编写一个简单的运动框架

    下班后,,没事捣鼓捣鼓个人的小爱好. 首先,说明我的这个运动框架(css所有属性)也是常见的框架一种,健壮性并不是太好,对于新手学习倒是挺好,,若是大神,老司机请拐弯. 上来,我们先定义一个区块,然后 ...

  8. 利用Python循环(包括while&for)各种打印九九乘法表

    一.for循环打印九九乘法表 #注意:由于缩进在浏览器不好控制,请大家见谅,后续会有图片传入. 1.1 左下角 for i in range(1,10): for j in range(1,i+1): ...

  9. 基于QEMU的ARM Cortex-A9开发板Vexpress-ca9的Linux内核的编译和运行

    宿主机:Ubuntu16.04 x64(Linux内核4.4.0) 交叉编译工具链:gcc-arm-linux-gnueabiarm-linux-gcc:4.4.3QEMU:2.5.0Linux ke ...

  10. Container With Most Water 容器最大水容量

    描述 Given n non-negative integers a1, a2, ..., an, where each represents a point at coordinate (i, ai ...