跟着锋哥学Java

深入浅出JVM(三)之堆内存(Heap)

1.堆内存简介

1.1什么是堆内存?

堆内存是java内存中的一种,它的作用是用于存储java中的实例对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。类加载器读取了类文件后,保存所有引用类型的真实信息,以方便执行器执行

1.2堆内存的特点

1.堆这块区域是JVM中最大的,应用的对象和数据都是存在这个区域.

2.堆这块区域也是线程共享的,也是 gc 主要的回收区.

3.一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的

4.堆内存的存储特点----先进先出,后进后出

5.堆可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,但缺点是,由于要在运行时动态分配内存,存取速度较慢

1.3堆内存不够的原因:

       1. Java虚拟机的堆内存设置不够,可能存在内存泄漏,内存溢出问题或者堆空间的大小不合理,在处理比较可观的数据量,没有显式指定JVM堆大小或者指定数值偏小,通过参数一Xms、一Xmx来调整。

      2.代码中创建了大量大对象,并且长时间不能被垃圾收集器回收(存在被引用)

2.堆内存的组成部分

1.堆内存逻辑上由新生代 ( Young ),老年代 ( Old )和永久代(Perm)组成,其中新生代 ( Young )又被划分为:Eden、From Survivor(From区)和To Survivor(T区)三个区域

2.从JDK8开始,Metaspace(元空间)替代了永久代

 

 

3.注意事项(重要)

(1) 堆内存逻辑上由新生代 ( Young ),老年代 ( Old )和永久代(Perm)或者Metaspace(元空间)组成. (逻辑上:堆=新生+老年+永久或者元空间)

(2)堆内存物理上由新生代 ( Young )和老年代 ( Old )组成,也就是堆内存的大小等于新生代的大小+老年代的大小(物理上:堆=新生+老年)

3.堆内存的空间比例分配

1.堆内存的空间分别分给了新生代和老年代,在默认情况下新生代 ( Young ) 占 1/3 的堆空间,老年代 ( Old ) 占2/3 的堆空间

2.新生代 又被划分为:Eden、From Survivor(From区)和To Survivor(To区)三个区域,默认情况下其中 Eden 区占8/10 的新生代空间,from 和to 占 1/10 的新生代空间.(Eden: from : to = 8 : 1 : 1)

3.JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化,也可以设置参数-XX:-UseAdaptiveSizePolicy

4.新生代

新生代是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命;新生代又分为两部分:伊甸区(Eden)和两个幸存者区(From区,To区)

4.1新生代的垃圾回收(MinorGC)

  (1)所有的类都是在伊甸区被new出来的,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1 区。那如果1 区也满了,再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。

   (2)Minor GC和Full GC 有什么不同呢?

         Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

         Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

  

4.1.1新生的垃圾回收过程(复制->清空->互换)

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法

1.eden,From区复制到To区,年龄+1

首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到From区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1

2.清空,eden区,From区

然后,清空Eden和From区中的对象,也即复制之后有交换,谁空谁是to

3.To区和From区互换

最后,To区和From区互换,原To区成为下一次GC时的From区部分对象会在From区和To区域中复制来复制去,如此交换15次(次数可以调节)最终如果还是存活,就存入到老年代

4.2新生代(年轻代)的参数调节

5.年老代

主要接收由年轻代发送过来的对象,一般情况下,经过了数次Minor GC 之后还会保存下来的对象才会进入到老年代,当老年代内存不足时,将引发 "major GC”,即"Full GC

5.1注意事项

1.若养老区执行了FullGC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”

2.如果出现java.lang.OutOfMemoryError: Javaheap space异常,说明Java虚拟机的堆内存不够,原因有二

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用),也就所谓内存泄漏

5.2Full GC

Full GC是发生在老年代的垃圾收集动作老年代一般是由标记清除算法或者是标记清除算法与标记整理算法的混合实现,因为老年代里面的对象几乎个个都是在 Survivor 区域中存活过来的,所以,Full GC 发生的次数不会有 Young GC那么频繁,并且做一次 Full GC 要比进行一次 Young GC 的时间更长

1.尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收

5.3什么样的对象可以直接进入老年代

   5.3.1大对象直接进入老年代 (根据对象大小)

       1.大对象就是需要大量连续内存空间的对象,如果对象超过设置大小会直接进入老年代,不会进入年轻代。最典型的大对象就是那种很长的字符串以及数组

       2.好处:为了避免为大对象分配内存时的复制操作而降低效率

       3.JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,然后再执行下上面的第一个程序会发现大对象直接进了老年代 ,不过这个参数只在 Serial 和ParNew两个收集器下

有效。
    5.3.2长期存活的对象将进入老年代(根据存活时间)
       1.虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,以此类推。当他的年龄增加到一定程度, 就将会被晋升到老年代中.
       2.对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置,默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同
     5.3.3对象动态年龄判断
       1.为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor区空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了
        2.例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会
把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的
       3.这个百分比可以通过参数-XX:TargetSurvivorRatio设置

5.4设置堆内存的大小

    5.4.1堆内存的默认大小

     1. 初始内存大小:物理电脑内存大小 / 64  最大内存大小:物理电脑内存大小 / 4

     2.其实堆内存的空间大小是可以手动设置的

     3.开发中建议将初始堆内存和最大的堆内存设置成相同的值,防止GC后频繁扩容或者回收操作造成的系统资源浪费.

      5.4.2堆内存的参数设置

        1.参数解释:-X:是jvm的运行参数,ms:是memory start(初始化大小),mx:是最大内存大小

        2.具体查看某个参数的指令(命令行):第一步:jps:查看当前运行中的进程,第二步:jinfo -flag SurvivorRatio 进程id

       3.具体参数设置如下:

-Xms: 用来设置堆空间(年轻代+老年代)的初始内存大小,例如-Xms700m
-Xmx: 用来设置堆空间(年轻代+老年代)的最大内存大小,例如-Xmx700m
-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
  -XX:+PrintFlagsFinal  :查看所有的参数的最终值(可能会存在修改,不再是初始值)

 -XX:NewRatio:配置新生代与老年代在堆结构的占比

 -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例

 -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄

  -XX:+PrintGCDetails:输出详细的GC处理日志

6.永久代(1.8之前)

永久代存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM 才会释放此区域所占用的内存

XX:PermSize //代表永久代的初始容量

备注:
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生
了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,
对于8G物理内存的机器来说,一般我会将这两个值都设置为256M

7.元空间(1.8)

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代元空间的本质和永久代类似。默认情况下,元空间的大小仅受本地内存限制类的元数据放入nativememory, 字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

元空间与永久代之间最大的区别在于永久代带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

当元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

 7.1元空间的JVM参数设置

-XX:MaxMetaspaceSize://设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。 
-XX:MetaspaceSize
//指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M

备注:
达到该值就会触发full gc 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值;
如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。

9.元空间、永久代与方法区的关系

  1.方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是一种实现,一个是标准的一个是实现。

  2.永久代物理是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中

posted on 2022-03-20 15:00  跟着锋哥学Java  阅读(2448)  评论(1编辑  收藏  举报

导航