个人粗浅的认为,JVM 的学习比较抽象,基本都是建立在概念上的。JVM 可以从两方面分析:
- 软件层面机器码的翻译
- 内存管理
软件层面机器码的翻译: Java 号称一次编译到处运行,是建立在对应与不同版本的JVM上的,对应不同OS的JVM 可以将编译好的文件代码翻译成对应的OS版本的机器码,来达到实现的;
内存管理:上图:
1
-
本地方法栈:C语言方法运行时数据存储对应的内存区域,比如 Thread.Start 启动时,会调用c语言的线程方法;因此此处也会发生OOM;
-
程序计数器:记录编译后的机器码的执行行号与地址:多线程切换时,需要记录每个线程的执行代码位置;
-
方法区:存储类信息,静态变量,常量以及JIT编译后的类信息等;也会发生OOM;
-
虚拟机栈:以线程为单位,调用一个方法就创建压栈,栈针代表最新的数据;栈里面存储着局部变量表,动态链接(没搞懂),操作数栈,方法出口等信息,其中局部变量表就是存储基本变量或者引用变量的地址值,操作数栈作用在于运行时计算数值,最终返给局部变量表中,方法出口记录调用方法的出口;当调用的方法过多,造成栈的深度过大,则会发生stack overflow(栈溢出);当分配给总的线程所耗费的内存过大超过虚拟机栈内存,则会发生OOM;
-
堆:堆是创建对象存储的地区;堆的区域划分,上图:
JVM 将内存模型设计成原因,牵扯到GC的回收,而GC的回收策略实现了不同的回收算法,先来说下GC的回收前提操作:
主要是申请堆内存的时候,发现不够的话,则GC:检索出垃圾对象,回收空间;那么如何判断垃圾:
1.引用计数法:无法解决循环依赖的对象问题;
2.可达性算法:
-
标记+清除算法:低效+内存碎片;首先扫描堆中所有的对象,发现垃圾对象则标志,然后清除;
-
标志+整理(标志+清除+压缩):低效,但是没有内存碎片:首先扫描堆中所有的对象,发现垃圾对象则标志,然后清除,最后将用到的内存区域移动在一起;
-
复制:高效,没有内存碎片,但是浪费空间;以可达性算法为基础:从根部开始,顺着链往下,发现一个则复制到新的内存区,这样没有GC-ROOT的对象就会被放弃掉,被GC线程回收;
回到前面的设计原因,要知道发生GC的时候防止由于并发产生各种不可描述的后果,会启动STW机制,停掉所有的用户线程,执行GC线程回收,频繁这么操作的话则导致程序性能相当差(GC调用最终就是减少STW),采用分代算法,根据每一个Bean的生命周期不一样的特点,采取这种模型的:当申请创建小对象的时候,会在伊甸区申请内存对象;下一次申请发生伊甸区的内存不够时,则判断是不是垃圾对象,不是则复制到生存区1,然后在伊甸区申请区域;同理,下一次申请发生伊甸区的内存不够时,判断伊甸区+生存区1的对象是不是垃圾对象,不是则复制到到生存区2,依次循环,总是保留一个空的生存区,在伊甸区申请;到达一定条件后,则将这些对象复制到老年区,这里有个老年区担保机制,如果发现老年区的连续控制不够则触发 MajorGC ,这里采用的是标志清除或者标志整理的算法的,如果此时还不够,则不允许复制过来,如果伊甸区申请的区域不够,则触发OOM;
当申请大对象时,则直接在老年区申请区域,这样往往很浪费内存,导致频繁GC的几率很大,进而导致STW,程序执行性能下降,笔者曾经犯过这个错误,后来学习了这部分知识才晓得;
另外,当年轻代的相同年龄的对象之和>生存区的一半的空间的时候,也会复制到老年区的;
内存规整性:在申请内存区域的时候会考虑到这个问题,从而引出指针碰撞以及空闲列表:
- 指针碰撞:当堆存在足够的连续性大小的内存区域的时候,意思是说,空闲的内存在一边,占用的内存在一边的,不存在内存碎片的时候(采用复制或者标志+整理)采用的申请区域策略;
- 空闲列表:当采用的是标志+清除算法时,铁定会产生内存碎片,这时JVM底层在用记录每一个对象的引用地址来记录,当下一次申请的时候则根据空闲列表的记录来申请内存;
程序执行会因为并发导致数据的安全问题,同样的当并发申请内存区域时候,也要做一定的处理:
1.同步: CAS机制;
2.TLAB:在堆区为每一个线程划分一个特定区域,在里面申请区域;
这样就可以保证内存区域的独立性了;
个人粗浅的认为:申请一个内存空间的时候就就牵扯到上面的内容了;
另外这里牵扯到另外概念:强引用,软引用,弱引用,虚引用:
强引用:就是主要存在 GC-ROOT ,不管怎么样,都不会回收的;
软引用:发生OOM前,执行判断引用类型,如果是软引用,则回收;
弱引用:主要发生GC,则回收;
虚引用:直接上高手的图:
另外一个问题,当GC时,发现对象不可达的时候,不一定会回收这个对象的,原因在于可能这个对象重写 finalize() 方法;在申请内存不够的时候,触发GC,GC首先检查标志对象是不是没有GC-ROOT以及重写了 finalize() 方法,立马回收;如果重写了则将其加入到队列里面去,由JVM底层创建一个线程去执行这个方法,如果在这个方法里面又重新 GC-ROOT 的话,则该对象移除队列,起死回生了;但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。