### 1、堆的概述
Java堆在JVM中的地位不言而喻,它位于JVM的运行时数据区内。

对于JVM中的堆区,需要明确以下几点:
- 一个进程会产生唯一的JVM实例,而一个JVM实例只存在一个堆内存,
- 堆是Java内存管理的核心区域。
- Java堆是在JVM启动的时候就被创建,其空间大小也就确定了,是JVM
管理的最大一块内存空间。|
- 堆内存的大小是可以调节的。
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在
逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Al location Buffer, TLAB)
- 《Java虚拟机规范》中对Java堆的描述是:几乎所有的对象实例以及数组都应
当在运行时分配在堆上。(The heap is the run-time data area from which memory for a1l class instances and arrays is allocated )
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。栈、堆、方法区之间的关系如下:

- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆是GC( Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域。
#### 1.1、堆内存的划分
|Java 7及之前堆内存逻辑划分|Java 8及之后堆内存逻辑划分|
|-------|-------|
|新生区(Young Generation Space)|新生区(Young Generation Space)|
|养老区(Tenure Generation Space)|养老区(Tenure Generation Space)|
|永久区(Permanent Space)|元空间(Meta Space)|
注:新生区 == 新生代 == 年轻代;养老区 == 老年代 == 老年区;永久区 == 永久代。

而在堆中的新生代,又会分为伊甸园区(Eden)、Survivor区(S0+S1)。

### 2、设置堆内存大小和OOM
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项" -Xmx”和"-Xms"来进行设置。
- -Xms用于表示堆区的起始内存,等价于-XX: InitialHeapSize。这里设置的只是(新生代—+老年代)的初始内存大小,不包含永久代或元空间大小。
- -Xmx则用于表示堆区的最大内存,等价于-XX :MaxHeapSize。这里设置的只是(新生代—+老年代)的最大内存大小,不包含永久代或元空间大小。
注:-X表示jvm的运行参数;-ms表示memory start;-mx表示memory max。
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。默认情况下,初始内存大小=物理电脑内存大小/64;最大内存大小=物理电脑内存大小/4。
#### 2.1 IDEA设置堆初始大小和最大大小。
首先,定义一个类HeapDemo:
```java
package online.miantiao.java;
/**
* @ClassName: HeapDemo
* @Description TODO
* @User: 面条
* @Date 2020/7/11 11:13
* @Version: 1.0
**/
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(2000000);
}
}
```
之后编辑运行设置:初始内存大小为10m,最大内存大小为10m。
<div align=center>

</div>
设置完后运行程序,用Jvisualvm工具和Visual GC插件查看内存情况。
<div align=center>

</div>
这里显示了我们设置的堆内存初始大小和最大大小。
<div align=center>

</div>
这里显示了内存分配的情况:可以看出Eden Space为1m;Survivor 0为1m;Survivor 1为1m;Old Gen为7m;这四部分内存的总和为10m,刚好等于设置的初始和最大内存大小。
#### 2.2 OOM举例
```java
package online.miantiao.java;
import java.util.ArrayList;
import java.util.Random;
/**
* 设置内存大小:-Xms600m -Xmx600m
* @ClassName: OOMTest
* @Description TODO
* @User: 面条
* @Date 2020/7/11 12:00
* @Version: 1.0
**/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
// try {
// Thread.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//不停地往byte数组中添加数据,导致老年代空间不断被占用,最终导致OOM
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
```
运行结果:报OOM异常---->java堆空间溢出
```
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at online.miantiao.java.Picture.<init>(OOMTest.java:32)
at online.miantiao.java.OOMTest.main(OOMTest.java:23)
```
### 3、年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持-致。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(01dGen)。其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间 (有时也叫做from区和to区)。
<div align=center>

</div>
#### 3.1 设置新生代、老年代的大小
在Java虚拟机中,新生代、老年代的大小一般不会直接指定,而是通过配置新生代与老年代在堆结构的占比,通常用-XX:NewRatio=* 命令来设置。默认情况下,-XX:NewRatio=2, 表示新生代占1,老年代占2,新生代占整个堆的1/3。如果设置-XX: NewRatio=4,则表示新生代占1,老年代占4,新生代占整个堆的1/5。
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1。当然开发,人员可以通过选项-XX:SurvivorRatio=*来调整这个空间比例。比如-XX:SurvivorRatio=8。
几乎所有的Java对象都是在Eden区被new出来的。并且绝大部分的Java对象的销毁都在新生代进行了。
> IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
事实上,也可以使用选项-Xmn设置新生代最大内存大小,但是这个参数一般使用默认值就可以了。如果同时设置了-XX:NewRatio=* 和 -Xmn,则以-Xmn为准。
#### 3.2 新生代、老年代大小设置实例
首先定义类:
```java
package online.miantiao.java;
/**
* @ClassName: HeapDemo
* @Description TODO
* @User: 面条
* @Date 2020/7/11 11:13
* @Version: 1.0
**/
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(2000000);
}
}
```
然后设置参数:
- -Xms600m -Xmx600m:设置堆内存初始大小为600m,最大大小为600m。
- -XX:NewRatio=4:设置新生代和老年代的比例为1:4,即新生代占堆内存的1/5,老年代占堆内存的4/5。
- -XX:SurvivorRatio=8:设置新生代中Eden和S0与S1的比例为8:1:1。

运行程序通过jvisualvm工具查看内存分配情况:

在jvm参数这一列,就可以看到设置的参数的值。

在Visual GC中,可以看到
- 堆内存大小 = 480m + 96m + 12m + 12m = 600m;
- 新生代:老年代 = (96m + 12m + 12m) : 480m = 1:4;
- Eden:S0:S1 = 96m : 12m : 12m = 8 : 1 : 1。
### 4、对象分配过程
#### 4.1 常规过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。主要可以总结出如下步骤:
- 1)、new的对象先放伊甸园区。此区有大小限制。
- 2)、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁并将剩余对象移动到幸存者0区。再加载新的对象放到伊甸园区。

