内存分配

对象优先在Eden区分配

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

HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,

并且在进程退出的时候输出当前的内存各区域分配情况。

在代码清单1-1的testAllocation()方法中,尝试分配三个2MB大小和一个4MB大小的对象,

在运行时通过-Xms20M、-Xmx20M、-Xmn10M这三个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。

-XX:Survivor-Ratio=8决定了新生代中Eden区与一个Survivor区的空间比例是8∶1,从输出日志可看到“eden space 8192K、from space 1024K、

to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。

执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,产生这次垃圾收集的原因是为allocation4分配内存时,

发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。

垃圾收集期间虚拟机又发现已有的三个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),

所以只好通过分配担保机制将其提前转移到老年代去

这次收集结束后,4MB的allocation4对象顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,

老年代被占用6MB(被allocation1、2、3占用)。通过GC日志可以证实这一点。

代码清单1-1 对象优先分配在Eden区

    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
}

大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型便是很长的字符串,或者元素数量很庞大的数组。

大对象对虚拟机的内存分配来说是一个不折不扣的坏消息,更加坏的消息是遇到一群朝生夕灭的短命大对象,写程序时应注意避免。

在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们

而当复制对象时,大对象意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,

目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。

执行代码清单1-2后,可以看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的分配对象直接就分配在老年代中,

这是因为-XX:PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能与-Xmx之类的参数一样直接写3MB),

因此超过3MB的对象都会直接在老年代进行分配。

代码清单1-2 大对象直接进入老年代

    private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB]; //直接分配在老年代中
}

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,

如Parallel Scavenge并不支持这个参数。如果必须使用此参数进行调优,可考虑ParNew加CMS的收集器组合。

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

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策存活对象应该放在新生代或是老年代。

为做到这点,虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,

并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。若不能容纳,则由空间分配担保,移动到老年代中。

对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

动态对象年龄判断

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

无须等到-XX:MaxTenuringThreshold中要求的年龄。

执行代码清单1-3程序后,并将设置-XX:MaxTenuring-Threshold=15,发现运行结果中Survivor占用仍然为0%,

而老年代比预期增加了6%,也就是说allocation1、allocation2对象都直接进入了老年代,并没有等到15岁的临界年龄。

因为这两个对象加起来已经到达了512KB,并且它们是同年龄的,满足同年对象达到Survivor空间一半的规则。

我们只要注释掉其中一个对象的new操作,就会发现另外一个就不会晋升到老年代了。

代码清单1-3 动态对象年龄判定

    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];
}

空间分配担保

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

如果不成立,则先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败;

如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;

如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

这里的冒险指的是由于新生代使用标记-复制算法,为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,

当出现大量对象在Minor GC后仍然存活的情况——最极端的情况就是内存回收后新生代中所有对象都存活,这时便需要老年代进行分配担保,

把Survivor无法容纳的对象直接送入老年代。

在JDK 6 Update 24之后,-XX:HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,

虽然源码中定义了-XX:HandlePromotionFailure参数,但是在实际虚拟机中已经不会再使用它。JDK 6 Update 24之后的规则变为

只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。

调优案例

堆外内存溢出

堆外内存区域只有在JVM 发生Full GC或是程序中手动调用System.gc()时才会被进行垃圾回收。

但如果JVM打开了-XX:+DisableExplicitGC开关,System.gc()就会被禁止使用,在程序一直没进行Full GC时,

虽然堆外内存中有许多可回收内存,但也不得不抛出OOM。

JVM 常见内存区域如下,这些内存总和受到本机内存和操作系统进程最大内存的限制:

  • 堆外内存,Redis保存BigKey时抛出堆外内存溢出异常,Redis 内部使用Netty,而Netty又使用了Java NIO分配堆外内存,堆外内存不足导致OOM。

    可通过-XX:MaxDirectMemorySize调整堆外内存大小。
  • 线程堆栈,可通过-Xss调整大小,内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)

    或者OutOfMemoryError(如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)。
  • Socket缓存区:每个Socket连接都有Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接

    多的话这块内存占用也比较可观。如果无法分配,可能会抛出IOException:Too many open files异常。
  • JNI代码, 如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用Java虚拟机的本地方法栈和本地内存的。
  • 虚拟机和垃圾收集器:虚拟机、垃圾收集器的工作也是要消耗一定数量的内存的。

虚拟机进程崩溃

如开放API 操作较耗时,在上次操作还未结束,调用方又通过异步发送了许多请求,

时间一长,导致等待的线程和Socket 接口越来越多,到超过虚拟机承受能力时导致虚拟机进程崩溃。

使用合适的数据结构

不正确的数据结构,在数据量较大且单个元素占用内存较小时,使用Map 构建会造成很大的空间浪费。

如Map<Integer, Integer>, 有效数据仅为8个字节,而创建Map.Entry 等的开销远大于此。

合理规划堆内存、合理编码

合理分配年轻代(Eden、survivor比例)、老年代内存比例,降低Full GC频率。

可以通过以下几个参数要求虚拟机生成GC日志:-XX:+PrintGCTimeStamps(打印GC停顿时间)、

