一篇聊聊 JVM 优化:堆

一、Java 堆概念
1、简介
对于 Java 应用程序来说, Java 堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java 堆是被所 有线程共享 的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。“几乎”是指从实现角度来看, 随着 Java 语 言的发展, 现在已经能看到些许迹象表明日 后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上 分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说 Java 对象实例都分配在堆上也渐渐变得不是 那么绝对了。

2、堆的特点
(1)是 Java 虚拟机所管理的内存中最大的一块。
(2)堆是 jvm 所有线程共享的。 堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
(3)在虚拟机启动的时候创建。
(4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。 (5)Java 堆是垃圾收集器管理的主要区域。
(6)因此很多时候 java 堆也被称为“GC 堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器 基本都采用分代收集算法,所以 Java 堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor 空间、To Survivor 空间。
(7)java 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms 和-Xmx 控制)。
(8)方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
(9)如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常
3、设置堆空间大小
1. 内存大小-Xmx/-Xms
使用示例: -Xmx20m -Xms5m
说明: 当下 Java 应用最大可用内存为 20M, 最小内存为 5M
total Memory 和最大的内存之间还是存在一定 差异的,就是说 JVM 一般会尽量保持内存在一个尽可能低的层面,而非贪婪做法按照最大的内存来进行分配。
其实 JVM 在分配内存过 程中是动态的, 按需来分配的。
4、堆的分类
现在垃圾回收器都使用分代理论,堆空间也分类如下:
在 Java7 Hotspot 虚拟机中将 Java 堆内存分为 3 个部分:
青年代 Young Generation
老年代 Old Generation
永久代 Permanent Generation

在 Java8 以后,由于方法区的内存不再分配在 Java 堆上,而是存储于本地内存元空间 Metaspace 中,所以永久代就不 存在了,在几天前(2018 年 9 约 25 日)Java11 正式发布以后,我从官网上找到了关于 Java11 中垃圾收集器的官方文档, 文档中没有提到“永久代”,而只有青年代和老年代。

二、年轻代和老年代
1.JVM 中存储 java 对象可以被分为两类:
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分 成 1 个 Eden Space 和 2 个 Suvivor Space(from 和 to)。
2)年老代(Tenured Gen):年老代主要存放 JVM 认为生命周期比较长的对象(经过几次的 Young Gen 的垃圾回收后仍 然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
2.配置新生代和老年代堆结构占比
默认 -XX:NewRatio=2 , 标识新生代占 1 , 老年代占 2 ,新生代占整个堆的 1/3
修改占比 -XX:NewPatio=4 , 标识新生代占 1 , 老年代占 4 , 新生代占整个堆的 1/5
Eden 空间和另外两个 Survivor 空间占比分别为 8:1:1
可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8 几乎所有的 java 对象都在 Eden 区创建, 但 80%的对象生命周期都很短,创建出来就会被销毁

从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生 代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以 通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域 是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。
三、对象分配过程
JVM 设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关, 因此还需要考虑 GC 执行完内存回收后是否存在空间中间产生内存碎片。
分配过程:
1.new 的对象先放在伊甸园区。该区域有大小限制
2.当伊甸园区域填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊 甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区
3.然后将伊甸园区中的剩余对象移动到幸存者 0 区
4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者 0 区的,如果没有回收,就会放到幸存者 1 区
5.如果再次经历垃圾回收,此时会重新返回幸存者 0 区,接着再去幸存者 1 区。
6.如果累计次数到达默认的 15 次,这会进入养老区。 可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
7.养老区内存不足是,会再次出发 GC:Major GC 进行养老区的内存清理
8.如果养老区执行了 Major GC 后仍然没有办法进行对象的保存,就会报 OOM 异常



分配对象流程
版权声明: 本文为 InfoQ 作者【高端章鱼哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/40cfb58ffa273066d27000543】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论