背景介绍
当JVM类加载器加载完字节码文件之后,会交给执行引擎执行,在执行的过程中会有一块JVM内存区域来存放程序运行过程中的数据,也就是我们图中放的运行时数据区,那这一块运行时数据区究竟帮我们做了哪些工作?我们常说的线上内存泄漏和内存溢出是因为什么?我们今儿来揭开看看它神秘的面纱。
过程
- 线程共有:堆、方法区
- 线程私有:程序计数器、Java虚拟机栈、本地方法栈
一、 程序计数器(PC)
上官方百度百科介绍:
作用:存放当前线程执行的下一条指令地址。
在多线程环境下线程之间会涉及到线程切换问题,为了保证线程切换之后还能继续按照上一次切换的位置继续执行,PC会进行指令的记录。PC是线程独有的,不可共享
二、Java虚拟机栈
作用:
每个线程在创建时都会创建一个虚拟机栈,保存了一个一个的栈帧 。针对栈帧我们来具体说说,下图为Java虚拟机中栈帧的内部结构,每执行一个方法都会创建一个栈帧(一个方法对应一个栈帧) ,而栈帧中包含了四部分:局部变量表、操作数栈、方法返回地址、动态链接,而每一个栈帧执行的过程也是入栈、出栈的过程。
包括:
- 操作数栈(Operand Stack):用来在执行字节码指令过程中用来计算的
- 局部变量表(LocalVariables):在方法执行过程中实时记录每个局部变量对应的值
- 方法返回地址(Return Address):地址
- 动态链接(Dynamic Linking):符号引用转换为调用方法的直接引用
特点:
- 线程私有
- 遵守栈FIFO规则,方法开始执行栈帧入栈,方法执行完栈帧弹出,所以虚拟机不需要垃圾回收
存在的问题:
- 如果线程太多了,但是没有足够空间创建虚拟机栈,会发生栈溢出
- 方法调用层次太多,可能出现StackOverflowError
三、本地方法栈(Native Method Stacks)
作用:存储Native方法
四、方法区(Method Area)
作用:
存储被Java虚拟机加载过后的class类信息、常量、静态变量、编译后的代码
不知道大家是否还记得上一篇分享中讲到的类加载过程,其中加载这一步会通过类全限定名加载成class类对象,其中就会把class信息、静态变量、常量等信息加载到方法区,看下面这张图
五、堆(Heap)
所有线程共享的一块内存区域,在new对象的时候会在Heap分配内存空间。可以细分为:
- 年轻代和老年代,对应的比例为2:1 ,新生代存放朝生夕死的对象,老年代存放生命周期较长的对象
- 年轻代又可以分为Eden、From Survivor、ToSurvivor三个区,对应的比例为:8:1:1(默认情况下,可以通过-XX:SurvivorRatio来调整)
那三个区域中对象是如何进行流转的呢?我们具体来看一下
1、在最开始讲述对象创建流程中包含了一步是分配内存,就是通过指针碰撞或空间散列在Heap中分配内存,新new对象的对象JVM会默认优先分配在Eden区,当Eden区空间逐渐减少(可以默认配置Eden空间容量)的时候,就会触发Young GC来清理,Eden区对象就会放入Survivor区;
2、Survivor区每次分配内存只使用其中一块,Eden和Survivor存活对象会复制到另一块Survivor区中,Eden和原来的Survivor区对象会被清理掉。(这也是为什么图中我只在其中一个Survivor区画了对象,另一块Survivor区没画的原因)
3、在对象头中记录了对象迭代的年龄(年龄计数器),当进入Survivor区开始每YoungGC一次年龄就会+1,当年龄达到15的时候就会进入老年代
从图上我们看到进入老年代的条件远不止年龄>=15这一个,对象会进入老年代的方式共有四个:
- 长期存活:对象头中记录了对象迭代的年龄,每次迭代都会—+1,当年龄达到15(默认)
- 超大对象:占用大量连续空间
- 动态年龄判断:servivor中相同年龄对象的总和>survivor空间一半
- 空间分配担保:Young GC后,新生代有大量对象对象存活,需要老年代分配担保