目录
内存区域(运行时数据区)
JDK 1.8之前:
JDK 1.8 :
线程私有
虚拟机栈
- 生命周期与线程相同
- 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储
局部变量表
、操作栈、动态链接、方法出口等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据
局部变量表: 主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)
对象引用:(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
虚拟机栈会出现两种异常
- StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常(比如无限递归)
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常
程序计数器
- 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的
行号指示器
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的
流程控制
,如:顺序执行、选择、循环、异常处理 - 在多线程的情况下,程序计数器用于
记录当前线程执行的位置
,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了 - 程序计数器是
唯一一个不会出现 OutOfMemoryError 的内存区域
,它的生命周期随着线程的创建而创建,随着线程的结束而死亡
本地方法栈
- 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的
Native 方法
服务(在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一) - 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息
- 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常
线程共享的
堆
- 在虚拟机启动时创建
- 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
方法区
- 用于存储已被虚拟机加载的
类信息、常量、静态变量、即时编译器编译后的代码
等数据
运行时常量池
- 运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
- 既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常
- JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池
方法区和永久代的关系
- 方法去是Java虚拟机规范中的定义,是一种
规范
- 永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种
实现方式
,其他的虚拟机实现并没有永久代这一说法
为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
- 永久代由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),
很容易造成 OOM
,有人说可以把永久代设置得足够大,但很难确定一个合适的大小
,受类数量,常量数量的多少影响很大 - 而元空间使用的是
直接内存
,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError,极端情况下,元空间可能会耗尽所有可用的系统资源
直接内存
- 堆外内存
堆外内存的对象回收
-
JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存
-
first是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Cleaner链表中,和first形成引用关系
-
ReferenceQueue是用来保存需要回收的Cleaner对象
-
如果该DirectByteBuffer对象在一次GC中被回收了,此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法
-
Cleaner对象的clean方法主要有两个作用:
- 把自身从Cleaner链表删除,从而在下次GC时能够被回收
- 释放堆外内存
如果JVM一直没有执行FGC的话,无效的Cleaner对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆
- 其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件很苛刻时,会主动调用
System.gc()
强制执行FGC - 不过很多线上环境的JVM参数有-XX:+DisableExplicitGC,导致了System.gc()等于一个空函数,根本不会触发FGC,这一点在使用Netty框架时需要注意是否会出问题