1.jvm基本结构
类加载子系统: 将class文件加载到jvm中
方法区: 加载class信息/运行时常量池存放在方法区
java堆: 基本上所有的java实例/对象都是存放在java堆中
方法区以及java堆都是公有
java栈: 普通方法的一些信息(方法入参/局部变量/中间临时数据/返回数据/异常状态处理)都是存放在栈中(私有)
本地方法栈: 类似java栈,不过不是普通方法而是nativeMethod
寄存器:用来指向正在被执行的位置
直接内存: 对外内存,向系统申请内存空间(与java堆相比,读写方法速度较快,但是申请比较耗时)
垃圾回收: 用来回收方法区,java堆,直接内存
执行引擎: 负责执行JVM中的字节码
2.JVM类加载基本流程
1.加载classLoader
通过类的全路径名,获取类的二进制数据流,获取类的信息,创建java.lang.Class类的实例
2.验证
判断字节码是够合法,规范性
主要涉及: 格式/语义/字节码/符号引用验证
3.准备
分配对应的内存空间
4.解析
将符号引用转为直接引用
5.初始化
开始执行字节码,已经被加载到系统中
符号引用 : 用符号来描述引用的目标直接关系
直接引用 : 通过Class文件加载到内存之后,通过对符号引用的转换,产生直接引用
为啥会有直接引用?
我们的java类编译成class文件之后,并不知道引用地址,所以用符号来代替
在class文件解析过后,会将符号转换为直接引用,用#12,#37来直接指向引用地址
3.虚拟机栈
虚拟机栈主要是为了执行方法服务的,用来描述方法执行的内存模型
主要内容: 栈帧(局部变量表,操作数栈,栈数据区)
入栈: 方法调用
出栈: 方法返回
栈顶: 正在执行的方法
private static void count(long arg1, long arg2, long arg3, long arg4, long arg5) {
long num1 = 1;
long num2 = 2;
long num3 = 3;
long num4 = 4;
long num5 = 5;
long num6 = 6;
long num7 = 7;
long num8 = 8;
count++;
count(arg1, arg2, arg3, arg4, arg5);
}
我们用jclasslib插件来看看以上代码在栈帧中有哪些东西
以及PC计数器用来记录执行的代码
4.java堆
几乎所有的对象都是存储在java堆上面,而且Java堆完全自动化管理,通过垃圾回收机制,垃圾对象会被自动清理
主要划分为:新生代(Eden区,S0和S1)和老年代
新生代:老年代 = 1:2 默认比例
Eden:S0:S1 = 8:1:1 默认比例
在大部分情况下,对象首先会分配在Eden区,在一次新生代GC回收后,如果对象还存在,则会进入S1或S0中,之后每经过一次新生代GC,如果对象还存活的话,它的年龄+1,当年龄达到一定条件后(默认为16),会被认为是老年代对象,进入老年代区
5.方法区
方法区的大小决定系统中能存储多少个类
存储类的信息/方法信息/常量池等
1.8之前使用永久代
1.8之后使用元空间
永久代: 内存永久保存区域,主要用于存在Class(类),在类被加载的时候放入永久代区域,GC不会对永久代区域进行清理,所以导致Class越来越多的时候,导致OOM异常
元空间: 元空间本质类似永久代,主要区别是,元空间并不在虚拟机中,而是使用堆外的直接内存,因此默认情况下,虚拟机会耗尽可用的系统内存
6.垃圾回收(GC)
可触及性: 从根节点开始,可以达到某个对象
可复活性: 对象引用被释放,但是可能在finalize()函数中被复活
不可触及性: 由于finalize()函数只会执行一次,错过这一次复活的对象,则变成不可触及状态
引用关系:
1.强引用: 一般来讲就是程序中的new对象
2:软引用(java.lang.ref.SoftReference): 当堆空间不足的时候,才会被回收,因此软引用不会引发内存溢出
3.弱引用(java.lang.ref.WeakReference): 当GC的时候,只要发现弱引用,无论系统空间是否不足,都会回收
4.虚引用(java.lang.ref.PhantomReference): 如果对象被虚引用,其实和没引用一样. 虚引用必须和引用队列在一起使用,它的作用是用于跟踪GC回收过程
对象分配过程:
逃逸分析: 就是指分配的对象是否逃出了自己作用域,那么就可以理解为该分配的对象已经不满足了线程私有的前提,因此不会栈上分配(默认开启)
标量替换: 把聚合量分解为标量, 通俗理解把对象分解为成员变量存储在栈帧或寄存器上(默认开启)
那么YoungGC又是怎么操作把垃圾清理掉的呢
复制算法: 将Eden区存活的对象复制到Survir区,并且清空Eden区
那么什么情况下会进入老年代呢
1. BigObject当对象达到一定程度(默认为0)
2. 当对象的年龄达到了阈值(默认15)
3. 当SurviveTo区空间不足的时候
以上情况都会将对象分配到老年代
那么老年代区的对象又是怎么清除(GC)的呢
标记压缩算法: 首先标记存活的对象,然后将存活的对象压缩到内存的一端,然后再清理所有存活对象之外的空间
标记清除算法: 标记存活的对象,直接将不存活对象的直接清理
7.垃圾回收器
7.1串行回收器 - Serial(JVM Client)模式下默认的回收器
特点: 1.只使用单线程的GC 2.独占式GC
言外之意就是当串行回收器(Serial)准备GC的时候,所有的其他的线程都需要停止,等我GC完毕之后,其他的线程再继续执行
7.2 并行回收器 - ParNew&ParallelGC&ParallelOldGC&CMS (将回收器多线程化)
ParNew&ParallelGC都是新生代并行回收器,采用的都是复制算法
ParallelOldGC 老年代并行回收器(标记压缩算法)
CMS 老年代并行回收器(标记清除算法)
7.3 CMS - 并行回收器
初始标记: 标记根对象(停止程序)
并发标记: 标记老年代所有的对象
预清理: 由于CMS回收用于老年代垃圾回收,所以预清理主要是为了避免和YoungGC发生时间碰撞
重新标记: 由于之前的并发标记并没有进行STW(停止程序)的操作,所以说新生代可能存在YoungGC的操作,并且会有新的对象进入老年代,那么就需要重新标记(一般来讲需要重新标记的对象不会很多),过程中必须STW(停止程序)
最后再进行并发清理和重置
7.4 G1回收器(JDK1.7之后使用)
特点:优先回收垃圾比例比较高的区域
G1回收器将堆空间分为多个区域,每次收集部分区域来减少GC的时间
主要步骤:
以上过程都是循环的过程一直在收集,主要我们来看看并发标记有哪些操作
1.初始化标记: 标记根对象(同时伴随着第一阶段的YoungGC并且STW)
2.根区域扫描: 由于上一步进行了YoungGC,所以这步主要是扫描SurvivorTo区存活的对象,如果对象满足进入老年代,直接进入老年代
3.并发标记: 分区标记存活的对象
4.重新标记: 由于整个收集器都是并行操作,需要重新标记新对象并且STW
5.独占清理: 由于G1收集器分区收集,这步主要是为了计算出每块区域中垃圾对象的比例,同时STW
6.并发清理: 主要是用于清理垃圾已经填满的区域块
那么以上步骤我们收集了堆中的所有对象,并且分区管理了,需要将垃圾比例高的区域给清理,并且将剩余的对象重新移到其他区域来减少内存碎片,而且由于循环的方式回到第一阶段YoungGC,那么Eden区的空间会全部清空,再次收集
最终才是我们收集之后的内存情况!