JVM基础

1、简介

大体上,虚拟机可以分为系统虚拟机和程序虚拟机。大名鼎鼎的VMware就属于系统虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型的代表就是java虚拟机了,它专门为执行某个单个计算机程序而设计。在java虚拟机中执行的指令我们称为java字节码指令。

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条java指令,java虚拟机中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪儿。

Java虚拟机特点:1、一次编译到处运行 2、自动内存管理 3、自动垃圾回收功能。一次编译到处运行动内存管理动垃圾回收功能

1.1 JVM的位置

详图:

加载—->链接—>初始化 , 其中,链接又分为 验证—>准备—>解析。

2.2.1 加载

  • 通过类名(地址)获取此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.2.2 链接

  • 验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
  • 准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
    不包含用final修饰的static实例变量,在编译时进行初始化
    不会为实例变量初始化
  • 解析:将类的二进制数据中的符 引用替换成直接引用(符 引用是用一组符 描述所引用的目标;直接引用是指向目标的指针).

2.2.3 初始化

类什么时候初始化/p>

  • 创建类的实例,也就是new一个对象。
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“”))
  • 初始化一个类的子类(会首先初始化子类的父类)

类的初始化顺序:

对static修饰的变量或语句块进行赋值.

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

顺序是:父类static–>子类static–>父类构造方法–>子类构造方法

2.3 类加载器分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader).

从概念上来讲,自定义类加载器一般指的是由开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器.

Java中类加载器可以分为三种:

2.3.4.1 工作原理:

1、如果一个类加载器收到了类加载器请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行。

2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。

3、如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。如果均加载失败,就会抛出ClassNotFoundException异常。

2.3.4.2 双亲委派优点

安全,避免用户自己编写的类替换Java的核心类(沙箱安全机制的体现)。

避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)

2.3.4.3 沙箱安全机制

沙箱安全机制是基于双亲委派机制的,作用就是防止恶意代码污染java源码,比如我们自己也定义了一个包名为java.lang,在其中也定义了一个String,但是因为沙箱安全机制,当委派到顶层加载器找到了这个各类,那么就先使用,后面的String就一概不能使用,这就保证了源码不会被恶意代码污染。

2.5 面试题

在jvm中如何判断两个对象是属于同一个类/p>

1.类的全类名(地址)完全一致

2.类的加载器必须相同

2.6 类的主动使用/被动使用(理解记忆)

JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就有被动使用.

2.6.1 主动使用

  • 通过new关键字
  • 访问类的静态变量,包括读取和更新
  • 访问类的静态方法
  • 对某个类进行反射操作,会导致类的初始化
  • 初始化子类会导致父类的初始化
  • 执行该类的main()

2.6.2 被动使用

除了上面的几种主动使用其余就是被动使用了。

注意: 引用类的静态常量,不会导致类的初始化,这里的常量指得是给定字面量的常量

构造某个类的数组时,也是不会导致该类的初始化。

主动使用和被动使用的区别在于类是否会被初始化。

3、JVM运行时数据区

3.1 概述组成部分(背)

JVM的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

  • 程序计数器
  • 本地方法栈
  • 虚拟机栈
  • 方法区

3.2 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行(hang) 指示器,是对物理PC寄存器的一种抽象模拟。

3.2.1 作用

程序计数器用来记录存储下一条指令的地址,也即将要执行的指令代码,由执行引擎去读取下一条指令。

  • 它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。
  • 程序计数器是线程私有的,生命周期与线程保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。
  • 程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,如果是在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

3.2.2 面试题

1、使用程序计数器存储字节码指令地址有什么用什么使用程序计数器记录当前线程的执行地址呢/p>

JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。并且CPU需要不同的切换各个线程,下次回来再执行线程时,就需要通过程序计数器中保存的值知道从哪继续。

2、程序计数器为什么被设定为线程私有的/p>

因为每个线程再执行方法时进度不一样,如果设置为线程共享,那么就会有资源竞争问题,到底方法无法正常执行。为了精准的记录各个线程正在执行的当前的字节码指令地址,所以没一个线程都配有一个程序计数器,从而互不干扰。

3.3 虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks),描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

栈解决程序运行问题,而堆解决数据存储问题。

3.3.1 栈的作用

  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用。

  • Java虚拟机栈是线程私有的,生命周期和线程一致。

  • Java虚拟机栈主管Java程序的运行,它保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

3.3.2 栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器

JVM直接对Java虚拟机栈的操作只有两个,调用方法:进栈。方法执行结束后:出栈。

