JVM(二) Heap体系、堆内存分析

新生代

堆分代体系

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异常
老年代的GC过程叫作MajorGC。

对象提升规则

 在age=16的时候,进升到老年代,多次GC后依然存活的对象会去老年代


Major GC

Major GC 和 Old GC 完全等价,老年代也可能会空间不足,会进行Major GC

老年代收集(Major GC/Old GC):严格来说,Major GC只是老年代的垃圾收集,属于部分收集。

Major GC触发机制

MajorGC采用标记清除算法(详见下一篇)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值