浅谈JVM

目录

一、JVM简介

二、JVM 运行流程

三、JVM中的内存区域划分

1、堆

2,Java虚拟机栈

3,本地方法栈

4,程序计数器

5,方法区

四、JVM延申知识点

1,数据类型和引用类型

2,解引用和对象

3,局部变量,成员变量和静态变量

4,静态方法和普通方法

五、GC

1,GC回收哪些内存

2,死亡对象的判断算法

引用计数算法

可达性分析算法

3,垃圾回收算法

标记清除算法

标记复制算法

标记整理算法

4,对象的一生

5,垃圾回收器

6,深拷贝与浅拷贝

六、类加载器

1,类加载过程

3,双亲委派模型

4,能否违背双亲委派模型


一、JVM简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

常见的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:

1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;

2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。

JVM 是一台被定制过的现实当中不存在的计算机。

二、JVM 运行流程

浅谈JVM_第1张图片

程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码 文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调 用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

三、JVM中的内存区域划分

浅谈JVM_第2张图片

堆(运行时常量池)

new的对象放在堆里

方法区

加载好的类放在方法区,静态成员也在

局部变量

程序计数器

存的是地址,描述当前线程,接下来要执行的指令在那个地方

什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时 刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能 恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存 储。我们就把类似这类区域称之为"线程私有"的内存。

1、堆

堆的作用:程序中创建的所有对象都在保存在堆中。

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象 会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

2,Java虚拟机栈

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数 栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。

浅谈JVM_第3张图片

        局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变 量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变 量。

        操作栈:每个方法会生成一个先进后出的操作栈。

        动态链接:指向运行时常量池的方法引用。

        方法返回地址:PC 寄存器的地址。

3,本地方法栈

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用 的。

4,程序计数器

程序计数器的作用:用来记录当前线程执行的行号的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是一个Native方法,这个计数器值为空。

5,方法区

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。

四、JVM延申知识点

1,数据类型和引用类型

浅谈JVM_第4张图片

2,解引用和对象

浅谈JVM_第5张图片

3,局部变量,成员变量和静态变量

局部变量在栈上

浅谈JVM_第6张图片

成员变量在堆上

浅谈JVM_第7张图片

静态变量在方法区

浅谈JVM_第8张图片

4,静态方法和普通方法

普通方法中有this,和实例相关

静态方法没有this,和类相关,和实例无关

五、GC

1,GC回收哪些内存

堆:主要回收这里

方法区:GC需要回收方法区的内存,但是方法区空间小,数据失去作用的概率低

栈:不需要回收,栈上的内存核实释放时机是明确的(线程结束,栈上的内存就被释放了)

程序计数器:只是存了地址,不需要回收

2,死亡对象的判断算法

引用计数算法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任 何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采 用引用计数法进行内存管理。

但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题

浅谈JVM_第9张图片

可达性分析算法

此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索 走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的

class Node{
    public int val;
    public Node right;
    public Node left;

    public Node(int val) {
        this.val = val;
    }
}
public class Text {

    public static Node build(){
        Node a = new Node(1);
        Node b = new Node(2);
        Node c = new Node(3);
        Node d = new Node(4);
        Node e = new Node(5);
        Node f = new Node(6);
        Node g = new Node(7);

        a.left = b;
        a.right = c;
        b.left = d;
        b.right = e;
        e.left = g;
        c.right = f;
        return a;
    }

    public static void main(String[] args) {
        Node root = build();
        root.right = null;
    }
}
root.right = null;

此时a开始遍历,无法访问到c了,c和f就被标记为垃圾

引用

引用本质就是低配指针,为了找到对象,引用不仅能找对象,还能决定对象的生死

        强引用:平时用到的引用,既能找到对象,也能决定对象的生死

        软引用:既能找到对象,也能一定程度决定对象生死(保对象一时)

        弱引用:只能找到对象,不能决定对象生死

        虚引用:不能找到对象,也不能决定对象生死

3,垃圾回收算法

通过上面的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想)。

标记清除算法

浅谈JVM_第10张图片

1. 效率问题 : 标记和清除这两个过程的效率都不高

2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中 需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

标记复制算法

浅谈JVM_第11张图片

 优点:能够解决内存碎片问题,保存内存回收后,并不存在碎片

缺点:需要额外一块内存空间,如果生存对象较多,效率就比较低

标记整理算法

浅谈JVM_第12张图片

优点:不再需要复制一样依赖更大的空间,也没有碎片

缺点:搬运效率相对较低,不适合频繁操作

4,对象的一生

浅谈JVM_第13张图片

1,对象诞生于新生代伊甸区,新对象的内存就是新生代中的内存

2,第一轮GC扫描伊甸区之后,就会把大量的对象干掉

3,进入生存区的对象,也会进行GC扫描,如果发现该对象已经不可达,也就要被销毁,被销毁的对象,通过复制算法,拷贝到另外的生存区

4,对象在生存区经经历了若干伦次的拷贝之后,也没被回收,此时说明,这个对象存活时间比较久,就拷贝到老年代

