前言
本篇主要介绍JVM内存区域划分,JVM类加载过程,JVM垃圾回收机制。
没什么好说的,八股文,背就完事了。
一.JVM内存区域划分
1.程序计数器
程序计数器保存着下一条要执行的指令的地址
这里所说的程序计数器不是cpu的寄存器,而是内存空间
这里的“下一条要执行的指令的地址”是Java的字节码(不是cpu的二机制机械语言)
2.堆
JVM上最大的空间,new出来的对象都在堆上
3.栈
函数中的局部变量,参数的形参,以及函数之间的调用关系存储在栈中
4.元数据区(方法区)
Java程序中的指令(指令都是包含在类的方法中)
保存了代码中涉及到类的相关信息
保存了类的static属性
在Java进程中,元数据区和堆只有一份
而程序计数器和栈,则可能有多份(当一个Java进程中有多个线程时,每个线程都有自己的程序计数器和栈)
一个变量处于哪个内存区域,和变量是不是内置类型无关,而是和变量的形态有关
局部变量-->栈
成员变量-->堆
静态成员变量-->元数据区(方法区)
二.JVM类加载过程
1.类加载是什么
当我们写了一个Java程序,得到的是一个.java文件
通过javac编译,得到.class文件
运行java程序时,JVM需读取.class中的内容,并且执行里面的指令,把类涉及到的字节码,从硬盘读取到内存中(元数据区),这就是类加载
加载一个.class文件,也就会对应创建一个类对象,类对象里包含了.class文件中的各种信息(类的名字,类的属性,属性名,类的方法,方法名等等...)
2.类加载的具体步骤
1)加载
代码中先见到类的名字,然后进一步找到对应的.class文件,打开并读取文件内容
2)验证
验证读到的.class文件数据是否安全,格式是否合法。
Java标准文档中,明确定义了.class文件的格式是怎么样的
在java代码编译时,会转化成类对象加载出来
3)准备
分配内存空间
我们最终得到的类对象需要内存来存放
根据刚才读取到的内容,确定类对象需要的内存空间,申请对应的内存空间,并将内存空间所有内容都初始化为0,避免上次的数据被当前程序误使用,从而引发bug
在C++中分配内存不是这样,C++中申请到的内存不会置零,内存上对应的数据,就是上次使用的时候留的数据(效率高)
4)解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
5)初始化
针对类对象做最终的初始化操作,执行静态成员的赋值语句。
经典问题:双亲委派模型
JVM默认有3个类加载器
这3个加载器有着父子关系(不是继承的关系 )
双亲委派模型是为了找到对应的.class文件
左边是加载器,右边是负责扫描的目录
1)当开始扫描时,Application ClassLoader拿到任务后向上传输,传到Platform ClassLoader,也不是立刻扫描目录,而是继续向上传递,直到为空。
2)当任务位于Bootstrap ClassLoader时,向上传输为空,于是开始扫描目录,在目录中寻找对应的类对象,如果找到了,就进入后续的类加载流程,双亲委派工作就结束了。
3)如果没有在这一层找到,则到下一层区寻找,如此重复,在最低的一层也没找到时,就会报错。
这样设计的核心目的是为了防止用户自己写的类把标准库覆盖。
保护标准库的类,被加载的优先级是最高的,扩展库其次,第三方库最低。
三.垃圾回收机制(GC)
C/C++中使用malloc/new时,都需手动释放掉内存free/delete(如不释放,会产生内存泄漏)
C++针对内存泄漏,给出的方案为“智能指针”
Java对内存泄漏给出了更系统更完整的解决方案(GC)
Java引入了垃圾回收机制,可以让程序员大胆new,JVM会自动识别哪些new完的对象不再使用,就会把这样的对象自动释放掉。
但GC也是有代价的
1)需要在JVM中引入额外的逻辑,销毁不少CPU资源进行垃圾扫描与释放
2)进行GC的时候可能触发STW(Stop The World)问题,导致程序卡顿
STW对性能要求高的场景影响很大
JVM中有好几个内存区域,其中GC负责回收堆与元数据区中的垃圾,而栈和程序计数器是跟随着线程的,不用回收。
垃圾回收是以对象为单位的。
GC具体回收过程
1)先找出哪些是垃圾
需要针对每个对象分别判定,判断是否为垃圾
在Java中使用一个类,一般是通过引用来使用的
方案一:引用计数
给每个对象分配一个计数器,衡量有多少个引用指向
每次增加一个引用,计数器+1
每次减少一个引用,计数器-1
当计数器为0时,此时该对象就是垃圾。
如:
new一个Test对象,每新增一个引用,程序计数器加1,
当把引用置为null时,程序计数器减少,当计数器为0时,该对象就是垃圾。
但是,JVM并为引入该方案(Python,PHP引入了该方案)
上述方案存在着两个问题
1)引用计数需要额外的内存,如果对象少,则没什么;如果对象一多,将消耗大量内存。
2)引入计数可能导致“循环引用”,使得上述判定出错
循环引用问题
先创建两个对象,引用分别为a和b,此时引用计数各为1
然后,相互赋值,将a引用赋值到b,b引用赋值到a,引用计数各加1
再将a和b引用置为空
此时,两对象引用计数为1,都不能释放,但对象又无法使用。
综上所述,JVM并为采用该方案
方案二:可达性分析
在JVM中专门搞了波线程进行周期性的扫描代码中所有对象,判定某个对象是否可达(能否被访问到),如果不可达,则标记为垃圾。
扫描的过程,类似于树的遍历,通过一些GC root节点将每一个结点进行遍历,访问更多的对象。
可达性分析比较消耗时间,尤其是类对象多的时候。
2)释放垃圾的内存空间
方案一:标记-清除
直接针对内存中的对应对象进行释放,例如:
直接释放被标记的内存,会引发“内存碎片问题”
虽然把上述内存释放掉,但整体这些内存并没有连在一起,后续申请内存时就可能申请不了(申请的内存一定是连续的)
尽量避免内存碎片问题,是释放内存的关键问题
方案二:复制算法
将一整块内存一分为二,同一时刻只使用其中的一侧。
标记好哪些是垃圾后,把不是垃圾的数据拷贝到另一侧的内存中,然后释放掉原先那一侧的全部内存,并确保另一侧的内存连续。
复制算法的缺点也是明显的
1)内存空间利用率低(只使用了一半内存)
2)如果存活的内存对象比较多,复制拷贝的成本则会很大
方案三:标记—整理
类似于顺序表删除中间元素的操作
先标记垃圾(释放的内存),再将后面不是垃圾的内存覆盖到垃圾上
此方案搬运开销也不少。
JVM中真正的解决方案是将上述几个方案综合整理,取长补短=>分代回收
方案四:分代回收
JVM根据对象的“年龄”(可达性分析周期性扫描,被扫描的次数越多,越老 )
年轻的称为新生代,年老的称为老生代。
整个堆的内存空间如此划分
其中,还可以区分更多区域
伊甸区:根据经验规律,绝大部分活不过第一轮GC。
幸存区:GC后没有被回收的对象会拷贝到幸存区,其中幸存区分两个,如果对象在第一个幸存区时间久,则会被拷贝到第二个幸存区。
达到一定程度后,会被拷贝到老年代
老年代中的对象生命周期都会比较长,但也要进行可达性分析,但进行GC的频率会降低。
分代回收是JVM的GC中的基本思想。
具体落实到JVM实现层面上,JVM提供了多种垃圾回收器
当前主要为CMS和G1
以上便是JVM的全部内容,如有不对,欢迎指正