jvm内存以及各部分的垃圾回收算法一直是很重要的部分,这部分网上也有很多博客或者书籍做了详细介绍,本片文章记录自己对jvm内存以及垃圾回收算法做的总结。
jvm内存管理图:
更为详细的模型图如下
堆(Heap):
- 堆里存放的是对象的实例,jvm启动时从操作系统申请
- 是jvm管理内存中最大也是GC工作的最主要的部分,被称为GC堆
- 线程共享
- 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(通过-Xmx和-Xms控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError.
- 堆内存物理上不一定要连续,只需要逻辑上连续即可,就像磁盘空间一样.
Object object = new Object();
我们new了一个对象,很明显对象存储在堆上面,但是我们是通过引用去操作我们new的对象,这个引用本身也是变量,它位于java的虚拟机栈。
堆分成两大块:新生代和老年代
对象产生之初在新生代,步入暮年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象
新生代= 1个Eden区+ 2个Survivor区
绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区,这个区真是名副其实的存在
Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态
如果YGC要移送的对象大于Survivor区容量上限,则直接移交给老年代
假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次YGC都会加1。
-XX:MaxTenuringThreshold
参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor 区交换14次之后,晋升至老年代
若Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代中进行分配;
如果老年代也无法放下,则会触发Full Garbage Collection(Full GC);
如果依然无法放下,则抛OOM.
堆出现OOM的概率是所有内存耗尽异常中最高的
出错时的堆内信息对解决问题非常有帮助,所以给JVM设置运行参数XX:+HeapDumpOnOutOfMemoryError`
方法区:
- 保存着被加载过的每一个类的信息(虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态变量,即时编译器编译后的代码等数据);这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中。
- 可以看做是将类(Class)的元数据,保存在方法区里。
- 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
- HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。
- 方法区同样存在垃圾收集,主要回收废弃常量和无用类,(类回收需要3个条件,1:该类所有实例均被GC,也就是jvm不存在该Class的任何实例,2:加载该类的classloader已被卸载,3:该类对应的java.lang.Class的对象没有在任何地方被引用,比如在不能在任何地方通过反射访问该类)所以在大量使用反射,动态代理,CGLib等等字节码框架以及OSGi这类频繁自定义classloader的场景都需要jvm具备类卸载的支持以保证方法区不被溢出。
当方法区内存空间无法满足内存分配需求时,将抛出OutOfMemoryError异常.
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分.方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码.其中常量存储在运行时常量池中.
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
class文件中的常量池具有动态性.
Java并不要求常量只能在编译时候产生,Java允许在运行期间将新的常量放入方法区的运行时常量池中.
String类中的intern()方法就是采用了运行时常量池的动态性.当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串,则返回池中的字符串.否则,将此 String 对象添加到池中,并返回此 String 对象的引用.
程序计数器:
每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。
内存占用小,线程私有,生命周期随线程生而生死而死,唯一不会出现OOM的地方(OutOfMemory)
如果执行的是本地方法(非java语言),程序计数器值为undefined,否则是当前线程正在执行的字节码指令的地址
本地方法栈:
类似于java虚拟机栈,主要不同在于描述的是本地方法(非java语言),HotSpot不区分这两块区域
java虚拟机栈:
java虚拟机栈描述的是java方法的执行模型,每个方法执行的时候都会创建一个帧(Frame),栈用来存放局部变量表,操作栈,动态链接,方法出口等信息,一个方法的执行过程,就是这个方法对于栈帧的入栈出栈过程。虚拟机栈是一个后入先出的数据结构,线程运行过程中,只有处于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,当前活动帧栈始终是虚拟机栈的栈顶元素。
在虚拟机规范中,对这个区域规定了两种异常情况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
如果虚拟机栈可以动态扩展,扩展时无法申请做够的内存,将会爬出OutOfMemorryError
直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但在JVM的实际运行过程中会频繁地使用这块区域.而且也会抛OOM