Java 开发

图解 JVM

言七墨 · 8月15日 · 2020年 · · · · 436次已读

一、JVM、垃圾回收

二、堆外内存

如何申请堆外内存

// 参数是申请的堆外内存的大小
// 返回的是 DirectByteBuffer(这个对象是 JVM 堆内存里的一个对象,里面包含指针,指向引用的一块堆外的内存)
// 可以把数据直接写入到堆外内存里去,然后将数据直接通过 Socket 发送
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); 
  • -XX:MaxDirectMemorySize:可以设置最大的堆外内存大小

堆外内存的优缺点

  • 优点:
    • 堆内的数据,要网络IO写出去,要先拷贝到堆外内存,再写入到socket里发送出去,如果直接将数据分配在堆外内存,是不需要有一次额外的拷贝的,性能是比较高的
  • 缺点:
    • 堆外内存难以控制,如果内存泄漏,很难排查

堆外内存分配

场景:如果设置的堆外内存最大可以使用1GB,此时已经使用了950MB空间,然后还要申请一块80MB的堆外内存

  1. 如果堆外内存足够,就直接预留一部分内存
  2. 如果堆外内存不足,则将已经被JVM垃圾回收的DirectByteBuffer对象所引用的堆外内存释放
  3. 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行System.gc()(Full GC)
  4. 如果9次尝试后,依旧没有足够的可用堆外内存,则抛异常(堆外内存溢出异常)

堆外内存的回收

  1. DirectByBuffer的回收,会回收关联的堆外内存:JVM一般分为young gcfull gc,无论发生哪种gc,都可能会回收掉一些没有GC roots变量引用的DirectByteBuffer对象,回收掉了之后,就会主动释放它们引用的那些堆外内存
  2. 手动释放:内部有一个cleaner对象言七墨,可以用反射获取它,然后调用它的clean方法来主动释放内存
  • 如果依靠JVM gc机制,可能DirectByteBuffer躲过Nminor gc进入了老年代,然后老年代迟迟没有放满,因此迟迟没有回收,此时可能导致DirectByteBuffer对象一直在引用堆外内存,当要分配更多的堆外内https://qimok.cn存时,无法腾出更多的内存,就会有堆外内存溢出了

使用案例

  • Ehcache中的一些版本,各种NIO框架:DubboMemcache等会用到NIO包下ByteBuffer来创建堆外内存,堆外内存其实是不受JVM控制的内存

三、逃逸分析

对象一定会分配在堆中吗?

不一定,JVM通过逃逸分析,那些逃不出方法的对象会在栈上分配

什么是逃逸分析?

  • 逃逸分析:是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨库函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新对象的引用的使用范围,从而决定是否要将这个对象分配到堆上
  • 逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联,当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其它方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,我们就称这个对象的指针发生了逃逸

逃逸分析的好处

  • 栈上分配:可以降低垃圾收集器运行的频率
  • 同步消除:如果发现某个对象只能从一个线程访问,那么这个对象上的操作不需要同步
  • 标量替换:把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上,好处有
    • 减少内存的使用,因为不用生成对象头
    • 程序内存回收效率高,并且GC七墨博客频率也会减少

四、方法区的实现:永久代、元空间

方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的字节码等数据

什么是永久代?它和方法区的关系?

如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代,可以说方法区是规范永久代是HotSpot针对该规范进行的实现,在Java7及以前的版本,方法区都是永久代实现的

什么是元空间?它和方法区的关系?

对于Java8HotSpot取消了永久代,取而代之的是元空间(Metaspace)。换句话说,方法区还是在的,只是实现变了,从永久代变为元空间了

为什么使用元空间替换永久代?

  • 永久代的方法区和堆使用的物理内存是连续的
  • 永久代通过以下参数配置大小:
    • -XX:PermSize:设置永久代的初始大小
    • -XX:MaxPermSize:设置永久代的最大值,默认是64M
  • 对于永久代,如果动态生成很多class的话,很可能出现java.lang.OutOfMemoryError: PermGen space错误,主要是因为永久代的配置是有限的。最典型的场景是在WEB开发比较多JSP页面的时候
  • JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上,机器的内存有多大,元空间就有多大
  • 可以通过以下参数来设置元空间的大小
    • -XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
    • -XX:MaxMetaspaceSize:最大空间,默认是没有限制
  • 元空间替换永久代的原因总结:
    • 表面上是为了避免OOM异常,因为通常使用PermSizeMaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多少合适,如果使用默认值很容易造成OOM。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制,而是由系统的实际可用空间来控制

五、什么是Stop The World?什么是OopMap?什么是安全点

Stop The World

进行垃圾回收的过程中,会涉及对象的移动,为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象的描述为Stop The World

OopMap

HotSpot中,有个数据结构(映射表)称为OopMap,一旦类加载动作完成的时候,在HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OoMap,在即时编译过程中,也会在特定的位七墨博客置生成OopMap,记录下栈上和寄存器里哪些位置是引用,这些特定位置主要在:

  1. 循环的末尾(非counted循环)
  2. 方法临返回前 / 调用方法的call指令后
  3. 可能抛异常的位置

安全点(safepoint

上面的位置叫作安全点(safepoint),用户执行时,并非在代码指令流的任意位置都能够停顿下来进行垃圾收集,而是必须执行到安全点才能够暂停。

六、守护线程与用户线程的区别

  • 七墨博客守护线程是程序运行的时候在后台提供的一种通用服务线程(垃圾回收线程就是典型的守护线程),用户线程是我们手动创建的线程
  • 当主线程退出时,JVM也跟着退出运行,即使是死循环,守护线程也会被同时回收,而用户线程会一直在死循环里跑
  • 守护线程拥有自动结束自己生命周期的特性,而用户线程没有
  • 如果垃圾回收线程是非守护线程,当JVM要退出时,假如垃圾回收线程还在运行,会导致程序无法退出

持续更新中…

0 条回应