[Javaee]JVM

前言

本篇主要介绍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的全部内容,如有不对,欢迎指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值