当前位置:主页 > 查看内容

聊聊JVM基础,快进来复习复习吧

发布时间:2021-07-24 00:00| 位朋友查看

简介:大家好,今天总结了一下老生常谈的 JVM,这也是面试必问的知识。 话不多说,整起来! 一、JVM 是什么? 1、Java 虚拟机(Jvm)是可运行 Java 代码的假想计算机。 2、Jvm 充当着一个翻译官的角色,我们平常所编写出的 Java 程序,是不能够被操作系统所直接识别的……

大家好,今天总结了一下老生常谈的 JVM,这也是面试必问的知识。

话不多说,整起来!

一、JVM 是什么?

1、Java 虚拟机(Jvm)是可运行 Java 代码的假想计算机。

2、Jvm 充当着一个翻译官的角色,我们平常所编写出的 Java 程序,是不能够被操作系统所直接识别的,这时候 JVM 的作用就体现出来了,它负责把我们的程序翻译给系统“听”,告诉它我们的程序需要做什么操作。

3、Jvm 针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的 Jvm,那么这份 Java 编译后的代码就能够运行起来,有句话大家一定听说过:「Java 能一次编译到处运行」,这就是原因所在。

二、Jvm 的体系架构?

Jvm 是这四部分组成:

  • 运行区数据
  • 类加载器
  • 执行引擎
  • 垃圾回收

下面就聊聊这四个部分~~

2.1 运行区数据

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域各有各的作用,各有各的生命周期。

有些区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束建立和销毁。

运行区数据的划分:方法区、虚拟机栈,本地方法栈、堆、程序计数器


上面这张图大家一定都见过,其实可以划分的更细点,看下面的这两张图:



能看出 1.8 版本前后的差别么,下面就看看这些区域都干啥的~~

程序计数器

  • 其实你可以把它看作是当前线程执行的字节码的行号指示器,在 Jvm 工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成。它就好像是一个路口的红绿灯一样。

特点:1、占用很小的内存 2、各线程私有

就比如下面字节码一样,每一行开头的黄色数字,我们就可以认为它是程序计数器所存储的内容:

  1. public void doSth1(); 
  2.    descriptor: ()V 
  3.    flags: ACC_PUBLIC 
  4.    Code: 
  5.      stack=2, locals=3, args_size=1 
  6.         0: ldc           #5 
  7.         2: dup 
  8.         3: astore_1 
  9.         4: monitorenter 
  10.         5: getstatic     #2 
  11.         8: ldc           #3 
  12.        10: invokevirtual #4 
  13.        13: aload_1 

虚拟机栈

  • 虚拟机栈,其描述的就是线程内存模型,也可以称作线程栈,也是每个线程私有的,生命周期与线程保持一致。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。

特点:1、随线程而生、随线程而死 2、先进后出

栈示意图:


本地方法栈

  • 本地方法栈,和虚拟栈其实很相似的,我们知道,java 底层用了很多 c 的代码去实现,而其调用 c 端的方法上都会有 native 来代表本地方法,而本地方法栈就是为其服务的。

特点:1、各线程私有 2、和本地方法有关

native 修饰的方法:

  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 

  1. 堆可以说是 jvm 中最大的一块内存区域了,它是所有线程共享,几乎所有的对象实例都会在这里分配。
  2. java 堆是垃圾回收器主要回收的区域。从内存回收的角度来说,堆空间可以分为新生代和老年代,而新生代又可以分为伊甸区,Survivor 区。

特点:1、所有线程共享 2、占用大的内存空间 3、先进先出

堆的划分:


方法区

  1. 方法区,也是各个线程共享的内存区域,它是用来被存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
  2. 从上面的图,可以看到 1.8 之前和之后,方法区所在的位置是有差别的。在 Java 8 之前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,这部分由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果大量地调用 String.intern 方法 (将字段串放入永久代中的常量区)或 动态生成类(将类信息放入永久代),很容易造成 OOM。
  3. 所以,在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,这个区域也就不会进行 GC,也因此提升了性能,正因为放到了本地内存,也就不存在由于永久代限制大小而导致的 OOM 异常了。
  4. 另外,运行时常量池也是方法区的一部分,用来存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入该常量池中。

