🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. JVM内存划分
- 1.1 堆区
- 1.2 程序计数器
- 1.3 元数据区
- 1.4 Java虚拟机栈
- 1.5 本地方法栈
- 1.6 不同形态的变量在数据区中的存储
- 2. JVM类加载
- 2.1 类加载过程
- 2.2 双亲委派模型
- 2.2.1 什么是双亲委派模型
- 2.2.2 工作过程
- 3. JVM垃圾回收机制(GC)
- 3.1 什么是GC
- 3.2 GC的工作流程
- 3.2.1 判断谁是垃圾
- 3.2.2 释放对应内存(垃圾回收)
1. JVM内存划分
JVM本质上是一个Java进程,这个进程一旦跑起来,就会从操作系统中申请一大块内存空间.那么JVM接下来就要对这一大块空间进行划分,每个区域都有不同的功能.划分出来的也叫JVM运行时数据区或者也叫内存布局.
1.1 堆区
- 堆区是各个数据区中最大的区域.
- 他的作用是:程序中创建的所有对象都保存在堆中.
- 这个区域是所有线程共享的.
- 堆中分为两个区域,一个是新生代,一个是老年代,新生代中还有三个区域,一个是伊甸区(Eden),两个生存区(s1/s0).具体这几个区域中都存放什么样的对象,我们后面讲到垃圾回收机制的时候再说.
1.2 程序计数器
- 这个区域是数据区域中最小的区域.
- 他的作用是:用来记录当前线程执行的行号.也就是当前要执行的下一条指令(JVM字节码)的地址,这个地址就是元数据区中的一个地址.
- 这块区域是各个线程都有自己单独的一块,也就是线程私有的.
1.3 元数据区
- 他的作用是:用来存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
- 创建一个类之后产生的类对象就存储在这里.
- 方法相关的信息也存储在这里,类中都有一些成员方法,每个方法都代表一系列"指令集合"(JVM字节码指令).
- 这里还存储着常量池,不仅仅是string类型,也可能是数字等其他类型.
- 这个区域是各个线程共享的.
1.4 Java虚拟机栈
- 线程私有
- Java虚拟机栈分为一下四部分:
- 局部变量表:存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进⼊⼀个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
- 操作栈:每个方法会⽣成⼀个先进后出的操作栈。
- 动态链接:指向运行时常量池的⽅法引用。
- ⽅法返回地址:PC寄存器的地址。
1.5 本地方法栈
- 和Java虚拟机栈类似,
- 也是线程私有的
- 他两的区别就在于,Java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的.
1.6 不同形态的变量在数据区中的存储
基本原则:一个对象在哪个区域,取决于对应变量的形态.局部变量在栈上,成员变量在堆上,静态成员变量在元数据区.下面我们来举例说明:
class Test2{}
class Test{
int a;
Test2 t2 = new Test2();
String s = "hello";
static int b;
}
public static void main(String[] args){
Test t = new Test();
}
- 在
main
方法中的t变量在栈上,用来保存对象的首地址.(堆上的地址). new Test()
在堆上.是通过new创建出的一个对象.new Test2()
在堆上,和上一个是同理,也是通过new创建出的一个对象.- Test类中的成员变量都在堆上.
- static修饰的是属于类的属性,就会出现在元数据区的类对象中.
2. JVM类加载
一个Java进程要想跑起来,就要把Java先变成**.class文件.加载到内存中**,得到"类对象".这个所谓的跑起来,就是执行指令,要执行的CPU指令,都是通过字节码让JVM翻译出来的.
2.1 类加载过程
对于类加载的过程,总共分为一下几个步骤:
- 加载: 在硬盘上找到.class文件,读取文件内容.
- 连接
- 验证:.class里的内容,是否符合要求.class文件格式的内容在官方文档中有明确的规定,把读取到的内容,往这一套标准中套入,即可判断是否符合要求
- 准备:给类对象分配内存空间,分配之后,把这个空间里的数据先全部填充为0.
- 解析:针对字符串常量初始化,把刚才.class文件中常量内容取出,放到元数据区.
- 初始化:针对类对象的初始化,其中就包含对静态成员初始化,执行初始化,执行静态代码块.注意不是针对对象的初始化.
2.2 双亲委派模型
2.2.1 什么是双亲委派模型
在上述三部中,“加载"的过程中.会根据代码中"全限定类名”(包名+类名)找到对应的.class文件.在JVM加载.class文件,并找到.class文件的时候,就要用到双亲委派模型.
在JVM中包含这样一个特定的模块,叫做"类加载器",这个类负责完成后续的类加载工作.
如果⼀个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每⼀个层次的类加载器都是如此.
2.2.2 工作过程
- 这其中有三个类加载器: AppClassLoader,ExtClassLoader,BootStrapClassLoader.这三个类加载器存在父子关系,如上图所示.
[注意] 这里的父子关系不是通过类的继承方式来表示的,而是通过类加载器中的"parent"字段指向自己的父亲. - 三个类加载器的作用:
- BootStrapClassLoader:加载标准库中的类.
- ExtClassLoader:加载JVM扩展库中的类.(各JVM厂商,在上述的标准上,对JVM进行了扩展,这样的扩展在JVM标准之外,但是在安装了JVM就有.
- AppClassLoader:加载第三方库的类和自己写的代码的类.
- 他们几个类加载器的工作过程就类似于望父成龙的过程.
- 工作从AppClassLoader开始,这个类加载器并不会立即搜索第三方库相关的目录.而是把任务交给自己的父亲来进行处理.也就是ExtClassLoader.
- 工作到了ExtClassLoader,也不会立即对自己负责的扩展库进行搜索,也是把任务交给自己的父亲来处理.
- 工作到了BootStrapClassLoader,BootStrapClassLoader也想交给自己的父亲进行处理.但是它的parent指向null,只能自己处理.在BootStrapClassLoader负责的标准库中的路径中搜索上述的类.
- 如果找到了,就搜索完成了,类加载器负责打开文件,读取文件等后续操作.
- 如果没有找到,任务还是要交给儿子来处理.
- 工作回到ExtClassLoader,搜索扩展库的类,如果找到了,搜索完成,如果没找到,继续交给儿子处理.
- 工作回到AppClassLoader,搜索第三方库中的类和自己创建的类.
举例说明:上司与下属处理问题
下属在发现一个问题的时候,不可以自己擅自处理,需要向上级上报,如果上报给上级之后,上级觉得这个问题非常重要,上级就会亲自处理,如果问题不是很重要,上级就会对下属说:"你自己看着办吧."于是下属就有权利处理这个问题了.
- 双亲委派模型适用于:自己写的类和标准库/扩展库冲突,JVM会确保加载的类是标准库中的类.
- 双亲委派模型可以破坏掉,我们可以通过自己实现类加载器来破坏模型.
3. JVM垃圾回收机制(GC)
3.1 什么是GC
这是Java提供的对于内存自动回收的机制.更本质地来说,GC其实回收的是"对象",回收的是"堆上的内存".回收对象的时候,一定是一次回收一个完整的对象,不能回收半个.
为什么GC只回收堆上的内存,不回收其他区域的?
- 程序计数器:不需要额外回收,他是每个线程私有的,线程销毁之后,自然就回收了.
- 栈:不需要额外回收,他是每个线程私有的,线程销毁之后,自然就回收了.
- 元数据区:一般也不需要,都是加载类,很少卸载类.
3.2 GC的工作流程
GC的工作流程分为两步:
- 找到谁是垃圾
- 释放对应的内存
3.2.1 判断谁是垃圾
判定一个对象是否是垃圾,判定的方式比较保守.判定某个对象是否存在引用指向它.使用对象,都是通过引用的方式来使用的,如果没有引用指向这个对象,意味着这个对象无法再代码中使用.于是就可以视为垃圾.
- 引用计数算法
[注意] 这种算法是PHP和python采用的算法,不是JVM采用的算法.
给对象增加⼀个引用计数器,每当有⼀个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死".
但是这种方法存在很大的缺点,所以JVM才没有采用这样的方式.- 额外消耗的存储空间较大
- 引用计数无法解决对象的循环引用问题
什么是对象循环引用问题,我们来看下面这样一段代码. - 首先创建了两个对象,并使用a,b引用指向他们,对象的计数+1.
- 之后两个对象中的t成员变量分别指向对方的引用,对象的计数+1.
- 之后让a,b指向null,a,b指向的对象的计数-1,但是还没有减为零,所以两个对象没有被视为垃圾回收掉.
class Test{
Test t;
}
public static void main(String[] args){
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;
}
- 可达性分析算法
为了避免上述的两个问题,所以JVM引入了可达性分析算法.解决了空间和循环引用的问题.但是也付出了时间上的代价.
这种算法的核心思想就是:遍历.
JVM把对象之间的引用关系,理解为一个树形结构.JVM会不停遍历这样的结构,把所有可能遍历访问到的对象标记为"可达",剩下的是"不可达".
例如下面这棵树:
从root出发,任何一个树上的结点都可达.
如果现在令c.right = null
,这样的话,f也无法访问到了,f就会被标记为"不可达",就会把f标记为垃圾.a.right = null
,此时c就不可达,f也不可达,c和f也会被标记为垃圾.
JVM会周期性对所有的树进行遍历,不停标记可达/不可达.
3.2.2 释放对应内存(垃圾回收)
把垃圾标记出来之后,就要对垃圾进行回收操作了.
- 标记-清除算法
这个算法分为标记和清除两个阶段.首先需要标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象.
但是这种算法有一些不足:- 效率问题:标记和清除这两个过程的效率都不高.
- 空间问题:标记清除之后会产生大量的不连续的内存碎片,空闲的内存被分成了一个碎片,不集中.空间碎片太多可能会导致以后子啊程序运行的时候需要分配较大对象的时候,无法找到足够连续的内存.
- 复制算法
当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另⼀块上面,然后再把已经使用过的内存区域⼀次清理掉.
但是这种做法也有一个致命的缺点:复制的时间开销很大. - 标记-整理算法
标记过程仍与"标记-清除"过程⼀致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向⼀端移动,然后直接清理掉端边界以外的内存.;类似于顺序表删除中间元素的操作.
这种算法实质上和复制算法差别不大,也是会涉及到复制操作,也会产生较大的时间开销. - 分代算法
这种算法就是根据对象的年龄进行垃圾回收.所谓的年龄就是经过GC扫描的次数.按照年龄,把对象分为新生代和老年带,其中新生代又分为伊甸区和生存区.这就和我们前面所提到的堆的数据区接轨了.
- 新创建的对象,会被放到伊甸区,伊甸区的大部分对象,生命周期都比较短,第一轮GC就有可能成为垃圾,只有极少数可以活过第一轮来到生存区.
- 生存区有两个,每经过一轮GC,生存区都会淘汰一批对象,剩下的进入到另一个生存区,到达另一个生存区是通过复制实现的.此外还有伊甸区新来的对象.每复制一次,年龄+1.就这样在两个生存区之间来回周转.
- 老年代对象也要经过GC,但是老年代的生命周期更长,就可以降低GC的频率.这里对老年代的GC,是通过标记=整理算法来完成的.
举例说明:校招找工作
- 伊甸区:投放简历,大量简历被直接淘汰
- 生存区:简历通过,要经过笔试,技术面,HR面等多轮筛选.
- 老年代:进入公司,但是会定期考核.