5,老年代的对象也是要经历GC扫描,但是由于老年代的对象,存活时间比较长,所以扫描老年代的周期比新生代周期长。

Particial GC:只进行一部分内存区域的GC

Full GC:针对整个内存区域进行GC

Minor GC:针对新生代内存的GC,执行频繁,速度较快

Major GC:针对老年代的GC,执行没有那么频繁,速度较慢,通常由Major发起

5,垃圾回收器

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。

浅谈JVM_第14张图片

评价垃圾回收器的好坏的标准

        回收的空间效率:扫一遍地,能扫除多少垃圾

        回收速度:扫一遍要花多少时间

        垃圾回收和应用线程之间能否并发执行,扫地的时候会不会影响到别人干活

        垃圾回收是否是多线程

        回收时间是否是可以预测的

Serial收集器

新生代收集器,串行GC

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一 条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线 程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。

复制算法,单线程进行标记+回收

ParNew收集器

新生代收集器,并行GC

Serial收集器的多线程版本

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Serial Old收集器

老年代收集器,串行GC

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器

老年代收集器,并发GC

初始标记

只是把和GCRoot直接相关的对象先标记出来,这个标记过程时间比较短

并发标记

执行整个标记遍历过程,不需要暂停用户线程,消耗时间相对较久,但是可以和用户线程并发

当进行并发标记的时候,由于用户线程也在执行,可能导致某个对象,刚刚标记成不是垃圾之后,被相应代码一改就变成垃圾

重新标记

修正刚才的误差,由于刚才出现的误差的毕竟是少数,重新标记代价太大,虽然STW了,用不了多长时间就结束了

并发清除

多线程的方式把刚才的垃圾对象都释放掉了,也可以和应用多线程并发执行

优点: CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

缺点:内存碎片;GC操作和应用线程是并发执行的,对CPU比较敏感

6,深拷贝与浅拷贝

深拷贝:针对对象A拷贝之后得到对象B,对A的修改不会对B造成任何影响,这种叫深拷贝

浅拷贝:针对对象A拷贝之后得到对象B,对A 的修改会影响到B,这种叫浅拷贝

private static B copy3(B b) {
        B result = new B();
        result.count = b.count;
        result.a = new A();
        result.a.num = b.a.num;
        return result;
    }

    private static B copy2(B b) {
        B result = new B();
        result.count = b.count;
        result.a = b.a;
        return result;
    }

    private static B copy1(B b) {
        return b;
    }
 public static void main(String[] args) {
        B b = new B();
        b.count = 10;
        b.a = new A();
        b.a.num = 100;

       // B b1 = copy1(b);
        B b2 = copy2(b);
        //B b2 = copy3(b);
        System.out.println(b2.count);
        System.out.println(b2.a.num);
        System.out.println("修改后的内容");
        b.count = 20;
        b.a.num = 200;
        System.out.println(b2.count);
        System.out.println(b2.a.num);

    }

浅谈JVM_第15张图片

public static void main(String[] args) {
        B b = new B();
        b.count = 10;
        b.a = new A();
        b.a.num = 100;

       //B b1 = copy1(b);
        //B b2 = copy2(b);
        B b2 = copy3(b);
        System.out.println(b2.count);
        System.out.println(b2.a.num);
        System.out.println("修改后的内容");
        b.count = 20;
        b.a.num = 200;
        System.out.println(b2.count);
        System.out.println(b2.a.num);

    }

浅谈JVM_第16张图片

浅谈JVM_第17张图片

浅谈JVM_第18张图片

六、类加载器

1,类加载过程

浅谈JVM_第19张图片

1,加载:根据类名,找到文件,读取文件,解析架构,把内容放到内存中,并构造出对应的类对象

2,链接:如果该类依赖一些其他类,链接过程就是把依赖内容进行引入

3,初始化:初始化静态成员,并执行静态代码块

2,类加载触发条件

构造该类的实例

调用该类的静态属性或方法

使用子类时会触发父类加载

3,双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父 类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载,类加载中,根据类名找类的.class文件查找的过程

以加载String类为例,三个加载器直接存在“父子关系”(和继承无关),类加载器中有一个parent属性,执行自己的父亲

浅谈JVM_第20张图片

 1,先从AppClassLoder开始

2,AppClassLoader不会立刻查找,而是先把类名交给它的父亲,先让父亲查找

3,ExtClassLoader也不会立刻进行查找,而是把类名交给它的父亲去寻找

4,BootstrapClassLoader拿到类名之后,只能自己查找,如果自己查找到了,直接加载类即可,如果未找到,再把这个类交还给ExtClassLoader来查找

5,ExtClassLoader再根据类名在目录中查找,如果找到就加载,未找到就还给AppClassLoader

6,AppClassLoader再去CLASS_PATH环境变量,-cp指定目录,当前目录去查找,如果找到了就加载,如果没找到,就抛出ClassNotFoundException

目的就是让标准库的类优先加载

4,能否违背双亲委派模型

可以违背,只是标准库中的三个类加载器要遵守,其他的类加载器不太需要遵守

你可能感兴趣的:(JavaWeb,java,开发语言,后端)