-XX:+PrintGCDetails(打印GC详细信息)、-verbose:gc(打印GC信息,输出内容已被前一个参数包括,可以不写)、-Xloggc:gc.log。

Minor GC 耗时较短,影响不大,而Full GC 相对来说耗时较长, 所以应该将程序 Full GC的频率控制得足够低。

控制Full GC频率的关键是老年代的相对稳定,这取决应用中的对象是否为符合'朝生夕死'原则,

程序应尽量避免成批量的、长时间存在的大对象产生,如此才能保障老年代的稳定。

选择合适的垃圾收集器

针对不同的应用场景,选择适合的垃圾收集器,例如有的注重低时延,有的注重高吞吐量。

JVM 内存分配、调优案例的更多相关文章

  1. JVM内存分配调优

    Reference: https://time.geekbang.org/column/article/108139 参考指标 GC频率:⾼频的FullGC会给系统带来⾮常⼤的性能消耗,虽然Minor ...

  2. 【JVM.4】调优案例分析与实战

    之前已经介绍过处理Java虚拟机内存问题的知识与工具,在处理实际项目的问题时,除了知识与工具外,经验同样是一个很重要的因素.本章会介绍一些具有代表性的案例. 本章的内容推荐还是原文全篇看完的好,实在不 ...

  3. JVM内存分配和垃圾回收以及性能调优

    JVM内存分配策略 一:堆中优先分配Eden 大多数情况下,对象都在新生代的Eden区中分配内存.而新生代会频繁进行垃圾回收. 二:大对象直接进入老年代 需要大量连续空间的对象,如:长字符串.数组等, ...

  4. Android性能调优篇之探索JVM内存分配

    开篇废话 今天我们一起来学习JVM的内存分配,主要目的是为我们Android内存优化打下基础. 一直在想以什么样的方式来呈现这个知识点才能让我们易于理解,最终决定使用方法为:图解+源代码分析. 欢迎访 ...

  5. 《深入理解Java虚拟机》-----第5章 jvm调优案例分析与实战

    案例分析 高性能硬件上的程序部署策略 例 如 ,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU.16GB物理内存,操作系统为64位CentOS 5.4 , Resin ...

  6. jvm调优思路及调优案例

    jvm调优思路及调优案例 ​ 我们说jvm调优,其实就是不断测试调整jvm的运行参数,尽可能让对象都在新生代(Eden)里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时 ...

  7. 性能调优案例分享:jvm crash的原因 1

    性能调优案例分享:jvm crash的原因   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq: ...

  8. jvm系列(六):Java服务GC参数调优案例

    本文介绍了一次生产环境的JVM GC相关参数的调优过程,通过参数的调整避免了GC卡顿对JAVA服务成功率的影响. 这段时间在整理jvm系列的文章,无意中发现本文,作者思路清晰通过步步分析最终解决问题. ...

  9. JVM(五) 生产环境内存溢出调优

    1.gc配置参数 1.1 控制台打印gc日志 -verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC(详细的gc信息) 1.2 输出gc日志到指定文件 - ...

随机推荐

  1. 36、网卡绑定bond

    注意:虚拟机需要网卡模式为同一模式,否则无法进行通信: 36.1.mode0(平衡负载模式): 平时两块网卡均工作,且自动备援,但需要在与服务器本地网卡相连的交换机设备上进行端口聚合来支持绑定技术. ...

  2. 49、django工程(cookie+session)

    49.1.介绍: 1.cookie不属于http协议范围,由于http协议无法保持状态,但实际情况,我们却又需要"保持状态",因此cookie就是在这样一个场景下诞生. cooki ...

  3. 图的存储与遍历C++实现

    1.图的存储 设点数为n,边数为m 1.1.二维数组 方法:使用一个二维数组 adj 来存边,其中 adj[u][v] 为 1 表示存在 u到 v的边,为 0 表示不存在.如果是带边权的图,可以在 a ...

  4. WebSocket实现前后端通讯

    WebSocket实现前后端通讯 长安如梦里,何日是归期. 简介:我们上线了一个商城项目,移交运营团队使用之后,他们要求商城有新订单来的时候同时加上声音提示,让她们可以及时知道有单来了.我这边想了想, ...

  5. Docker:部署PXC8.0集群时,启动容器报错New joining cluster node didn‘t find all needed SSL artifacts

    使用docker部署mysql PXC集群8.0版本,启动第二个节点的时候遇到报错,New joining cluster node didn't find all needed SSL artifa ...

  6. js 简单实现获取短信按钮倒计时60秒

    <!DOCTYPE html><html lang="en"><head> <meta http-equiv="Content- ...

  7. python twain 扫描

    def multiScan(self): """ Scan and return an array of PIL objects If no images, will r ...

  8. DEV C++5.11编译没有结果提示

    点击"视图"菜单--选择"浮动报告 窗口"

  9. C语言:字符编码

    C语言是 70 年代的产物,那个时候只有 ASCII,各个国家的字符编码都还未成熟,所以C语言不可能从底层支持 GB2312.GBK.Big5.Shift-JIS 等国家编码,也不可能支持 Unico ...

  10. SQL2008 合并多个结构相同的表的所有数据到新的表

    select * into tikua from (select * from tiku20210303 union all select * from tiku) a