元空间与直接内存的关系

随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
元空间与直接内存的关系_第1张图片

从上图可以看到,元空间和直接内存都是堆外内存,那么二者的区别是什么呢?

元空间:DK1.8以前的HotSpot JVM有方法区,也叫永久代(permanent generation)。(永久代是针对hotspot虚拟机来说的)

方法区用于存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码

方法区是一片连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。

JDK1.7开始了方法区的部分移除:符号引用(Symbols)移至native heap,字面量(interned strings)和静态变量(class statics)移至java heap。

为什么要用Metaspace替代永久代?
随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。

默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  1. XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

  2. -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
      除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

    1)-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
    2) -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

    -verbose参数是为了获取类型加载和卸载的信息

Metaspace内存管理

在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。
每个加载器有单独的存储空间。
省掉了GC扫描及压缩的时间。
当GC发现某个类加载器不再存活了,会把对应的空间整个回收。

直接内存:直接内存主要被 Java NIO 使用,某种程度上也就是指DirectByteBuffer对象占用的堆外内存。
DirectByteBuffer对象创建时会通过Unsafe类接口直接调用操作系统的malloc分配内存,然后将内存的起始地址和大小保存下来,据此就可以直接操作内存空间
元空间与直接内存的关系_第2张图片
可以看出,直接内存的大小并不受到java堆大小的限制,甚至不受到JVM进程内存大小的限制。它只受限于本机总内存(RAM及SWAP区或者分页文件)大小以及处理器寻址空间的限制(最常见的就是32位/64位CPU的最大寻址空间限制不同)。

DirectByteBuffer使用直接内存的原因有两点

1) 这块内存真正的分配并不在 Java 堆中,堆中只有一个很小的对象引用,这种方式能减轻 GC 压力
2) 对于堆内对象,进行IO操作(Socket、文件读写)时需要先把对象复制一份到堆外内存再写入 Socket 或者文件,而当 DirectByteBuffer 就在堆外分配内存时可以省掉一次从堆内拷贝到堆外的操作,减少用户态到内核态的操作,性能表现会更好

直接内存的回收

需注意堆外内存并不直接控制于JVM,这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,而 Young GC 的时候只会将年轻代里不可达的DirectByteBuffer对象及其直接内存回收,如果这些对象大部分都晋升到了年老代,那么只能等到Full GC的时候才能彻底地回收DirectByteBuffer对象及其关联的堆外内存。因此,堆外内存的回收依赖于 Full GC

Full GC一般发生在年老代垃圾回收或者代码调用System.gc的时候,依靠年老代垃圾回收触发 Full GC,进而实现堆外内存的回收显然具有太大的不确定性。如果年老代一直不进行垃圾回收,那么堆外内存就得不到回收,机器的物理内存可能就会被慢慢耗光。为了避免这种情况发生,可以通过参数-XX:MaxDirectMemorySize来指定最大的直接内存大小,当其使用达到了阈值的时候将调用System.gc来做一次Full GC,从而完成可控的堆外内存回收。这样做的问题在于,堆外内存的回收依赖于代码调用 System.gc,先捕获到异常,再在Catch块里面通过System.gc()命 令来触发垃圾收集。但如果Java虚拟机再打开了-XX:+DisableExplicitGC开关,禁止了人工触发垃圾 收集的话,根本不会触发Full GC,这样在使用Netty等 NIO 框架时需注意是否会因为这个参数导致直接内存的泄露

直接内存出现OutOfMemoryError的原因是对该区域进行内存分配时,其内存与其他内存加起来超过最大物理内存限制(包括物理的和操作系统级的限制),从而导致OutOfMemoryError。
  另外,若我们通过参数“-XX:MaxDirectMemorySize”指定了直接内存的最大值,其超过指定的最大值时,也会抛出内存溢出异常。
  
-XX:MaxDirectMemorySize 参数没有指定的话,那么根据directMemory = Runtime.getRuntime().maxMemory()最大直接内存的值和堆内存大小差不多,即与堆内存大小一致

你可能感兴趣的:(jvm)