JVM相关概念和重点问题

目录

1.JVM 简介

2. JVM 运行流程

3. JVM 运行时数据区

4.JVM内存区域的划分

2.JVM类加载机制

4.JVM垃圾回收机制GC


1.JVM 简介

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

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

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

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

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

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

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

2. JVM 运行流程

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键;

JVM执行流程:

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

 JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
1. 类加载器(ClassLoader)
2. 运行时数据区(Runtime Data Area)
3. 执行引擎(Execution Engine)
4. 本地库接口(Native Interface)

3. JVM 运行时数据区

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 大部分组成:

JVM相关概念和重点问题_第1张图片

 3.1 堆(线程共享)

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

3.2 Java虚拟机栈(线程私有)

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

1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变 量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变 量。
2. 操作栈:每个方法会生成一个先进后出的操作栈。
3. 动态链接:指向运行时常量池的方法引用。
4. 方法返回地址:PC 寄存器的地址。

3.3 本地方法栈(线程私有)

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

3.4 程序计数器(线程私有)

程序计数器的作用:用来记录当前线程执行的行号的。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器值为空。
程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域。

3.5方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 的。
在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域 叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。

JDK 1.8 元空间的变化
1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内
存的参数影响了,而是与本地内存的大小有关。
2. JDK 8 中将字符串常量池移动到了堆中。

运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

3.6 内存布局中的异常问题

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来 GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。

4.JVM内存区域的划分

JVM也就是启动的时候,会申请到一整个很大的内存区域;
JVM是一个应用程序,要从操作系统这里申请内存(相当于租了个写字楼);
JVM就要根据需要,把整个空间,分成几个部分,每个部分各自有不同的功能作用。

具体划分:

JVM相关概念和重点问题_第2张图片

 整个栈空间内部,可以认为是包含很多个元素(每个元素表示一个方法)
把这里每个元素,称为一个“栈帧”。

问题:某个变量在哪个区域?
原则:
1.局部变量在 栈上;
2.普通成员变量在 堆上;
3.静态成员变量在 方法区/元数据区;

2.JVM类加载机制

准备的来说,类加载就是.class文件,从文件(硬盘)被加载到内存中(元数据区)这样的过程;
java通过javac;
类加载的过程:加载、验证、准备、解析、初始化。

加载:把.class文件找到(找的过程),打开文件,读文件,把文件内容读到内存中;最终加载完成,是要得到类对象的;
验证:检查下.class文件的格式对不对;.class文件是一个二进制文件,这里的格式是有严格说明的;
官方提供了JVM虚拟机规范,规范文档上详细描述了.class的格式;
准备:给类对象分配内存空间(此时内存初始化成全0)=》静态成员,也就是设为0值了;
解析:针对字符串常量进行初始化,把符合引用转为直接引用;
字符串常量,得有一块内存空间,存这个字符的实际内容还得有一个引用,来保存这个内存空间的起始地址;
在类加载之前,字符串常量,此时是处在.class文件中的.
此时这个“引用”记录的并非是字符串常量的真正的地址.而是它在文件中的"偏移量"这个东西.(或者是个占位符)类加载之后,才真正把这个字符串常量给放到内存中.
此时才有"内存地址”,这个引用才能被真正赋值成指定内存地址.
初始化:真正针对类对象里面的内容进行初始化,加载父类,执行静态代码块的代码;

JVM相关概念和重点问题_第3张图片

 一个类,啥时候会被加载呢?
不是Java程序一运行,就把所有的类都加载了,而是真正用到才加载(懒汉模式)。
1.构造类的实例;
2.调用这个类的静态方法、使用静态属性;
3.加载子类,就会先加载其父类;
一旦加载过后,后续就不必再重复加载了;

3.双亲委派模型(重点)
加载:把。class文件找到,读取文件内容;双亲委派模型,描述的是这个加载,找.class文件,基本过程;
JVM默认提供了三个类加载器:存在"父子关系",不是父类子类,相当于每个class loader有一个parent属性,指向自己的父类加载器;
BootstrapClassLoader 负责加载标准库中的类;(Java规范,要求提供哪些类,无论是哪种JVM的实现,都会提供这些一样的类)
ExtensionClassLoader 负责加载JVM扩展库中的类;(规范之外,由实现jvm的厂商/组织,提供额外的功能)
ApplicationClassLoader 负责加载用户提供的第三方库/用户项目代码中的类;

上述类加载器如何配合工作?

首先加载一个类的时候,是先从ApplicationClassLoader 开始;

但是ApplicationClassLoader 会把加载任务,交给父亲,让父亲去进行;

于是ExtensionClassLoader要去加载了.....但是也不是真加载,而是再委托给自己的父亲BootstrapClassLoader 要去加载了.也是想委托给自己的父亲.结果发现,自己的父亲是null.没有父亲/父亲加载完了,没找着类,才由自己进行加载;

ExtensionClassLoader真正搜索扩展库相关的目录,如果找到就加载,如果没找到,就由子类加载器进行加载;