对于栈来说,不存在垃圾回收问题。

  • 不同线程所包含的栈帧(方法)是不允许互相引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。虚拟机栈是线程独有的。

  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

  • Java方法有两种返回的方式,一种是正常的返回,使用return,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出。

    3.5.6 栈的内部结构

    3.5.7 面试题

    1、什么情况下会出现栈溢出(StackOverflowError)/p>

    当方法执行时创建的栈帧超过了栈的深度,就是栈溢出。最有可能导致栈溢出的情况就是方法递归。

    2、通过调整栈大小,就能保证不出现溢出吗/p>

    不能

    3、分配的栈内存越大越好吗/p>

    并不是的,只能延缓这种现象的出现,可能会影响其他内存空间。

    4、垃圾回收机制是否会涉及到虚拟机栈/p>

    不会

    3.4 本地方法栈

    本地方法栈(Native Method Stack),与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的。

    • Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法(native)的调用。
    • 本地方法栈也是线程私有的。
    • 允许被实现成固定或者是可动态扩展的内存大小.内存溢出方面也是相同的。
    • 本地方法是用C语言写的
    • 它的具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库.

    3.5 堆

    3.5.1 堆内存概述

    Java堆(Java Heap),是Java虚拟机中内存最大的一块,被所有线程共享,在虚拟机启动时候创建,Java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么“绝对”了。

    • 一个JVM实例只存在一个堆内存

    • Java堆区在JVM启动时的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间。

    • 堆内存的大小是可以调节。

      例如: -Xms:10m(堆起始大小)-Xmx:30m(堆最大内存大小)

      一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。

    • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。

    • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时分配在堆上。

    • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区。

    • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

    • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域

    3.5.2 堆内存区域划分

    Java8以后堆内存分为:新生区(新生代) + 老年区(老年代)

    而新生区又分为 Eden(伊甸园)区和Survivor(幸存者)区

    这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

    如上图,可能遇到超大对象直接分到老年代,如果老年代剩下的空间不够则就行majorGC,如果majorGC后还是放不下,就会 错OOM

    另外,幸存者区由Eden区满了出发GC,自己不能主动出触发GC。

    3.5.5 新生区与老年区配置比例

    配置新生代与老年代在堆结构的占比(一般不会调)

    • 默认 -XX:NewRatio =2,表示新生代占1,老年代占2,新生代占整个堆的1/3。可以修改**-XX:NewRatio**=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

    3.5.9 堆空间参数

    官 地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

    -XX:+PrintFlagsInitial 查看所有参数的默认初始值

    -XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)

    -Xms:初始堆空间内存(默认为物理内存的 1/64)

    -Xmx:最大堆空间内存(默认为物理内存的 1/4)

    -Xmn:设置新生代的大小(初始值及最大值)

    -XX:NewRatio:配置新生代与老年代在堆结构的占比

    -XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

    -XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

    -XX:+PrintGCDetails 输出详细的 GC 处理日志

    3.5.10 字符串常量池

    参考博文https://www.cnblogs.com/cosmos-wong/p/12925299.html

    JDK1.6及以前,常量池在方法区,这时的方法区也叫做永久代;

    JDK1.7的时候,方法区合并到了堆内存中,这时的常量池也可以说是在堆内存中;

    JDK1.8及以后,方法区又从堆内存中剥离出来了,但实现方式与之前的永久代不同,这时的方法区被叫做元空间,常量池就存储在元空间。

    使用元空间代替永久代来实现方法区,但是方法区并没有改变。所谓”Your father will always be your father”,变动的只是方法区中内容的物理存放位置。正如上面所说,类型信息(元数据信息)等其他信息被移动到了元空间中;但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。

    JDK1.8中字符串常量池和运行时常量池逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两则存在于方法区中,这就是造成误解的地方。

    3.6 方法区(非堆)

    3.6.1 简介

    方法区(Method Area),其 中 主 要 存 储 加 载 的 类 字 节 码class/method/field 等元数据static final 常量static 变量即时编译器编译后的代码等数据。存的是一些轻易不改变的东西。另外,方法区包含了一个特殊的区域“运行时常量池”。方法区在 JVM 启动时被创建,并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的.

    方法区、堆、栈的交互关系如下图所示

    4.1 什么是本地方法

    简单来讲,native method就是java调用非java代码的接口,一个native method是这样一个java方法:该方法的底层实现由非Java语言实现,比如 C。该方法支持由本地的操作系统函数调用。关键字 native 可以与其他所有的java 标识符连用,但是 abstract 除外。

    4.2 为什么要使用本地方法

    参考博客 : https://blog.csdn.net/lansine2005/article/details/5753741

    • 与 java 环境外交互

      声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2021年6月18日
    下一篇 2021年6月18日

    相关推荐