特点:1、所有线程共享 2、1.8 之后移到了元空间 3、涉及到常量池

直接内存

从上面的图中,看到有直接内存这个区域

  • 直接内存,并不是虚拟机运行时数据区的一部分,其实可以理解为堆外内存,在一些场景下,比如:NIO 类引入了一种基于通道(Channel)与缓冲区(Buffer)的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储 Java 堆中的对象作为这块内存的引用,这样能够显著提高性能,因为避免了 Java 堆和 Native 堆中来回复制数据。

2.2 类加载器

1、什么是类加载机制?

JVM 运行时,java 虚拟机会把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被 jvm 可以直接使用的类型,这就是类加载机制。

2、说说类加载的过程?

开局一张图:


这张图说明了类从加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期。

一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化,具体行为在 Java 虚拟机规范里有非常详细的定义。

1、首先是加载阶段

  1. 它是 Java 将字节码(jar 包)数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),
  2. 重点:加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
  3. 通过字节流将类的.class 文件中的二进制数据读入到内存。然后在堆中创建 java.lang.class 对象,用来封装类在方法区的数据结构
  4. 只会创建一个 Class 对象,该 Class 对象来描述有哪些构造方法,都有哪些成员变量

2、第二阶段是链接,这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:

① 验证

  1. 这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,
  2. 验证阶段有可能触发更多 class 的加载。

② 准备

  1. 创建类或接口中的静态变量,并初始化静态变量的初始值。
  2. 但这里的“初始化”和下面的显式初始化阶段是有区别的,
  3. 测重点在于分配需要的内存空间,不会去执行更进一步的 JVM 指令

这里的初始化是指:

1、8 种基本数据类型的默认初始值是 0。

2、引用类型默认的初始值是 null。

3、对于有 static final 修饰的常量会直接赋值,例如:static final int x=123;则 x 直接会初始化为 123。

③ 解析

  1. 在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
  2. 符号引用就是唯一的字符串,直接引用可以理解为一个地址值和偏移量

3、最后是初始化阶段

这一步真正去执行类初始化的代码逻辑,包括静态字段动作,以及执行类定定义中的静态初始化块内的逻辑编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

初始化顺序:

  • 先是父类静态域(静态成员变量)或者静态代码库块
  • 然后是子类静态域或者子类静态代码块
  • 所以最先初始化的总是 java.lang.Object 类

三、什么时候会对类进行初始化?

通过 new 关键字实例化对象、读取或设置类的静态变量、调用类的静态方法

通过反射发生上面的三种行为

初始化子类时,会触发父类的初始化

作为程序入口运行,就是指的 main 方法

四、类加载器有哪些?

启动类加载器:负责加载环境变量下 jre/lib 下面的 jar 文件

扩展类加载器:负责加载环境变量下 jre/lib/ext 目录下面的 jar 包

应用类加载器:就是加载我们熟悉的 classpath 的内容

自定义加载器:继承 ClassLoader 就可以实现

五、了解双亲委派模型吗?


这是一张很经典的图,通常情况下,各个类加载器的协作关系就是这样的。

概念:就是说一个类加载器收到了类加载的请求,不会自己先加载,而是把它交给自己的父类去加载,层层迭代。

用上图来说明就是如果应用程序类加载器收到了一个类加载的请求,会先给扩展类加载器,然后再给启动类加载器,如果启动类加载器无法完成这个类加载的请求,再返回给扩展类加载器,如果扩展类加载器也无法完成,最后才会到应用类加载器。

好处:1、避免重复加载 Java 类型 2、沙箱安全机制:保证核心的类不会被篡改。

六、classLoader与class.forName区别

  • class.forName()除了将类的.class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块,当然你可以指定是否执行静态块。
  • classLoader 只干一件事情,就是将.class 文件加载到 jvm 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。

七、脑图

最后送大家一张自己总结的脑图呀


今天就写到这里啦!!

给大家介绍了JVM、运行区数据、类加载机制。希望大家面试前能掌握和Jvm有关的知识。


本文转载自网络,原文链接:https://mp.weixin.qq.com/s/JC86s2UmIGrFp6yQ5P-T8A
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