4.1 堆体系结构
一个JVM只存在一个堆内存,堆内存的大小是可以可以调节的,类加载器读取了类文件之后,需要把类,方法,常量变量放到堆内存中,保持所有引用类型的真实信息;
其中堆内存分位三部分 :新生+养老+永久
注意:Java8之后 永久区就变成了元空间
再还有注意的点就是 :
堆内存在逻辑上分为新生+养老+永久 ;在物理上分为新生+养老
4.2 对象在堆中的生命周期
(1) 首先,新生区是类的诞生,成长和消亡的区域。一个类在这里被创建并使用,最终被垃圾回收器收集,结束生命。
(2)然后所有类都是在Eden Space区被new出来的。而当Eden Space用完时程序需要又要创建对象,JVM垃圾回收器则会将Eden Space中不再被其他对象所引用的对象进行销毁,也就是垃圾回收(Minor GC)。此时的GC可以认为是轻量级GC。
(3)然后将Eden Space剩余的未被回收的对象移动到Survivor 0 Space,以此往复,直到Survivor 0 Space也慢了,再对Survivor 0 Space
进行垃圾回收,剩余未被回收的对象,则再移动到Survivor 1 Space,Survivor 1 Space
也满了的话,再移动至Tenure Generation Space
。
(4)最后如果Tenure Generation Space
也满了的话,那么这个时候就会被垃圾回收(Major GC or Full GC)并将该区的内存清理。此时的GC可以认为是重量级GC。如果Tenure Generation Space
被GC垃圾回收之后,依旧处于占满状态的话,就会产生我们场景的OOM
异常,即OutOfMemoryError
。
4.3 Minor GC的过程
Survivor 0 Space,也叫做幸存者0区,也叫from区
Survivor 1 Space,也叫做幸存者1区,也叫to区
注意:from区和to区的名分和位置是不固定的。是相互交换的,意思是说,在每次GC之后,两者都会进行交换,谁空谁为to区
接下来,继续讲的更明白 这时候需要记住两个比例
新生区中三者的比例 8:1:1 新生区和养老区的比例为1:2
(1) Eden Space
、from
复制到to
,年龄+1。
首先当Eden Space满的时候会触发第一次GC,把还活着的对象复制到from区。而当Eden Space再次触发GC的时候,会扫描Eden Space
和from区,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to区
(注意只要产生GC
Eden Space必会清空)
注意:
如果对象的年龄已经达到老年的标准,则移动至老年代区,同时把这些对象的年龄加+1
(2)清空Eden Space
、from
然后清空Eden Space和
from区中的对象,此时from是空的
(3)from
和to
互换
最后,from和to进行互换,原from成为下一次GC时的to,原to成为下一次GC时的from。部分对象会在from和to中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。
总结一句话,GC之后有交换,谁空谁是to。
这样也是为了保证内存中没有碎片,所以Survivor 0 Space
和Survivor 1 Space
有一个要是空的。
4.4 HotSpot虚拟机的内存管理
注意:不同对象的生命周期不同,其中98%的对象都是临时对象,即这些对象的生命周期大多只存在于Eden区
实际而言,方法中(Method Aera)和堆一样是各个线程共享的内存区域,它是存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等。虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆内存),目的就是要和堆区分开
对于HotSpot虚拟机而言,很多开发者习惯将方法区称为 “永久代(Permanent Gen)” 。但严格来说两者是不同的,或者说只是使用永久代来实现方法区而已,永久代是方法区(可以理解为一个接口interface)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走。(字符串常量池,JDK1.6在方法区,JDK1.7在堆,JDK1.8在元空间。)
如果没有明确指明,Java虚拟机的名字就叫做HotSpot
。
4.4 永久区
永久区是一个常驻内存区域,用于存放JDK自身所携带的class,Interface的元数据(也就是上面文中的提到的rt.jar包等),也就是说它存储的是运行环境所必须的类信息,被装载此区域的数量是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域的所占用的内存。
(1) JDK 1.7
(2) JDK 1.8
在JDK1.8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。元空间和永久代的最大区别在于:永久代所使用的JVM的堆内存,但是java8以后的元空间并不存在虚拟机而是使用物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据方法 native memory ,字符串池和类的静态放入Java堆中,这样可以加载多少类的元数据就不再由MaxPermSize
控制, 而由系统的实际可用空间来控制。
4.5 堆参数调优
在进行调优之前。我们在代码里可以获取到jvm的信息
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");
}
}
运行结果如下:
这个3607.5MB
和243.5MB
是怎么算出来的?看下图就明白了,虚拟机最大内存为物理内存的1/4,而初始分配的内存为物理内存的1/64。
IDEA中如何配置JVM内存参数?在【Run】->【Edit Configuration…】->【VM options】中,输入参数-Xms1024m -Xmx1024m -XX:+PrintGCDetails
,然后保存退出。
注意:JVM的初始内存和最大内存一般怎么分配
初始内存和最大内存一定是一样大,理由是避免GC和应用程序争抢内存,进而导致内存忽高忽低产生停顿。
运行结果如下:
4.6 堆溢出 OutOfMemoryError
现在我们来演示一下OOM
,首先把堆内存调成10M后,再一直new对象,导致Full GC也无法处理,直至撑爆堆内存,进而导致OOM
堆溢出错误,程序及结果如下:
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);
}
}
}
注意:如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够,造成堆内存溢出。原因有两点:
①Java虚拟机的堆内存设置太小,可以通过参数-Xms和-Xmx来调整。
②代码中创建了大量对象,并且长时间不能被GC回收(存在被引用)。