JVM相关概念和重点问题

Posted 纯电版的豆腐车

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 大部分组成:

 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就要根据需要,把整个空间,分成几个部分,每个部分各自有不同的功能作用。

具体划分:

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

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

2.JVM类加载机制

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

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

 一个类,啥时候会被加载呢?
不是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主要针对堆进行释放的;

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的轮次)
年龄越大,这个对象存在的时间就越久;
堆,划分成一系列区域:

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

JVM的相关概念

 

本文首先介绍一下Java虚拟机的生存周期,然后大致介绍JVM的体系结构,最后对体系结构中的各个部分进行详细介绍。 (  首先这里澄清两个概念:JVM实例和JVM执行引擎实例,JVM实例对应了一个独立运行的java程序,而JVM执行引擎实例则对应了属于用户运行程序的线程;也就是JVM实例是进程级别,而执行引擎是线程级别的。)

 

              一、 JVM的生命周期 JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点,既然如此,那么JVM如何知道是运行class A的main而不是运行class B的main呢?这就需要显式的告诉JVM类名,也就是我们平时运行java程序命令的由来,如java classA hello world,这里java是告诉os运行Sun java 2 SDK的java虚拟机,而classA则指出了运行JVM所需要的类名。 JVM实例的运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。 JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

 

            二、JVM的体系结构  粗略分来,JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎。  下面将先介绍类装载器,然后是执行引擎,最后是运行时数据区

 

              1,类装载器,顾名思义,就是用来装载.class文件的。JVM的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。(下面所述情况是针对Sun JDK1.2) 动类装载器:只在系统类(java API的类文件)的安装路径查找要装入的类       用户自定义类装载器:  系统类装载器:在JVM启动时创建,用来在CLASSPATH目录下查找要装入的类 其他用户自定义类装载器:这里有必要先说一下ClassLoader类的几个方法,了解它们对于了解自定义类装载器如何装载.class文件至关重要。 protected final Class defineClass(String name, byte data[], int offset, int length)  ;protected final Class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain); protected final Class findSystemClass(String name) ;protected final void resolveClass(Class c) defineClass用来将二进制class文件(新类型)导入到方法区,也就是这里指的类是用户自定义的类(也就是负责装载类)       findSystemClass通过类型的全限定名,先通过系统类装载器或者启动类装载器来装载,并返回Class对象。 ResolveClass: 让类装载器进行连接动作(包括验证,分配内存初始化,将类型中的符号引用解析为直接引用),这里涉及到java命名空间的问题,JVM保证被一个类装载器装载的类所引用的所有类都被这个类装载器装载,同一个类装载器装载的类之间可以相互访问,但是不同类装载器装载的类看不见对方,从而实现了有效的屏蔽。

 

               2, 执行引擎:它或者在执行字节码,或者执行本地方法    要说执行引擎,就不得不的指令集,每一条指令包含一个单字节的操作码,后面跟0个或者多个操作数。(一)指令集以栈为设计中心,而非以寄存器为中心 这种指令集设计如何满足Java体系的要求: 平台无关性:以栈为中心使得在只有很少register的机器上实现java更便利 compiler一般采用stack向连接优化器传递编译的中间结果,若指令集以stack为基础,则有利于运行时进行的优化工作与执行即时编译或者自适应优化的执行引擎结合,通俗的说就是使编译和运行用的数据结构统一,更有利于优化的开展。 网络移动性:class文件的紧凑性。 安全性:指令集中绝大部分操作码都指明了操作的类型。(在装载的时候使用数据流分析期进行一次性验证,而非在执行每条指令的时候进行验证,有利于提高执行速度)。 (二)执行技术 主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行 其中解释属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式 自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。

 

 

    3,运行时数据区:主要包括:方法区,堆,java栈,PC寄存器,本地方法栈

    (1)方法区和堆由所有线程共享 堆:存放所有程序在运行时创建的对象 方法区:当JVM的类装载器加载.class文件,并进行解析,把解析的类型信息放入方法区。

    (2)Java栈和PC寄存器由线程独享,在新线程创建时间里

    (3)本地方法栈: 存储本地方法调用的状态 上边总体介绍了运行时数据区的主要内容,下边进行详细介绍,要介绍数据区,就不得不说明JVM中的数据类型。 JVM中的数据类型:JVM中基本的数据单元是word,而word的长度由JVM具体的实现者来决定 数据类型包括基本类型和引用类型,

        (1) 基本类型包括:数值类型(包括除boolean外的所有的java基本数据类型),boolean(在JVM中使用int来表示,0表示false,其他int值均表示true)和returnAddress(JVM的内部类型,用来实现finally子句)。

        (2)引用类型包括:数组类型,类类型,接口类型 前边讲述了JVM中数据的表示,

  下面让我们输入到JVM的数据区 首先来看方法区: 上边已经提到,方法区主要用来存储JVM从class文件中提取的类型信息,那么类型信息是如何存储的呢?众所周知,Java使用的是大端序(big—endian:即低字节的数据存储在高位内存上,如对于1234,12是高位数据,34为低位数据,则java中的存储格式应该为12存在内存的低地址,34存在内存的高地址,x86中的存储格式与之相反)来存储数据,这实际上是在class文件中数据的存储格式,但是当数据倒入到方法区中时,JVM可以以任何方式来存储它。

  类型信息:包括class的全限定名,class的直接父类,类类型还是接口类型,类的修饰符(public,等),所有直接父接口的列表,Class对象提供了访问这些信息的窗口(可通过Class.forName(“”)或instance.getClass()获得),下面是Class的方法,相信大家看了会恍然大悟,(原来如此J) getName(), getSuperClass(), isInterface(), getInterfaces(), getClassLoader(); static变量作为类型信息的一部分保存 指向ClassLoader类的引用:在动态连接时装载该类中引用的其他类 指向Class类的引用:必然的,上边已述 该类型的常量池:包括直接常量(String,integer和float point常量)以及对其他类型、字段和方法的符号引用(注意:这里的常量池并不是普通意义上的存储常量的地方,这些符号引用可能是我们在编程中所接触到的变量),由于这些符号引用,使得常量池成为java程序动态连接中至关重要的部分 字段信息:普通意义上的类型中声明的字段 方法信息:类型中各个方法的信息 编译期常量:指用final声明或者用编译时已知的值初始化的类变量     class将所有的常量复制至其常量池或者其字节码流中。 方法表:一个数组,包括所有它的实例可能调用的实例方法的直接引用(包括从父类中继承来的) 除此之外,若某个类不是抽象和本地的,还要保存方法的字节码,操作数栈和该方法的栈帧,异常表。 举例: class Lava{   private int speed = 5;   void flow(){} } class Volcano{   public static void main(String[] args){ Lava lava = new Lava(); lava.flow(); } } 运行命令java Volcano;

    (1)JVM找到Volcano.class倒入,并提取相应的类型信息到方法区。通过执行方法区中的字节码,JVM执行main()方法,(执行时会一直保存指向Vocano类的常量池的指针)

    (2)Main()中第一条指令告诉JVM需为列在常量池第一项的类分配内存(此处再次说明了常量池并非只存储常量信息),然后JVM找到常量池的第一项,发现是对Lava类的符号引用,则检查方法区,看Lava类是否装载,结果是还未装载,则查找“Lava.class”,将类型信息写入方法区,并将方法区Lava类信息的指针来替换Volcano原常量池中的符号引用,即用直接引用来替换符号引用。

    (3)JVM看到new关键字,准备为Lava分配内存,根据Volcano的常量池的第一项找到Lava在方法区的位置,并分析需要多少对空间,确定后,在堆上分配空间,并将speed变量初始为0,并将lava对象的引用压到栈中

    (4)调用lava的flow()方法 好了,大致了解了方法区的内容后,让我们来看看堆 java对象的堆实现: java对象主要由实例变量(包括自己所属的类和其父类声明的)以及指向方法区中类数据的指针,指向方法表的指针,对象锁(非必需), 等待集合(非必需),GC相关的数据(非必需)(主要视GC算法而定,如对于标记并清除算法,需要标记对象是否被引用,以及是否已调用finalize()方法)。

  那么为什么java对象中要有指向类数据的指针呢?我们从几个方面来考虑 首先:当程序中将一个对象引用转为另一个类型时,如何检查转换是否允许?需用到类数据 其次:动态绑定时,并不是需要引用类型,而是需要运行时类型, 这里的迷惑是:为什么类数据中保存的是实际类型,而非引用类型?这个问题先留下来,我想在后续的读书笔记中应该能明白 指向方法表的指针:这里和C++的VTBL是类似的,有利于提高方法调用的效率 对象锁:用来实现多个线程对共享数据的互斥访问 等待集合:用来让多个线程为完成共同目标而协调功过。(注意Object类中的wait(),notify(),notifyAll()方法)。 Java数组的堆实现:数组也拥有一个和他们的类相关联的Class实例,具有相同dimension和type的数组是同一个类的实例。数组类名的表示:如[[Ljava/lang/Object 表示Object[][],[I表示int[],[[[B表示byte[][][] 至此,堆已大致介绍完毕,下面来介绍程序计数器和java栈 程序计数器:为每个线程独有,在线程启动时创建,   若thread执行java方法,则PC保存下一条执行指令的地址。   若thread执行native方法,则Pc的值为undefined Java栈:java栈以帧为单位保存线程的运行状态,java栈只有两种操作,帧的压栈和出栈。 每个帧代表一个方法,java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。 帧的组成:局部变量区(包括方法参数和局部变量,对于instance方法,还要首先保存this类型,其中方法参数按照声明顺序严格放置,局部变量可以任意放置),操作数栈,帧数据区(用来帮助支持常量池的解析,正常方法返回和异常处理)。 本地方法栈:依赖于本地方法的实现,如某个JVM实现的本地方法借口使用C连接模型,则本地方法栈就是C栈,可以说某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。

 

以上是关于JVM相关概念和重点问题的主要内容,如果未能解决你的问题,请参考以下文章

java:JVM及相关概念

JVM内存模型的相关概念

关于java重点知识点

Java内存模型

你必须掌握的 21 个 JAVA 核心技术!

JVM相关的几个基本概念