Java JVM的内存使用

  1. 内存总览

    • :运行时数据区域,所有类实例和数组的内存均从此处分配,堆是在 Java 虚拟机启动时创建的;
    • 非堆:非堆就是JVM留给 自己用的,所有方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中;
    • 堆外:不归JVM管的内存区域,os操作系统负责管理的一部分内存,叫做堆外内存。
JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。堆外内存内存回收并不由JVM控制,需要手动申请与释放。(具体可参考下面用法)
  1. 堆内内存(Heap)

 
  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
  1. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  1. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  1. 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:
  • MetaspaceSize :初始化元空间大小,控制发生GC阈值
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
 
  1. 分代概念

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
  • Minor GC : 清理年轻代
  • Major GC : 清理老年代
  • Full GC : 清理整个堆空间,包括年轻代和永久代,所有GC都会停止应用所有线程。
  1. 为什么分代?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
  1. 为什么survivor分为两块相等大小的幸存空间?

主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。
  1. JVM堆内存常用参数

参数 描述
-Xms 堆内存初始大小,单位m、g
-Xmx(MaxHeapSize) 堆内存最大允许大小,一般不要大于物理内存的80%
-XX:PermSize 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了
-XX:MaxPermSize 非堆内存最大允许大小
-XX:NewSize(-Xns) 年轻代内存初始大小
-XX:MaxNewSize(-Xmn) 年轻代内存最大允许大小,也可以缩写
-XX:SurvivorRatio=8 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1
-Xss 堆栈内存大小
  1. 为什么会堆内存溢出?

在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。
OOM(Out of Memory)异常常见有以下几个原因:
1)老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
2)永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
3)代码bug,占用内存无法及时回收。
OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
JAVA_OPTS="-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:
ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails  -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log"
  1. 垃圾回收算法(GC,Garbage Collection)

  1. 实操

java -Xmx512m -XX:+PrintGCDetails -jar java_study.jar
  1. 非堆内存(Non-Heap 方法区、栈)

各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  1. 堆内与非堆联系

      • 堆:存储new出来的对象(包括实例变量)
        • 垃圾:没有任何引用指向的对象
        • 内存泄漏:不再使用的对象还没有被及时的回收
        • 实例变量的生命周期:从对象在堆中创建开始到对象从堆中被回收结束
      • 栈:存储正在调用中的方法中的局部变量(包括方法参数)
            调用方法时会为该方法在栈中分配一块对应的栈帧,栈帧中存储局部变量(包括方法参数)
            当方法调用结束时,栈帧被清除,局部变量一并失效
            局部变量的生命周期:调用方法时存储在栈中,方法结束时与栈帧一并被清除
            堆溢出(OutOfMemoryError:java heap space)
      • 方法区:存储.class字节码文件(包括方法、静态变量)
      方法只有一份,通过this来区分具体的调用对象
       持久带溢出(OutOfMemoryError: PermGen space)
      
 
