目录

JVM介绍

JVM介绍

一.JVM内存区域划分

1.内存区域介绍

1)程序计数器(比较小的空间)

保存了下一条要执行指令的地址。此处的程序计数器不再是CPU的寄存器了,而是内存空间。并且,此处的下一条要执行的指令是Java的字节码(不是CPU的二进制的机器语言)

2)堆

JVM上最大的空间,new出来的对象都在堆上。

3)栈

函数中的局部变量,函数的形参,函数之间的调用关系。

Java虚拟极栈:JVM之上运行的Java代码的方法调用关系

本地方法栈:JVM里头,C++代码的函数调用关系。

4)元数据区(方法区)

Java程序中的指令(指令都包含在类的方法中)

保存了代码中涉及到的类相关的信息

类的static属性

在一个Java进程中,元数据区和堆是只有一份的(同一个进程中的所有线程都是共用一份数据的),程序计数器和栈可能有多份,当一个Java进程中有多个线程的时候,每个线程都有自己的程序计数器和栈

线程就代表一个执行流,每个线程就需要保存自己的程序计数器,每个线程也需要记录自己的调用关系。

https://i-blog.csdnimg.cn/direct/498ba531ec3f439d9427e1941ae98703.png

2.代码中变量位于的区域

类似于这样一段的代码,需要明确其中变量存在于JVM内存区域的哪个区域。一个变量处于哪个内存区域,这个和变量是不是“内置类型”无关,而是和变量的形态有关。

(1)局部变量:栈

(2)成员变量:堆

(3)静态成员变量:元数据区(方法区)

https://i-blog.csdnimg.cn/direct/0110ee348e8548639a246d43642ccd48.png#pic_center

a,b都是成员变量,均在堆区。

c,d是静态成员变量,均在元数据区(d是new了一个对象,new出来的对象是在堆区,但是把这个堆上的内存地址,赋值给了d引用类型的变量)

e,f都是局部变量,均在栈区(f也是new了一个对象, new出来的对象依旧在堆区,但是把这个堆上的内存地址,赋值给了这个f变量)

二.JVM类加载的过程

运行java进程的时候,JVM就需要读取 .class中的内容,并且执行里面的指令(读取 .class中的内容就是类加载,把类涉及到的字节码从硬盘读取到内存中(元数据区),把这个 .class 中的指令转换成类对象)

加载一个 .class文件,就会对应创建一个类对象,类对象就包含了 .class文件中的各种信息(比如:类名,类的属性和方法,继承的父类,实现的接口等等······)也就说明类对象就是对象的说明书/蓝本

1.类加载的具体步骤

类加载分为五个环节,也可以分为三个环节,无论是那种都是对的,只是一些环节被归为一类而已,此处说的是五个环节

(1)加载

把 .class 文件找到,代码中先见到类的名字,然后进一步的找到对应的 .class 文件(涉及到一系列目录查找的过程)找到后,

打开并读取文件内容

(2)验证

验证读到的 .class 文件的数据是否正确与合法,在Java标准文档中,明确定义了 .class 文件的格式是怎么样的

https://i-blog.csdnimg.cn/direct/36b43245a7894c5ab5b0815402867adf.png#pic_center

https://i-blog.csdnimg.cn/direct/d4ffdbf764e440c7a2a7e80afbd0eb99.png#pic_center

u4:4个字节的unsigned int,u2:2个字节的unsigned short这是在Java中的,c++则不一定,c++会因为不同的操作系统和编译器导致l同类型的字节数不同。

magic:魔幻数字,在二进制文件中的开头,有若干个字节,设置一个固定的常数进去,通过这个常数,标识当前这个文件是什么样的文件

minor_version和major_version:确保编译和运行时的JDK版本一致

constant_pool_count:常量池

cp_info:其他结构体

access_flags,this_class,super_class:代表这个类是public还是其他的修饰词,后续代表的是类和父类

fields_count ,fields[fields_count]:代表类中的属性

后面的都代表类中的方法

https://i-blog.csdnimg.cn/direct/f640fb9dd58d4ca9ad65bf5c3bcf4a8e.png#pic_center

(3)分配内存空间

根据刚才读取到的内容,确定出类对象需要的内存空间,再申请这样的内存空间,并且把内存空间中的所有内容,都初始化为0(Java创建一个内存空间,都会把这个内存空间全部设置为0,后续再进一步的初始化)

类加载执行到第三步时,此时a的值还为0

