新生代
堆分代体系
Heap 堆:一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存从GC的角度分为三部分:
新生代(年轻代)、老年代、永久代(持久代)。永久区(非堆)就是方法区
其中 新生代默认占 1/3堆空间,老年代默认占2/3堆空间 ,永久代占非常少的堆空间
为什么需要把Java堆分代?不分代就不能正常工作了吗
其实不分代完全可以,分代的唯一理由就是优化GC性能
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。
>新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0/sl)构成,to总为空。
>老年代:存放新生代中经历多次GC仍然存活的对象。
新生代
新生区是对象的诞生、成长、消亡的区域,一个对象在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace)
幸存区有两个: 0区和1区 (From区和To区)
- Eden区默认占8/10新生代空间
- ServivorFrom区和ServivorTo区默认分别占 1/10新生代空间
所有的对象都是在伊甸区被new出来的,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存者0区。
注意:0区和1区会交换,保证接收伊甸区的是幸存者0区
1.若幸存者0区也满了,再对该区(0区)进行垃圾回收,然后剩余对象移动到 幸存者1 区。
2.那如果1 区也满了呢?再次垃圾回收(回到第上步,,0区和一区永远都有其中是一个为空),满足条件后再移动到养老区。
3.若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
Minor GC
YGC和Minor GC完全等价
JVM在进行GC时,并非每次都对三个内存(新生代、老年代;方法区)区域一起回收,大部分时候回收的都是指新生代。
新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾收集,属于部分收集
年轻代GC触发机制
MinorGC 采用复制算法实现(详见下一篇)
From区和To区
from区和to区会交换,保证空的区永远都是to区,下一次gc的时候。对象去放到to区
伊甸园区满的时候会触发Minor GC,将伊甸园区和幸存者区的对象进行GC,survior区是被动进行gc的
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。空间充足默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁。 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象 会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色
也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。 不管怎样,都会保证名为To的Survivor区域是空的。
Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲(to)和活动区间(from),而另外80%的内存,则是用来给新建对象分配内存的。
一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
老年代
老年代
1、经历多次GC仍然存在的对象(默认是15次),老年代的对象比较稳定,不会频繁的GC.
2、Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代3、在老年代没有内存空间可分配时,会抛出Out Of Memory异常
对象提升规则
在age=16的时候,进升到老年代,多次GC后依然存活的对象会去老年代
Major GC
Major GC 和 Old GC 完全等价,老年代也可能会空间不足,会进行Major GC
老年代收集(Major GC/Old GC):严格来说,Major GC只是老年代的垃圾收集,属于部分收集。
Major GC触发机制
Full GC
整堆收集(FULL GC):新生代、老年代、方法区都要垃圾收集,注意涉及到了方法区
Full GC触发机制
说明:Full GC是开发或调优中尽量要避免的。
永久代
永久代存什么
永久代是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到永久代中。
首先明确:只有HotSpot才有永久代。
方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
永久代 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有永久代
元空间 是 JDK8及之后, HotSpot 虚拟机 对
方法区
的新的实现。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致永久代被占满。
深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
============
域信息
============
方法信息
non-final 的类变量
需要注意的是:
全局常量:static final 被声明为final的类变量的处理方法则不同,每个全局常量在编译(变成class文件的过程)的时候就会被分配了。
运行时常量池
还有一个非常重要的是:运行时常量池
常量池
在字节码文件中:内部有一个常量池(class文件中信息量最大的)
为什么提供一个常量池呢
运行时常量池
在方法区中的运行时常量池,对应字节码文件中的常量池
不同jdk版本方法区
Jdk1.6及之前: 有永久代,静态变量和字符串常量池都在永久代
Jdk1.7: 有永久代,但已经逐步“去永久代”,字符串常量池,静态变量从永久代中移除到了堆中
Jdk1.8及之后: 无永久代,字符串常量池、静态变量仍在堆中,类型信息、字段、方法、常量保存在本地内存的元空间,不在虚拟内存了
元空间
Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用内存空间决定。
1.为永久代设置空间大小是很难确定的,动态类加载过多,会出现Perm区的OOM。
“java.lang.OutOfMemoryError:PermGen space”
2.为永久代调优是很困难的
jdk7 为什么要调整 String Table
因为永久代的回收频率很低,在full gc的时候才会触发。(full gc是老年代空间不足、永久代空间不足时才会触发)。这就导致String Table 回收效率不高。
而实际在开发中,会有大量的字符串被创建,回收效率低的话,会导致永久代内存不足。
放在堆里的话,相对来说回收的频率高一些
堆内存分析
1、内存溢出OOM
内存溢出OOM(针对堆空间)
内存空间不够时,进行独占式的Full GC之后内存还不够,就会报OOM。是程序崩溃的罪魁祸首之一
造成空闲不足的2个原因:
当然,也不是在任何情况下垃圾收集器都会被触发的
比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集 并不能解决这个问题,所以直接抛出OutofMemoryError,不会触发GC
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有2:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象【集合、数组】,并且长时间不能被垃圾收集器收集(存在被引用)
2、内存泄漏
严格意义:
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
宽泛意义:
但实际情况一些疏忽会导致对象的生命周期变得很长甚至导致OOM,叫做宽泛意义上的“内存泄漏”
尽管内存泄漏不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OOM, 导致程序崩溃。
=====---==-==-
内存泄漏举例
一些提供close的资源未关闭导致内存泄漏 数据库连接dataSourse.getConnection(),网络连接socket和io连接必须手动close,否则是不能被回收的。
3、查看堆内存详情
配置JVM参数
-XX:+PrintGCDetails 可以打印堆内存信息+GC的情况
-Xmx50m -Xms30m -XX:+PrintGCDetails
配置后如下:
运行如下程序
System.out.print("最大堆大小:"); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:"); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("=================================================="); byte[] b = null; for (int i = 0; i < 10; i++) { b = new byte[1 * 1024 * 1024];//1MB的数组 }
打印结果:
新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory();不信的话就跟我学学,要相信科学。。。
4、GC演示
jvm参数配置不变
运行如下程序:
public static void main(String[] args) { System.out.println("=====================Begin========================="); System.out.print("最大堆大小:Xmx="); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("==================First Allocated==================="); byte[] b1 = new byte[5 * 1024 * 1024]; System.out.println("5MB array allocated"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("=================Second Allocated==================="); byte[] b2 = new byte[10 * 1024 * 1024]; System.out.println("10MB array allocated"); System.out.print("剩余堆大小:free mem="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("当前堆大小:total mem="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); System.out.println("=====================OOM========================="); System.out.println("OOM!!!"); System.gc(); byte[] b3 = new byte[40 * 1024 * 1024]; }
先说明一个点:
当前堆大小=新生代+老年代
剩余堆大小=当前堆大小-jvm自己也要存很多额外的数据
谈谈你对System.gc() 的理解
1.显示触发Full GC,同时对老年代和新生代进行回收。
2.但是不能确保它什么时候执行(免责声明),仅仅是提醒JVM垃圾回收器要进行一次垃圾回收
5、dump文件分析
MAT是Memory Analyzer 的简称,它是一款功能强大的java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
是免费的,基于Eclipse开发的,可以在http:www.eclipse.org/mat/ 下载并使用MAT
生成dump文件
方式一:命令行使用jmap
方式二:使用JVisualVM导出
方式三:
运行参数改成
-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\tmp
-XX:HeapDumpPath:生成dump文件路径。
堆里的内存数据持久化到这里了
Dumping heap to E:\tmp\java_pid29536.hprof ...
=====================
生成的这个文件怎么打开?
方式一:使用MAT工具(推荐)
MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
方式二:jvisualvm.exe 分析堆转储文件
文件-->装入-->选择要打开的文件即可
1