ApplicationClassLoader 加载器进行加载.(由于当前没有子类了,就只能抛出类找不到这样的异常);

为啥要有上述顺序?
上述这套顺序其实是出自于JVM实现代码的逻辑;
这段代码大概是类似于“递归”的方式写的;
这样就能保证,即使出现上述问题,也不会让jvm已有代码混乱,最多是用户自己写的类不生效罢了;
再另一方面,类加载器,其实是可以用户自定义的,上述三个类加载器是jvm自带的;用户自定义的类加载器,也可以加入到上述流程中,就可以和现有的加载配合使用了;

4.JVM垃圾回收机制GC

1.概念

啥是垃圾?指的就是不再使用的内存;

垃圾回收,就是把不用的内存,帮我们自动释放了;

C语言有malloc; c++有new,需要手动释放内存,如果不释放,这块内存的空间,就会持续存在,一直存在到进程结束;

2.GC的优缺点

GC的好处:非常省心,让程序员写代码简单点,不容易出错;
GC坏处:需要消耗额外的系统资源,也有额外的性能开销;
GC还有一个比较关键的问题,STW问题,stop the world;
如果有时候,内存中的垃圾已经很多了,此时触发一次GC操作开销可能非常大,大到可能就把系统资源吃了很多;
另一方面GC回收垃圾的时候可能会涉及到一些锁操作,导致业务代码无法正常执行这样的卡顿,极端情况下,可能是出现几十毫秒甚至上百毫秒;

3.JVM中的内存区域:
1.堆 2.栈 3.程序计数器 4.元数据区
GC主要针对堆进行释放的;

JVM相关概念和重点问题_第4张图片

4.GC实际工作过程:
1.找到垃圾/判断垃圾,哪个对象是垃圾,哪个对象以后一定不用了?哪个对象后面还可能使用;
2.再进行对象的释放;

5.(普适性的)垃圾回收流程:
1.找到垃圾/判断垃圾
关键思路,抓住这个对象,看看它到底有没有“引用”指向它;Java中,使用对象,只有这一条路,通过引用来使用。
Java中,使用对象,只有这一条路,通过引用来使用!!如果一个对象,有引用指向它,就可能被使用到.如果一个对象,没有引用指向了,就不会再被使用了.
6.Java具体如何知道对象是否有引用指向?
两种典型的实现:
1)引用计数(不是java的做法,python、php)
给每个对象都分配了一个计数器(整数),每次创建一个引用指向该对象;
每次创建一个引用指向该对象,计数器就+1,每次引用被销毁了,计数器就-1;
Java未使用的原因:
1.内存空间浪费的多(利用率低)
每个对象都要分配一个计数器,如果按4个字节算的,代码中的对象非常少,无所谓,如果对象特别多了,占用的额外空间就会很多,尤其是每个对象都比较小的;

2.存在循环引用的问题

2)可达性分析(java的做法)
Java中的对象,都是通过引用来指向并访问的;
经常,是一个引用指向一个对象,这个对象里的成员,又指向别的对象;

可达性分析,就是把所有这些对象被组织的结构视为树,就从树根节点出发,遍历树,所有能被访问到的对象,标记成“可达”,(不能访问到的,就是不可达);JVM自己捏着一个所有对象的名单,通过上述遍历,把可达的标记出来,剩下的不可达的就可以作为垃圾进行回收了;

7.如何清理垃圾:

1.标记清除

简单粗暴,内存碎片问题,被释放的空闲空间,是零散的,不是连续的;
申请内存要求的是连续空间,总的空闲空间可能很大,但是每一个具体的空间都很小,可能导致申请大一点的内存的时候就失败了;

2.复制算法

解决了内存碎片问题:
这个地方,直接把整个内存分成两半,用一半丢一半;

缺点:1.空间利用率低;

2.如果要是垃圾少,有效对象多,复制成本大。

3.标记整理
解决复制算法的缺点:类似于顺序表删除中间元素,会有元素搬运的操作;

基于基本策略,搞了个复合策略:分代回收

4.分代回收

java的对象要么就是生命周期特别短,要么就是特别长~根据生命周期的长短,分别使用不同的算法.
给对象引入一个概念,年龄.(单位不是年,而是熬过GC的轮次)
年龄越大,这个对象存在的时间就越久;
堆,划分成一系列区域:

JVM相关概念和重点问题_第5张图片

伊甸区=>幸存区,复制算法;
幸存区之后,也要周期性的接受GC的考验;
如果变成垃圾,就要被释放.如果不是垃圾,拷贝到另外一个幸存区~~(这俩幸存区同一时刻只用一个),在两者之间来回拷贝―(复制算法);
由于幸存区体积不大,此处的空间浪费也能接受;
如果这个对象已经再两个幸存区中来回拷贝很多次了,这个时候就要进入老年代了;
老年代都是年纪大的对象.生命周期普遍更长;
针对老年代,也要周期性GC扫描,但是频率更低了;
如果老年代的对象是垃圾了,使用标记整理的方式进行释放。

你可能感兴趣的:(javaEE,jvm,java,开发语言)