https://i-blog.csdnimg.cn/direct/9a2e8da80634479e9da720bbdf405940.png#pic_center

(4)解析

主要针对类中的字符串常量进行处理,解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

直接引用:平时谈到的代码中的引用,里面也保存了变量的地址。

符号引用:字符串常量,已经在 .class 文件中。

“hello”字符串常量,已经在 .class 文件中了,但是没有地址这样的概念,此时地址的概念就是内存的地址,取而代之的是,类似于文件偏移量的概念。

https://i-blog.csdnimg.cn/direct/a873814df11348c08c585d3798eaa5f1.png#pic_center

符号引用和直接引用图解:

https://i-blog.csdnimg.cn/direct/0ab84505706349fd86d1ff5e61ceca72.png

(5)初始化

针对类对象做最终的初始化操作,执行静态成员的赋值语句。

https://i-blog.csdnimg.cn/direct/8590303bf8a74867be5487c920676704.png#pic_center

执行类中的静态代码块,针对父类也要进行加载,在这个环节也会触发对父类的进行加载(执行上述的五个环节)

2.双亲委派模型

(1)双亲委派模型介绍

是类加载五个步骤中,第一个步骤里面的一个环节,给定 类全限定名,找到对应的class文件位置。

类加载器JVM中,已经内置了一些加载器,完成上述的内加载过程

JVM默认有三个类加载器:

不是Java父类子类的继承关系,而是类加载器中有一个parent这样的引用指向父亲

https://i-blog.csdnimg.cn/direct/f18aadfc952e467f94a4624428bbc125.png#pic_center

(2)工作流程

图解:

https://i-blog.csdnimg.cn/direct/3c0577f0e1ab4d05b2a0d4729f551753.png

这样做的核心目的:

防止用户自己写的类,把标准库的类给覆盖了

保证标准库的类,被加载的优先级是最高的

扩展库其次,第三方库的优先级最低

三.JVM的垃圾回收机制(GC)

1.需要回收的内存区域

JVM中的内存区域的程序计数器和栈是不需要GC的,因为它们会跟随线程一起销毁。而堆是GC主要回收的地方,堆上new的对象就需要GC判断是否需要回收。元数据区中的类对象的类加载是有上限的,不会出现无限增长的情况,一般不需要GC。

堆的分区

https://i-blog.csdnimg.cn/direct/1de9bcf144fa451c9eed184e659861a3.png

2.回收机制实现

(1)找出“垃圾”

需要针对每个对象进行判定是否为“垃圾”,在Java中使用一个对象,一般都是通过引用来使用的,如果一个对象没有引用指向了,都可以认为这个对象是“垃圾”了。

i.引用计数器

给每个对象分配一个计数器,衡量有多少个引用指向,每次增加一个引用,计数器+1,每次减少一个引用计数器-1,当计数器减为0时,此时对象就是“垃圾”了(多线程的可重入锁也是类似的机制)

图解计数器:

第一步

https://i-blog.csdnimg.cn/direct/f0556578bd26419eb19664b4a38c7ef7.png

第二步

https://i-blog.csdnimg.cn/direct/fbcefc3c22b54b5d842c6e98ce333781.png

第三步

https://i-blog.csdnimg.cn/direct/25772c7d649c45c693049f186b669948.png

第四步

https://i-blog.csdnimg.cn/direct/352e423617a4437193c80f70e108cfa5.png

这样的方案在Java中并没有采纳,而是python和PHP进行了使用

上述方案存在两个问题:

1.消耗了额外的空间(假设Test类就只有一个int成员(4个字节),此时为了引入计数器,最少也要一个short(2个字节),内存就多占用了50%)

2.引入计数器可能导致“循环引用”使得上述的判定出错

第一步

https://i-blog.csdnimg.cn/direct/305498c9fb8a48cc9376a83df8047aae.png

第二步

https://i-blog.csdnimg.cn/direct/4f4ec73838d24b82bd4ab67bf7e74d1c.png

第三步

https://i-blog.csdnimg.cn/direct/7cca1b9154a5473d84cea05921202fca.png

这两个对象的引用计数都不是0,不能被释放,但是这两个对象又无法被使用,就造成了类似于死锁的场景。循环引用也是有解的,需要引入更多的机制(环路检测)代价就更大了。

ii.可达性分析(Java采用的方案)

在JVM中,专门搞了一些周期性的线程,扫描代码中所有的对象,判定某个对象是否是“可达”(可以被访问到),对应的,不可达的对象就是垃圾了。