将main方法加载到栈内存中,执行int [ ] arr = ,此时有new,在堆内存中开辟一块空间,创建一个数组(5个连续的空间),数组的索引为0-4。
开辟空间后,数组会生成一个在内存当中的地址值,并把地址值赋值给arr。即可通过此地址值找到对应的内存空间。打印arr就会显示内存的地址值。( [ 代表一维数组,I 代表当前数据类型,@为固定连接符号,后面为十六进制的内存地址值)。
int默认的初始化值为0,此时为数组进行赋值,通过地址值找到这一块空间,通过0索引找到这一块,将默认的初始化值替换即可
 
将main方法加载到栈内存中,执行int [ ][ ] arr = ,此时虽然采用静态初始化方法无new,依旧在堆内存中开辟一块空间,创建一个数组(3个连续的空间),数组的索引为0-2。针对此时的索引进行输出时得到的会是一个一维数组的地址值(即二维数组中“行”存放“列”的地址值)。此时创建3个一维数组,每个一维数组对应的索引为0-1。在这时的一维数组分别保存1-2,3-4,5-6。三个一维数组分别有三个内存地址值,此时二维数组存放的就是这3个内存地址值。
在二维数组通过3个地址值就可以找到3块空间,此时二维数组才算创建完毕,也会有一个对应的地址值(图上的0x0011),并把这个地址值赋值给arr。
此时如果输出arr[0]就会出现第一个一维数组的地址值,输出arr即输出二维数组的地址值。
  1. 堆外内存(Off-Heap)

  • off-heap是什么
  • JVM堆内存满了后会怎么样?
  • 基于堆外内存解决系统GC卡顿问题
  1. off-heap是什么

  1. JVM堆内存满了后会怎么样?

但是这个老年代里如果放了太多缓存数据以后,就可能会导致他剩余的可用空间就会比较少了,此时可能会导致老年代经常会放一点别的数据就塞满了,一旦塞满了就会触发JVM的full gc,有一个垃圾回收线程会去回收老年代里的数据,此时如下图。
可是此时一般来说能回收的也就是除了缓存数据之外的一些空间,哪怕你回收了,但是缓存数据是要一直存在的,所以没法回收掉,此时会导致每次你回收了一部分剩余空间之后,然后还是剩余了很多缓存数据,此时对于缓存数据来说会一直占据老年代的很大空间。
那么此时必然导致一个现象,那就是老年代会频繁的写一点数据就满了,写一点数据就满了,然后一会儿就得触发一下full gc,一会儿就得触发一下full gc,每次full gc都会导致JVM停止运行,没法处理外部请求,此时对外部来说,就会感觉你的系统性能经常抖动,一会卡一下,一会儿卡一下。
所以往往来说,把很多数据缓存在JVM内部,是很可能导致上述现象,就是老年代频繁塞满、频繁触发fullgc、频繁导致系统停顿没法处理请求,如下图。
  1. 基于堆外内存解决系统GC卡顿问题

所以针对这种情况,往往我们的优化手段,就是会把要缓存的数据,从JVM堆内存里转移到offheap堆外内存里去,那所以问题来了,啥叫做堆外内存呢?就是顾名思义,不归JVM管的内存区域,os操作系统负责管理的一部分内存,叫做堆外内存。
所以我们其实可以选择把很多数据直接写入到堆外内存里去,这样的话,就不会占用JVM堆中的老年代空间了,也就不会导致老年代频繁塞满,频繁触发full gc,导致系统性能频繁抖动了,如下图。
那既然这个堆外内存这么好,问题来了,他有什么缺点呢?当然有了,因为如果你用的是JVM堆内的内存,你写入了很多数据以后,如果内存满了,此时JVM会自动进行垃圾回收,帮你释放掉一些内存空间,他是全自动的。
但是如果你用的是堆外内存,那可没有JVM来帮你管理了,此时你必须自己管理那块内存空间,也就是说,你写入了数据以后,到了需要的时候,你得自己注意把部分内存进行释放,所以这就导致了堆外内存虽然不会导致你的JVM频繁gc,但是他可能会导致你的代码管理难度变高,如下图。
那么这个堆外内存一般来说我们用java代码是如何申请的呢?看下面的代码,一般类似netty、rocketmq等中间件因为就是要管理大量的内存数据,所以都会选择申请一块堆外内存,把数据放在里面,自己进行精细化的管理。
// 定义好要申请的堆外内存的大小,这里是1GB
int memorySize = 1024 * 1024 * 1024;
// 用Java里的ByteBuffer.allocateDirect方法就可以申请一块堆外内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(memorySize);
// 把数据写入到堆外内存里去
byte[] bytes = "hello world".getBytes();
byteBuffer.put(bytes);
// 从堆外内存里读取数据
byteBuffer.flip();
byte[] readBytes = new byte[bytes.length];
byteBuffer.get(readBytes, 0, bytes.length);
那大家通过这块代码看到了我们如何申请堆外内存,以及如何往堆外内存里写入数据和如何读取数据之后,现在思考一下,堆外内存我们应该如何进行释放呢?是这样的,这个堆外内存其实是被JVM堆内的一个ByteBuffer对象来引用的,所以如果要是JVM堆内的ByteBuffer对象被回收了,那他关联的堆外内存就会被释放了,如下图。
  1. Spark的内存使用

 
posted @ 2022-11-09 11:16  changfan  阅读(166)  评论(0编辑  收藏  举报