聊聊JVM的那些事

聊聊JVM的那些事_第1张图片

ee 

目录

1.JVM的内存区域划分

          1.1程序计数器

          1.2栈

          1.3堆

          1.4方法区

2.JVM的类加载机制

          2.1类加载的意义

          2.2类加载的环节

                    2.2.1Loading

                    2.2.2Linking

                    2.2.3Initializing

          2.3经典面试题

          2.4双亲委派模型

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

         3.1找垃圾/判断垃圾

                   3.1.1基于引用计数

                   3.1.2基于可达性分析

        3.2释放垃圾

                   3.2.1标记 - 清除

                   3.2.2复制算法

                   3.2.3标记 - 整理

                   3.2.4分代回收


1.JVM的内存区域划分

JVM的内存是从操作系统中申请出来的,与此同时,JVM就把这个场地也划分成了不同的区域。另外我们需要知道的是,下面这个内存划分区域,不一定是符合实际情况的,JVM在实现的过程中,会因为很多原因而导致划分形成不同的结果。

          1.1程序计数器

这是内存中最小的区域,保存了下一条要执行的指令的地址在哪里,这里的指令就是字节码的意思。程序要想运行,JVM就得把字节码加载起来,放在内存中,程序就会一条条把指令从内存中取出来,放到CPU上执行,在执行的过程,要随时清楚,当前执行到哪一条了。并且CPU是并发执行程序,CPU不仅仅是要给你一个进程提供服务的,同时它要服务所有的进程。正是因为操作系统是以线程为单位进行调度执行的,每个线程都得记录自己的位置,因此,程序计数器中每个线程都有一个。

          1.2栈

①什么是栈?

存放局部变量和方法调用信息的内存空间大小。

②栈的特点:

每个线程都有一份栈,当调用一个新的方法时,就会涉及到“入栈”操作,而每当执行完一个方法后,就会涉及到“出栈”的操作。而这里的“入栈”,“出栈”和数据结构中所学的栈的机制是差不多的。

在JVM中栈的空间大小实际上是比较小的,一般就是几M,或者几十M,因此栈很容易就会满,而当栈满的时候就会抛出栈溢出异常,StackOverflowException。

③实例:

聊聊JVM的那些事_第2张图片

          1.3堆

①什么是堆?

存放成员变量和new的对象的内存空间大小。

②堆的特点:

堆是内存中空间最大的区域。堆和栈不同,对于JVM中的栈来说,每个线程就有一个栈,而对于堆而言,每个进程才有一份,多个线程共用一个堆。

③示例及常见问法:

聊聊JVM的那些事_第3张图片

正确的应该是局部变量和调用方法在栈上,而成员变量和new的对象在堆上。 

          1.4方法区

①什么是方法区?

用于存放“类对象”的内存空间被我们称为方法区。

②对类对象的剖析:

.java -> .class(二进制字节码),.class会被加载到内存中,也就被JVM构造成了类对象,而这个加载的过程,就被称为"类加载"。而何为类对象?类对象实际上就是用来描述该类实际是如何的,比如:类的名字是啥,里面有哪些成员,有哪些方法,每个成员的名字以及类型,以及每个方法的名字和类型。以及方法中的不同指令,类对象中还有个很重要的东西,叫做静态成员,static修饰的成员,称为“类属性”,而普通的成员,叫做“实例属性”。

2.JVM的类加载机制

          2.1类加载的意义

其实就是设计一个运行时环境的一个重要的核心的功能

          2.2类加载的环节

类加载就是要将.class文件加载到内存中,构建成类对象。一个类的生命周期如下:

聊聊JVM的那些事_第4张图片

                    2.2.1Loading

①什么是loading?
先找到对应的.class文件,然后打开并读取.class文件,同时初步生成一个类对象。而loading中最关键的一个环节就是.class文件里面到底是什么样的。而通过读取,会把读取之后解析到的信息,初步填写到类对象中

②示例一个解析后的信息的结构模板:

聊聊JVM的那些事_第5张图片

无论是要实现编译器,还是JVM都需要按照这个格式来进行构造。 

                    2.2.2Linking

①verification:
这主要是起到校验的作用。而在此处,主要校验的是过程。验证读到的内容是不是和规范中规定的格式完全匹配,如果发现这里读到的数据格式不符合规范,那么就会类加载失败,并且抛出异常。

