内存分配

对象优先在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频率的关键是老年代的相对稳定,这取决应用中的对象是否为符合'朝生夕死'原则,

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

选择合适的垃圾收集器

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

最新文章

  1. 身份证验证JS代码
  2. AFN中的PATCH 和 DELETE 请求方式
  3. Git Day02,工作区,暂存区,回退,删除文件
  4. 25Spring_事务管理的两种机制
  5. git 删除分支和删除文件夹
  6. 【动态规划】Vijos P1493 传纸条(NOIP2008提高组第三题)
  7. 推送:腾迅信鸽 VS Bmob
  8. HDU 4521 间隔》=1的LIS 线段树+dp
  9. django template出错
  10. LeetCode OJ 217.Contains Duplicate
  11. python 中的input()和raw_input()功能与使用区别
  12. DaemonSet 案例分析 - 每天5分钟玩转 Docker 容器技术(130)
  13. Django admin自定制功能
  14. SmartSql Map
  15. 动态规划——Freedom Trail
  16. webpack(6)-模块热替代&amp;tree shaking
  17. Java实现Base64加密
  18. [20180606]如何dump数据库里面的汉字.txt
  19. 第17月第26天 setImageForState变形
  20. 吴恩达讲了干货满满的一节全新AI课,全程手写板书充满诚意非常干货

热门文章

  1. mysql字符集utf8和utf8mb4区别
  2. 如果给IIS添加防火墙入站配置,支持外部或者局域网访问
  3. 『无为则无心』Python序列 — 24、Python序列的推导式
  4. SpringBoot | 1.2 全注解下的Spring IoC
  5. Springboot:Springboot+Swagger2集成服务
  6. 2012年第三届蓝桥杯C/C++程序设计本科B组省赛 密码发生器
  7. 【故障公告】redis 服务器宕机引发博客站点故障
  8. Linux | 通配符 &amp; 转义符
  9. Chirp Z-Transform
  10. python 12篇 mock接口之flask模块