1)JVM有一个所有对象的总名单

2)JVM针对当前教室里面的对象进行点名操作(被点到的,回答到),没有回答到的,就是缺课的(也就是“垃圾”)

通过类似于二叉树的结构,获取到根节点,就可以对整个树进行遍历,遍历就能够得到每一个节点的对象,此时这些对象都是可

达的。如果此时根节点的右节点为null,则为不可达。

类似于这样的树结构,当c不可达时,f也是不可达的,此时的f的可达性依赖于c的可达性。

https://i-blog.csdnimg.cn/direct/068ce47a92cd4aafb9880dd232fc3421.png#pic_center

可达性分析的起点称为GC root,一个程序中,GC root不是只有一个,而是有很多(栈上的局部变量(引用类型),方法去中,静态的成员变量(引用类型),常量池引用指向的对象,这三个GC root)

可达性分析,其实是挺消耗时间的,尤其是你的程序中,对象特别多的情况下,就需要很多时间。

(2)释放“垃圾”的内存空间

1.标记- 清除

直接针对内存中对应的对象进行释放

https://i-blog.csdnimg.cn/direct/8b9310ce981740d895f2a508f6e166a9.png

但是上述的做法会引入“内存碎片问题”,对象的释放是随机的,很可能释放的这个内存不是连续的,虽然把上述的内存释放掉了,但是整体这些空闲内存并没有连在一起,后续申请内存的时候,就无法申请(申请的内存必须是连续的)

2.复制算法

将一块内存分成两块,同一时刻,只使用其中的一半,当释放垃圾之前,把不是垃圾的对象拷贝到另一半中(确保拷贝的对象是连续的)然后把需要释放垃圾的这一半的空间都是释放了

第一步

https://i-blog.csdnimg.cn/direct/e908ed92733040c49e1c175e1da7d8a9.png

第二步 https://i-blog.csdnimg.cn/direct/0e0a6fbf57e94abb816b7bb0d392945e.png

但是复制算法的缺点也很明显:内存空间利用率太低了;如果存活下来的对象比较多,复制的成本也比较大。

3.标记-整理

非常类似于,顺序表删除中间的元素。

第一步

https://i-blog.csdnimg.cn/direct/ce0ecfafd28d4a5980d6984697f94800.png

第二步

https://i-blog.csdnimg.cn/direct/6b1d79e1ac234182936f831bb0f8c5f6.png

但是像这样进行搬运的开销也不小。

4.分代回收

JVM中真实的解决方案,是把上述几个方案综合一下,取长补短,也就转换成了分代回收

JVM根据对象的年龄(可达性分析,周期性的每次经过一轮扫描,对象仍然存活(不是垃圾)年龄+1),把对象进行区分

新创建的对象存储在伊甸区,根据经验规律,绝大部分的新对象,活不过第一轮GC,留存下来的对象,拷贝到幸存区。

https://i-blog.csdnimg.cn/direct/eae9841a57214991a950f61eab6eed53.png

幸存区是两个相等的空间,也是按照复制算法(反复进行多次在幸存者区的两个内存空间中来回进行复制,只要通过GC的考验,就能够存活下来)

新生代中,真正要拷贝的对象不多(经验规律),但是内存利用率低(幸存区中的另一个内存就会被浪费,但是幸存区的内存空间比较小,所以没关系)

https://i-blog.csdnimg.cn/direct/5b5c9184e037407caceecffe2e971cdd.png

如果一个对象在幸存区,已经反复被拷贝多次,也不是垃圾,年龄则会不断增长,达到一定程度后,对象就要被拷贝到老年代了。

根据经验规律,老年代中的对象的生命周期都会比较长,老年代的对象当然也需要进行可达性分析,但是老年代进行GC的频率就会降低,另外,老年代也是通过标记整理(需要整理的次数也不多)

3.其他的垃圾回收器

分代回收时JVM中GC的基本思想方法,具体落实到JVM的实现层上,JVM还提供了多种“垃圾回收器”(对上述的分代回收,做进一步的扩充和具体实现)

重点了解CMS和G1

https://i-blog.csdnimg.cn/direct/df6df5e9dac44ed2bee09db1cbaae05e.png#pic_center

CMS:

https://i-blog.csdnimg.cn/direct/1f1d0edd83bc46dda4e5b620c9fda213.png

G1: https://i-blog.csdnimg.cn/direct/c41ce06e55b34e8298b7bff19afb5f9b.png