②Preparation:
准备阶段是正式为类中定义的变量(这里指的是静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。给静态变量分配内存,并且设置上0值

③Resolution:
解析阶段是java虚拟机将常量池内的符号替换成为直接引用的过程,也就是初始化常量的过程。.class文件中,常量是集中放置的,每个常量有一个编号。.class文件的结构体里初始情况下只是记录了编号。就需要根据编号找到对应的内容,将其填充到类对象中。

简单来讲就是一个完善省略符号表示实际含义的过程。

                    2.2.3Initializing

真正对类对象进行初始化,尤其是针对静态成员。

          2.3经典面试题

①题的代码:分析下面题,我们自己试着写出结果:

class A{
    public A(){
        System.out.println("这是A的构造方法");
    }
    {
        System.out.println("这是A的代码块");
    }
    static{
        System.out.println("这是A的静态代码块");
    }
}
class B extends A{
    public  B(){
        System.out.println("这是B的构造方法");
    }
    {
        System.out.println("这是B的代码块");
    }
    static{
        System.out.println("这是B的静态代码块");
    }
}
public class test extends B{
    public static void main(String[] args) {
        new test();
        new test();
    }
}

②结果如下:

聊聊JVM的那些事_第6张图片

 解释也放在图中了。 总结来说,就是类加载的时候是第一步,就会执行对应其父类的静态代码块,并且静态代码块只会执行一次,而构造方法和构造代码块每次实例化都会执行,并且代码块的优先级是高于构造方法的。

          2.4双亲委派模型

①什么是双亲委派模型:

双亲委派模型描述的就是JVM中的类加载器,如何根据类的全限定名(java.lang.string )找到.class文件的过程。JVM里提供了专门的对象叫做类加载器,负责对类来进行加载。当然找文件的过程也是类加载器来负责的。
.class文件,可能放置的位置有很多,有的要放到jdk目录里,有的放到项目目录里,还有的放置在其它的特定位置。因此JVM里面提供了多个类加载器,每个类加载器负责一个片区。默认的类加载器,主要是3个。

a.BootStrapClassLoader:负责加载标准库中的类(String,ArrayList,Random,Scanner…)
b.ExtensionClassLoader:负责加载JDK扩展的类(现在很少用到)
c.ApplicationClassLoader:负责加载当前项目目录中的类
d.自定义类加载器,来加载其他目录中的类。比如,Tomcat就自定义了类加载器,用来专门加载webapps里面的.class

②如何实现的:

(1)考虑加载java.lang.string:

程序启动,就会先进入ApplicationClassLoader类加载器
ApplicationClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器ExtensionClassLoader
ExtensionClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器BootStrapClassLoader
BootStrapClassLoader类加载器也会检查下,它的父加载器是否已经加载过了,然后发现没有父亲,于是就扫描自己负责的目录
然后java.lang.String这个类就在标准库中能找到,然后后续就由BootStrapClassLoader加载器负责后续的加载过程,查找环节就结束了。

执行图如下:

聊聊JVM的那些事_第7张图片

 (2)考虑加载自己写的Test类:

程序启动,就会先进入ApplicationClassLoader类加载器
ApplicationClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器ExtensionClassLoader
ExtensionClassLoader类加载器就会检查下,它的父加载器是否已经加载过了,如果没有,就调用父 类加载器BootStrapClassLoader
BootStrapClassLoader类加载器也会检查下,它的父加载器是否已经加载过了,然后发现没有父亲,于是就扫描自己负责的目录,没扫描到,就会回到子加载器中继续扫描
ExtensionClassLoader扫描自己负责的目录,也没有扫描到,再回到子加载器中继续扫描
ApplicationClassLoader也扫描自己负责的目录,自己写的类就在自己的项目目录下,因此就能找到,然后后续的类加载就由ApplicationClassLoad完成,此时查找目录的环节就结束了~~(另外如果ApplicationClassLoader也没有找到们就会抛出ClassNotFoundException异常)
执行图如下:

聊聊JVM的那些事_第8张图片

如果觉得太过抽象,那么这里举个例子:

就类似于在公司里,一个员工发现了一个问题,首先得报告给主管,主管得报告给老板,要是老板觉得这个问题太简单了,就会让他主管自己决定,然后主管觉得太简单了,就会让员工自己处理。当然老板和主管愿意处理的话,处理完成,下面的人就不用再进行处理了。

③为什么JVM要这样设计:

是为了确保当我们自己写的类和标准库中的类,全限定类名重复了,也能够顺利的加载到标准库中的类。如果是自定义加载器的话,是否也要遵守双亲委派模型呢?答案是不一定。既可以遵守,也可以不遵守。看需求,比如Tomcat中的webapp中的类就没有遵守,当然这里遵守了也没有什么实际意义。 

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

         3.1找垃圾/判断垃圾

下面介绍两种主流思路。

                   3.1.1基于引用计数

①什么是基于引用计数:

针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向它。画个图示例:

聊聊JVM的那些事_第9张图片

t是一个对象, new Test()也是一个对象,所以先会额外引入两小块内存,引用计数也会引入额外一小块内存,创建第一个对象的时候,有一个引用,当Test t2=t时,又有一个引用,所以引用数为2,当这个内存不在使用时,就会被释放,那么相应的引用计数就会减1,直到减到0为止,而当引用计数为0了,也就是没有引用指向这个对象了,换句话说就是没有代码能够访问到这个对象了,我们就认为这个对象是个垃圾。同样我们举一个图例:

 ②优缺点:

a.优点;

简单且高效。

b.缺点:(有两个致命缺点)

(1)空间利用率比较低

因为每当我们新new一个对象,都会搭配一个计数器(我们这里假设这个计数器的大小为4字节),这个时候,如果对象本身很大,自然是影响不大,但要是对象本身就4字节左右的话,这个时候这个计数器就显得太浪费。空间利用率就太低了。

(2)会有循环引用的问题

聊聊JVM的那些事_第10张图片

 ③值得注意的点:

这不是Java中采取的方案,这是Python及其他语言的方案。因此在面试时,我们要搞清楚面试官问的到底是什么。

                   3.1.2基于可达性分析

①什么是基于可达性分析:

可达性分析指的是通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置,我们把它称为GCRoots,会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(此时,带有标记的对象就是可达的对象),没有被标记的对象就是不可达的,换言之,就是垃圾。

而这里的GCRoots的遍历位置,往往是以下三种:
a.栈上的局部变量
b.常量池中的引用指向的对象
c.方法区中的静态成员指向的对象
这个给大家举个例子,便于大家更好的理解可达与不可达。

聊聊JVM的那些事_第11张图片

②优缺点:

优点:
克服了引用计数的两个缺点:空间利用率低,循环引用。
缺点:
系统开销大,遍历一次可能比较慢。
③注意:

这个就是java采取的方案。

        3.2释放垃圾

明确了谁是垃圾之后,接下来就要谈论回收垃圾了。回收垃圾实质上就是释放内存

                   3.2.1标记 - 清除

①什么是标记 - 清除?

标注就是可达性分析的过程,而清除,就是直接释放内存。我们用一个数组来表示这个过程,垃圾我们用绿色来表示。

这个时候,我们就需要清除他们; 

②优缺点:

优点:方便,可以直接清除。

缺点:当我们清除这个垃圾后,就变成了一个个离散的空间。这个时候就造成了空间碎片,可能过多,可能过少,利用率就大大降低了。

                   3.2.2复制算法

①什么是复制算法?

复制算法就是为了解决上述这种离散空间碎片的问题。它的原理是用一半,丢一半。就是将原来的空间划成两份。把是垃圾的放在一起,不是垃圾的放在一起,最后释放掉是垃圾的部分。(这种放在一起就是利用的拷贝的原理),那么最后就可以得到连续的内存空间。

②优缺点:

优点:

解决了内存碎片的问题

缺点:

内存空间利用率低,当要保留的对象较多,释放的对象较少时,此时复制的开销就很大了。

                   3.2.3标记 - 整理

①什么是标记 - 整理:

类似于删除顺序表中间的元素,然后将后面有空间的移位至于前面。实质上就是一个搬运的工作。

  ②优缺点:

优点:

针对复制算法解决了问题,空间利用率最高

缺点:

但是没有解决复制搬运过程的开销大的问题。

                   3.2.4分代回收

①什么是分代回收?

分代回收实质上就是结合了上述多种方案来完成的操作:

原理是针对对象进行分类(根据对象的“年龄”来进行分类):一个对象,每经过一次GC扫描,就相当于“长了一岁”。接着我们根据不同年龄的对象,来采取不同的方案。

我们以下面这张图来进行讲解:

聊聊JVM的那些事_第12张图片

a.伊甸区:

我们把刚创建出来的对象放在伊甸区

b.幸存区:

如果在伊甸区经过一轮GC扫描,就会被拷贝到幸存区A(这里就是指第一个幸存区) 

在后续的几轮GC中,幸存区的对象就在两个幸存区之间来回拷贝,这里用的是复制的算法,每轮都会淘汰一批幸存者

c.老年代:

在持续若干轮后,对象终于可以进入老年代,老年代里的对象都是年龄较大的,我们默认一个对象越老,继续存活的可能性就越大,那么老年代GC的扫描概率大大低于新生代,老年代中就会使用标记整理的方式来进行回收

②举个例子:

其实这个阶段就很像我们考研的过程。最开始有很多人决定考研,这个时候大家都在伊甸区,一段时间后,一些人放弃了,剩下的就进入了幸存区,经过时间反复沉淀,就会有很多放弃的,最后一批人进了复试,进入了老年代,但是复试中仍然会有表现不好,而被刷的。

③注意:

分代回收中,还有一个特殊情况,有一类对象可以直接进入老年代,(占有较多内存的对象),但是大对象拷贝开销比较大,不适合使用复制算法

之后开始更Spring啦

你可能感兴趣的:(JavaEE,jvm,java,算法)