一、JVM、垃圾回收

二、堆外内存

如何申请堆外内存
// 参数是申请的堆外内存的大小
// 返回的是 DirectByteBuffer(这个对象是 JVM 堆内存里的一个对象,里面包含指针,指向引用的一块堆外的内存)
// 可以把数据直接写入到堆外内存里去,然后将数据直接通过 Socket 发送
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
-XX:MaxDirectMemorySize
:可以设置最大的堆外内存大小
堆外内存的优缺点
- 优点:
- 堆内的数据,要网络
IO
写出去,要先拷贝到堆外内存,再写入到socket
里发送出去,如果直接将数据分配在堆外内存,是不需要有一次额外的拷贝的,性能是比较高的
- 堆内的数据,要网络
- 缺点:
- 堆外内存难以控制,如果内存泄漏,很难排查
堆外内存分配
场景:如果设置的堆外内存最大可以使用1GB
,此时已经使用了950MB
空间,然后还要申请一块80MB
的堆外内存
- 如果堆外内存足够,就直接预留一部分内存
- 如果堆外内存不足,则将已经被
JVM
垃圾回收的DirectByteBuffer
对象所引用的堆外内存释放 - 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行
System.gc()(Full GC)
- 如果
9
次尝试后,依旧没有足够的可用堆外内存,则抛异常(堆外内存溢出异常)
堆外内存的回收
-
DirectByBuffer
的回收,会回收关联的堆外内存:JVM
一般分为young gc
和full gc
,无论发生哪种gc
,都可能会回收掉一些没有GC roots
变量引用的DirectByteBuffer
对象,回收掉了之后,就会主动释放它们引用的那些堆外内存 - 手动释放:内部有一个
cleaner
对象,可以用反射获取它,然后调用它的clean
方法来主动释放内存
- 如果依靠
JVM gc
机制,可能DirectByteBuffer
躲过N
次minor gc
进入了老年代,然后老年代迟迟没有放满,因此迟迟没有回收,此时可能导致DirectByteBuffer
对象一直在引用堆外内存,当要分配更多的堆外内存时,无法腾出更多的内存,就会有堆外内存溢出了
使用案例
Ehcache
中的一些版本,各种NIO
框架:Dubbo
、Memcache
等会用到NIO
包下ByteBuffer
来创建堆外内存,堆外内存其实是不受JVM
控制的内存
三、逃逸分析
对象一定会分配在堆中吗?
不一定,JVM
通过逃逸分析,那些逃不出方法的对象会在栈上分配
什么是逃逸分析?
- 逃逸分析:是一种可以有效减少
Java
程序中同步负载和内存堆分配压力的跨库函数全局数据流分析算法。通过逃逸分析,Java Hotspot
编译器能够分析出一个新对象的引用的使用范围,从而决定是否要将这个对象分配到堆上 - 逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联,当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其它方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(
Escape
)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,我们就称这个对象的指针发生了逃逸
逃逸分析的好处
- 栈上分配:可以降低垃圾收集器运行的频率
- 同步消除:如果发现某个对象只能从一个线程访问,那么这个对象上的操作不需要同步
- 标量替换:把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上,好处有
- 减少内存的使用,因为不用生成对象头
- 程序内存回收效率高,并且
GC
频率也会减少
四、方法区的实现:永久代、元空间
方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的字节码等数据
什么是永久代?它和方法区的关系?
如果在H
虚拟机上开发、部署,很多程序员都把方法区称作永久代,可以说方法区是规范,永久代是HotSpot
针对该规范进行的实现,在Java7
及以前的版本,方法区都是永久代实现的
什么是元空间?它和方法区的关系?
对于Java8
,HotSpot
取消了永久代,取而代之的是元空间(Metaspace
)。换句话说,方法区还是在的,只是实现变了,从永久代变为元空间了
为什么使用元空间替换永久代?
- 永久代的方法区和堆使用的物理内存是连续的

- 永久代通过以下参数配置大小:
-XX:PermSize
:设置永久代的初始大小-XX:MaxPermSize
:设置永久代的最大值,默认是64M
- 对于永久代,如果动态生成很多
class
的话,很可能出现java.lang.OutOfMemoryError: PermGen space错误
,主要是因为永久代的配置是有限的。最典型的场景是在WEB
开发比较多JSP
页面的时候 - 在
JDK8
之后,方法区存在于元空间(Metaspace
)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上,机器的内存有多大,元空间就有多大

- 可以通过以下参数来设置元空间的大小
-XX:MetaspaceSize
:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC
会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize
时,适当提高该值-XX:MaxMetaspaceSize
:最大空间,默认是没有限制
- 元空间替换永久代的原因总结:
- 表面上是为了避免
OOM
异常,因为通常使用PermSize
和MaxPermSize
设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多少合适,如果使用默认值很容易造成OOM
。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize
控制,而是由系统的实际可用空间来控制
- 表面上是为了避免
五、什么是Stop The World
?什么是OopMap
?什么是安全点
Stop The World
进行垃圾回收的过程中,会涉及对象的移动,为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象的描述为Stop The World
OopMap
在HotSpot
中,有个数据结构(映射表)称为OopMap
,一旦类加载动作完成的时候,在HotSpot
就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OoMap
,在即时编译过程中,也会在特定的位置生成OopMap
,记录下栈上和寄存器里哪些位置是引用,这些特定位置主要在:
- 循环的末尾(非
counted
循环) - 方法临返回前 / 调用方法的
call
指令后 - 可能抛异常的位置
安全点(safepoint
)
上面的位置叫作安全点(safepoint
),用户执行时,并非在代码指令流的任意位置都能够停顿下来进行垃圾收集,而是必须执行到安全点才能够暂停。
六、守护线程与用户线程的区别七墨博客
- 守护线程是程序运行的时候在后台提供的一种通用服务线程(垃圾回收线程就是典型的守护线程),用户线
言七墨 程是我们手动创建的线程 - 当主线程退出时,
JVM
也跟着退出运行,即使是死循环,守护线程也会被同时回收,而用户线程会一直在死循环里跑 - 守护线程拥有自动结束自己生命周期的特性,而用户线程没有
- 如果垃圾回收线程是非守护线程,当
JVM
要退出时,假如垃圾回收线程还在运行,会导致程序无https://qimok.cn 法退出
持续更新中…