- 4)、如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

- 5)、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
- 6)、啥时候能去养老区呢?当一个对象经历的回首次数达到一定阈值时,就会被放入到养老区。这个阈值默认是15次,也可以通过-XX :MaxTenuringThreshold=<N>进行设置。

**针对幸存者s0,s1区的总结**:复制之后有交换,谁空谁是to,反之为from.
**关于垃圾回收**:在新生区的垃圾回收非常频繁,很少在养老区收集,几乎不在永久区/元空间收集。
#### 4.2 特殊情况
当伊甸园区出发一次YGC时,不被销毁的对象由于太大,在幸存者区放不下,那么会直接放入养老区;如果养老区也放不下,就会直接报OOM。
<div align = "center">

</div>
### 5、Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC) ,一种是整堆收集(Fu1l GC)。
- **部分收集**:不是完整收集整个Java堆的垃圾收集。其中又分为:
- **新生代收集**(Minor GC / Young GC) :只是新生代的垃圾收集;
- **老年代收集**(Major GC / 0ld GC): 只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。值得注意的是,很多时候Major GC会和Fu1ll GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- **混合收集**(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为。
- **整堆收集**(Fu1l GC):收集整个java堆和方法区的垃圾收集。
#### 5.1、年轻代GC(Minor GC)触发机制
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC(每次 Minor GC会清理年轻代的内存)。因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。Minor GC会引发STW, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
**STW**是Java中Stop-The-World机制的简称,它的原理是指在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。
#### 5.2、老年代GC(Major GC/Ful1 GC)触发机制
对象从老年代消失时,我们说“Major GC”或“Fu11 GC”发生了。出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC。Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。如果Major GC后,内存还不足,就报OOM了。
#### 5.3、Fu1l GC触发机制
触发Ful1 GC执行的情况有如下五种:
- 调用System. gc()时,系统建议执行Fu11 GC,但是不必然执行;
- 老年代空间不足时;
- 方法区空间不足时;
- 通过MinorGC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0 (From Space) 区向survivor space1 (ToSpace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小时。
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短-一些。
### 6、堆空间分代思想
Java的堆空间被划分为新生代、老年代、元空间/永久代三大部分。
JDK1.7及之前划分为:
- 新生代:有Eden、两块大小相同的Survivor (又称为from/to,s0/s1) 构成,并且to区总为空。
- 老年代:存放新生代中经历多次GC仍然存活的对象;
- 永久代;
<div align=center>

</div>
JDK1.8及之后划分为:
- 新生代:有Eden、两块大小相同的Survivor (又称为from/to,s0/s1) 构成,并且to区总为空。
- 老年代:存放新生代中经历多次GC仍然存活的对象;
- 元空间;
<div align=center>

</div>
之所以会进行这样的代划分,是因为:经研究,不同对象的生命周期不同,70%-99%的对象是临时对象。分代的唯一理由就是优化Gc性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一个地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
### 7、内存分配策略
针对不同年龄段的对象分配原则如下所示:
- 优先分配到Eden区;
- 大对象直接分配到老年代。实际开发中应尽量避免程序中出现过多的大对象;
- 长期存活的对象分配到老年代;
- 动态对象年龄判断。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- 空间分配担保。通过XX:HandlePromotionFailure参数设置来实现。
### 8、为对象分配内存:TLAB
TLAB即Thread Local Allocation Buffer的缩写,JDK为了提升内存分配的效率,会为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,这块空间称为TLAB (Thread Local Allocation Buffer),其大小由JVM根据运行情况计算而得,可通过-XX:TLABWasteTargetPercent来设置TLAB可占用的Eden Space的百分比,默认值为1%。JVM将根据这个比率、线程数量及线程是否频繁分配对象来给每个线程分配合适大小的TLAB空间。在TLAB上分配内存时不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配,如果对象过大或TLAB空间已用完,则仍然在堆上进行分配。因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,可通过在启动参数上增加-XX:+PrintTLAB来查看TLAB空间的使用情况。
<div align=center>

</div>
这样做的原因是:
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为**快速分配策略**。
尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。在程序中,开发人员可以通过选项“-XX :UseTLAB”设置是否开启TLAB空间。默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“-XX: TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
<div align=center>

</div>
### 9、堆空间的参数设置
JVM中堆空间常用的参数都有:
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal : 查看所有的参数的最终值(可能会存在修改,不再是初始值)
- -Xms : 初始堆空间内存( 默认为物理内存的1/64)
- -Xmx : 最大堆空间内存(默认为物理内存的1/4)
- -Xmn : 设置新生代的大小。(初始值及 最大值)
- -XX:NewRatio : 配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
- -XX:MaxTenuringThreshold : 设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails :输出详细的Gc处理日志
- 打印gc简要信息:①-XX:+PrintGC ②-verbose:gc
- -XX:HandlePromotionFailure : 是否设置空间分配担保
### 10、堆是分配对象的唯一选择吗?
在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
>随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH (GC invisible heap) 技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
如何将堆上的对象分配到栈,需要使用**逃逸分析**手段。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸。例如作为调用参数传递到其他地方中。
不发生逃逸举例:
```java
public viod myMethod(){
V v = new V();
...
...
v = null;
}
//这个方法中的V的对象在方法中创建,并且只在方法内部使用,因此没有发生逃逸。
//则该对象可以分配在栈上,随着方法执行结束占空间就被移除。
```
发生逃逸举例:
```java
public static StringBuffer createStringBuffer(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
//这里StringBuffer的对象sb在方法内部创建,并在内部使用后作为返回值返回,
//因此发生了逃逸,故不能在栈上分配
```
如果想让上述例子中的sb在栈上分配,则需要重写成这样:
```java
public static String createStringBuffer(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb。toString();
}
//这里StringBuffer的对象sb在方法内部创建,并在内部使用,而返回值返回为一个String对象。
```
在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以:
- 通过选项“-XX: +DoEscapeAnalysis" 显式开启逃逸分析。
- 通过选项“-XX: +PrintEscapeAnalysis" 查看逃逸分析的筛选结果。
因此,为了使应用程序的运行效率更高,**开发中能使用局部变量的,就不要使用在方法外定义**。
### 11、代码优化
使用逃逸分析,编译器可以对代码做如下优化:
- **栈上分配**。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- **同步省略**。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- **分离对象或标量替换**。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
#### 11.1、栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
常见的逃逸出方法的场景有:
- 给成员变量赋值;
- 方法返回值;
- 实例引用传递。
栈上分配举例:
```java
package online.miantiao.java;
/**
* @ClassName: StackAllocation
* @Description TODO
* @User: 面条
* @Date 2020/7/13 21:26
* @Version: 1.0
* 栈上分配测试
* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();//调用含有非逃逸对象的方法。
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User {
}
}
```
***
使用-XX:-DoEscapeAnalysis关闭逃逸分析,设置内存大小为1G,输出gc详情,运行程序,结果为:
```
花费的时间为: 95 ms
```
可以发现进行了GC,这时通过jvisualvm工具查看内存占用情况,发现User对象有10000000个,并且都放在堆中的Eden区。


***
使用-XX:+DoEscapeAnalysis关闭逃逸分析(默认是打开的),并设置内存大小为50m,输出gc详情,运行程序,结果为
```
花费的时间为: 4 ms
```
可以看出,打开逃逸分析后,User对象的创建划分到栈上,减少了Eden区的GC,提高了程序的运行效率。


#### 11.2、同步省略
线程同步的代价是相当高的,同步的后果是降低并发性和性能。在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
***
例如:
```java
public void f() {
object hollis = new object () ;
synchroni zed (hollis) {
System. out.println(hollis);
}
}
```
代码中对hollis这个对象进行加锁,但是hollis对 象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:
```java
public void f() {
object hollis = new object () ;
System. out.println(hollis);
}
```
#### 11.3、标量替换
**标量**(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做**聚合量**(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
可以使用参数-XX:+EliminateAllocations:开启标量替换(默认是打开的),允许将对象打散分配在栈。上。|

循序渐进Java虚拟机-堆