一、堆(Heap)的概述
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。同时,堆内存的大小是可以调节的。《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,但在堆里还可以划分线程私有的缓冲区 ( Thread Local Allocation Buffer, TLAB)
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上(The heap is the run-time data area from which memory for all class instances and arrays is allocated )。其实从实际使用角度看,“几乎”所有的对象实例都在这里分配内存。数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
为了说明对象与栈和堆的关系,我们用一段代码:
public class SimpleHeap {
//属性、成员变量
private int id;
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimpleHeap sl = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
}
}
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除(因为垃圾收集的时候才会去扫描垃圾)。堆是GC ( Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
二、堆内存
2.1 内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
- Young Generation Space 新生区 Young/New 又被划分为Eden区和survivor区
- Tenure generation space 养老区 Old/ Tenure
- Permanent Space 永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
- Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
2.2 常用的相关JVM参数
2.2.1 堆空间大小设置
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置。
- -Xms用于表示堆区的起始内存,等价于-XX:InitialHeapSize
- -Xmx则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始内存大小 = 物理电脑内存大小 / 64;最大内存大小 = 物理电脑内存大小 / 4
下面来实践一下,写一段代码看看堆内存大小:
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
}
运行结果:
-Xms : 15M
-Xmx : 247M
系统内存大小为:0.9375G
系统内存大小为:0.96484375G
修改一下:
-Xms : 580M
-Xmx : 580M
系统内存大小为:36.25G
系统内存大小为:2.265625G
2.2.2 老年代和新生代比例设置
存储在VM中的Java对象可以被划分为两类:一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)。其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1空间(有时也叫做from区、to区)。
配置新生代与老年代在堆结构的占比使用 -XX:NewRatio
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。
2.2.3 年轻代内的比例设置
在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1,开发人员可以通过选项 -XX:SurvivorRatio 调整这个空间比例。比如-XX:SurvivorRatio=8。
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。IBM公司的专门研究表明,新生代中80% 的对象都是“朝生夕死”的。
可以使用选项"-Xmn"设置新生代最大内存大小。这个参数一般使用默认值就可以了。
2.2.4 利用VisualVM直观查看
我们可以直观地看一下这些比例。随便运行一段不结束的程序:
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("我只是来打个酱油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们设置堆的大小为600M:
打开visualVM:
我们将堆区设置为600M,默认-XX:NewRatio=2,新生代占1/3,即200M;老年代占2/3,即400M。默认-XX:SurvivorRatio=8,Eden区占8/10,即160M,两个Survivor区各占1/10,即20M。
2.2.5 关闭自适应的内存分配策略
JVM默认是开启自适应内存分配策略的,也就是说,新生代的8:1:1 并不会被严格遵守,而是会由虚拟机动态地调整。我们可以使用 -XX:-UseAdaptiveSizePolicy 来关闭自适应的内存分配策略。
三、对象分配过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中等生内存碎片。
3.1 对象分配的一般过程
下面我们来看看对象分配的过程:
new的对象先放伊甸园区。此区有大小限制,当伊甸园的空间填满时,程序又需要创建对象,此时JVM的垃圾回收器将对伊甸园区进行对象进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,然后将伊甸园中的剩余对象移动到幸存者0区。幸存者区中会有年龄计数器,每活过一次GC,年龄就会+1。
如果再次触发垃圾回收,伊甸园区幸存的对象和幸存者0区中依然没有被回收的对象,就会放到幸存者1区。
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。当幸存者区中的对象超过15岁时,就会被分配到老年区。进入老年代的年龄是可以设置的,设置参数:
-XX:MaxTenuringThreshold=<N> 。
在养老区,相对悠闲。当养老区内存不足时,再次触发GC(Major GC),进行养老区的内
存清理。
若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
3.2 对象分配的全过程
这里要注意的是,当new出特别大的对象(Eden放不下)时,首先要进行一次YGC,还是放不下,就直接放到老年代了;如果Servivor区满了,而Eden区没满,此时不会触发YGC。多余的对象会直接放到老年代。
GC频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
四、Young GC、Major GC和 Full GC
4.1 概念区分
JVM在进行GC时,并非每次都对三个内存(新生代、老年代;方法区)区域一起回收,大部分时候回收的都是新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
目前,只有CMS GC会有单独收集老年代的行为。
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集
4.2 年轻代Gc(Minor GC)触发机制
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden满,Survivor满不会引发GC。
因为Java对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
4.3 老年代GC (Major GC/Full GC)触发机制
出现Major GC,经常会伴随至少一次的Minor GC(但非绝对的),也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC。Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。如果Major GC后,内存还不足,就出现OOM。
触发Full GC执行的情况有如下五种:
(1)调用System.gc()时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space0 (From Space)区向survivor space1 (ToSpace)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
总结起来就一点:老年代或方法区空间不够用了。