一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
堆体系结构
JDK 1.7: 永久代使用的是堆空间内存
类加载器读取了类文件之后,需要把类、方法、常量变量放到堆内存中,保持所以引用类型的真实信息,方便执行器执行。
其中,堆内存分为3个部分:
- Young Generation Space,新生区、新生代
- Tenure Generation Space,老年区、老年代
- Permanent Space,永久区、元空间
JDK 1.8: 将永久区变成了元空间,使用的是物理内存
对象在堆中的生命周期
那么如何直观的了解对象在堆中的生命周期呢?
-
首先,新生区是类的诞生、成长、消亡的区域。一个类在这里被创建并使用,最后被垃圾回收器收集,结束生命。
-
其次,所有的类都是 在Eden Space被new出来的。而当Eden Space的空间用完时,程序又需要创建对象,JVM的垃圾回收器则会将Eden Space中不再被其他对象所引用的对象进行销毁,也就是垃圾回收(Minor GC)。此时的GC可以认为是轻量级GC。
-
然后将Eden Space中剩余的未被回收的对象,移动到 Survivor 0 Space,以此往复,直到Survivor 0 Space也满了的时候,再对Survivor 0 Space进行垃圾回收,剩余的未被回收的对象,则再移动到 Survivor 1 Space。Survivor 1 Space也满了的话,再移动至 Tenure Generation Space。
-
最后,如果Tenure Generation Space也满了的话,那么这个时候就会被垃圾回收(Major GC or Full GC)并将该区的内存清理。此时的GC可以认为是重量级GC。如果Tenure Generation Space被GC垃圾回收之后,依旧处于占满状态的话,就会产生我们场景的OOM异常,即
OutOfMemoryError
。
Minor GC的过程
前提:
- Survivor 0 Space,幸存者0区,也叫from区
- Survivor 1 Space,幸存者1区,也叫to区
- 新生区:养老区=1:2
- Eden:s0:s1=8:1:1
- 每次从伊甸园区经过GC幸存的对象,年龄(代数)会+1
其中,from区和to区的区分不是固定的,是互相交换的,意思是说,在每次GC之后,两者会进行交换,谁空谁就是to区。
- Eden Space、from复制到to,年龄+1。
首先,当Eden Space满时,会触发第一次GC,把还活着的对象拷贝到from区。而当Eden Space再次触发GC时,会扫描Eden Space和from,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to区(如果对象的年龄已经达到老年的标准,则移动至老年代区),同时把这些对象的年龄+1。 - 清空Eden Space、from
然后,清空Eden Space和from中的对象,此时的from是空的。 - from和to互换
最后,from和to进行互换,原from成为下一次GC时的to,原to成为下一次GC时的from。部分对象会在from和to中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。
总结一句话:GC之后有交换,谁空谁是to
这样也是为了保证内存中没有碎片,所以Survivor 0 Space和Survivor 1 Space有一个要是空的
HotSpot虚拟机的内存管理
不同对象的生命周期不同,其中98%的对象都是临时对象,即这些对象的生命周期大多只存在于Eden区。
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等。
虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap
(非堆内存),目的就是要和堆区分开。
对于HotSpot虚拟机而言,很多开发者习惯将方法区称为 “永久代(Permanent Gen)” 。但严格来说两者是不同的,或者说只是使用永久代来实现方法区而已,永久代是方法区(可以理解为一个接口interface
)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走。(字符串常量池,JDK1.6在方法区,JDK1.7在堆,JDK1.8在元空间。)
如果没有明确指明,Java虚拟机的名字就叫做HotSpot
永久区
永久区是一个常驻内存区域,用于存放JDK自身所携带的
Class
,Interface
的元数据(也就是上面文章提到的rt.jar
等),也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
JDK1.7
JDK1.8
在JDK1.8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
堆参数调优
注意:尽量配置初始内存和最大内存一样大,避免GC和应用程序争抢内存,进而导致内存忽高忽低产生停顿
获取虚拟机的相关内存信息:
public class JVMMemory {
public static void main(String[] args) {
// 返回 Java 虚拟机试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double) 1024 / 1024) + "MB");
// 返回 Java 虚拟机中的内存总量
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");
}
}
运行结果如下:虚拟机最大内存为物理内存的1/4,而初始分配的内存为物理内存的1/64
配置JVM内存参数:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
运行结果如下:
堆溢出 OutOfMemoryError
实例:
import java.util.Random;
public class OOMTest {
public static void main(String[] args) {
String str = "Atlantis";
while (true) {
// 每执行下面语句,会在堆里创建新的对象
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
}
}
}
把堆内存调成10M后,再一直new对象,导致Full GC也无法处理,直至撑爆堆内存,进而导致OOM
堆溢出错误
出现java.lang.OutOfMemoryError: Java heap space
异常,说明Java虚拟机的堆内存不够,造成堆内存溢出。原因有两点:
- ①Java虚拟机的堆内存设置太小,可以通过参数
-Xms
和-Xmx
来调整。 - ②代码中创建了大量对象,并且长时间不能被GC回收(存在被引用)。