Java 开发

图解 JVM

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

一、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对象一直在引用堆外内存,当要分配更多的堆外内存时,无法腾出更多的内存,就会有堆外内存溢出了

使用案例

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

三、逃逸分析

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

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

言七墨

什么是逃逸分析?

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

逃逸分析的好处

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

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

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

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

如果在H七墨博客otSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代,可以说方法区是规范永久代是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要退出时,假如垃圾回收线程还在运行,会导致程序无https://qimok.cn法退出

持续更新中…

0 条回应