JVM

JVM快速入门

从面试开始

1.JVM是什么? JVM的内存区域分为哪些?

2.什么是OOM? 什么是StackoverflowError? 有哪些方法分析?

3.JVM 的常用参数调优你知道哪些?

4.GC是什么? 为什么需要GC?

5.什么是类加载器?

什么是JVM

JVM:Java Virtual Machine,Java虚拟机

**位置:**JVM是运行在操作 系统之上的,它与硬件没有直接的交互。

为什么要在程序和操作系统中间添加一个JVM?

Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要 JVM 进行一番转换。有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux、Windows、MacOS 等平台上了。而 Java 跨平台的意义在于一次编译,处处运行,能够做到这一点 JVM 功不可没。

主流虚拟机有哪些?

  • JCP组织(Java Community Process 开放的国际组织 ):Hotspot虚拟机(Open JDK版),sun2006年开源
  • Oracle:Hotspot虚拟机(Oracle JDK版),闭源,允许个人使用,商用收费
  • BEA:JRockit虚拟机
  • IBM:J9虚拟机
  • 阿里巴巴:Dragonwell JDK(龙井虚拟机),电商物流金融等领域,高性能要求。

结构图

JVM的作用:加载并执行Java字节码文件(.class) - 加载字节码文件、分配内存(运行时数据区)、运行程序

JVM的特点:一次编译到处运行、自动内存管理、自动垃圾回收

1

  • 类加载器子系统:将字节码文件(.class)加载到内存中的方法区

  • 运行时数据区:

    • 方法区:存储已被虚拟机加载的类的元数据信息(元空间)。也就是存储字节码信息。
    • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
    • 虚拟机栈(java栈):虚拟机栈描述的是Java方法执行的内存模型**。每个方法被执行的时候都会创建一个**栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 本地方法栈:本地方法栈则是记录虚拟机当前使用到的native方法
    • 程序计数器:当前线程所执行的字节码的行号指示器
  • 本地方法接口:虚拟机使用到的native类型的方法,负责调用操作系统类库。(例如Thread类中有很多Native方法的调用)

  • 执行引擎:包含解释器、即时编译器和垃圾收集器 ,负责执行加载到JVM中的字节码指令。

注意:

  • 多线程共享方法区和堆;
  • Java栈、本地方法栈、程序计数器是每个线程私有的。

执行引擎Execution Engine

Execution Engine执行引擎负责解释命令(将字节码指令解释编译为机器码指令),提交操作系统执行。

JVM执行引擎通常由两个主要组成部分构成:解释器和即时编译器(Just-In-Time Compiler,JIT Compiler)

  1. 解释器:当Java字节码被加载到内存中时,解释器逐条解析和执行字节码指令。解释器逐条执行字节码,将每条指令转换为对应平台上的本地机器指令。由于解释器逐条解析执行,因此执行速度相对较慢。但解释器具有优点,即可立即执行字节码,无需等待编译过程。
  2. 即时编译器(JIT Compiler):为了提高执行速度,JVM还使用即时编译器。即时编译器将字节码动态地编译为本地机器码,以便直接在底层硬件上执行。即时编译器根据运行时的性能数据和优化技术,对经常执行的热点代码进行优化,从而提高程序的性能。即时编译器可以将经过优化的代码缓存起来,以便下次再次执行时直接使用。

JVM执行引擎还包括其他一些重要的组件,如即时编译器后端、垃圾回收器、线程管理器等。这些组件共同协作,使得Java程序能够在不同的操作系统和硬件平台上运行,并且具备良好的性能。

本地方法接口Native Interface

本地接口的作用是融合不同的编程语言为 Java 所用,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

例如Thread类中有一些标记为native的方法:

Native Method Stack

本地方法栈存储了从Java代码中调用本地方法时所需的信息。是线程私有的。

PC寄存器(程序计数器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即 将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

类加载器ClassLoader

  • 负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe)。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 加载的类信息存放到方法区的内存空间。

类的加载过程

类加载过程主要分为三个步骤:加载链接初始化,而其中链接过程又分为三个步骤:验证准备解析,加上使用、卸载两个步骤统称为为类的生命周期。

阶段一:加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流代表的静态存储结构转为方法区运行时数据结构
  • 在内存中生成一个代码这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

结论:类加载为懒加载

阶段二:链接

  • 验证:验证阶段主要是为了为了确保Class文件的字节流中包含的信息符合虚拟机要求,并且不会危害虚拟机
  • 准备:
    • 为类的静态变量分配内存并且设置该类变量的默认初始值,即赋初值【赋的默认值】
    • 实例变量是在创建对象的时候完成赋值,且实例变量随着对象一起分配到Java堆中
    • final修饰的常量在编译的时候会分配,准备阶段直接完成赋值,即没有赋初值这一步。被所有线程所有对象共享
  • 解析:将符号引用替换为直接引用
    • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
    • 直接引用:可以直接指向目标的指针,而直接引用必须引用的目标已经在内存中存在

阶段三:初始化

​ 初始化阶段是执行类构造器 的过程。这一步主要的目的是:根据程序员程序编码制定的主观计划去初始化类变量和其他资源。

类加载器的作用

负责加载class文件,class文件在文件开头有的文件标识**(CA FE BA BE)**,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

类加载器分类

分为四种,前三种为虚拟机自带的加载器。

  • 启动类加载器(BootstrapClassLoader):由C++实现。

  • 扩展类加载器(ExtClassLoader/PlatformClassLoader):由Java实现,派生自ClassLoader类。

  • 应用程序类加载器(AppClassLoader):也叫系统类加载器。由Java实现,派生自ClassLoader类。

  • 自定义加载器 :程序员可以定制类的加载方式,派生自ClassLoader类。

Java 9之前的ClassLoader

  • Bootstrap ClassLoader加载$JAVA_HOME中【jre/lib/rt.jar】,加载JDK中的核心类库
  • ExtClassLoader加载相对次要、但又通用的类,主要包括$JAVA_HOME中【jre/lib/ext/*.jar】或-Djava.ext.dirs指定目录下的jar包
  • AppClassLoader加载-cp指定的类,加载用户类路径中指定的jar包及目录中class

Java 9及之后的ClassLoader

  • Bootstrap ClassLoader,使用了模块化设计,加载【lib/modules】启动时的基础模块类,java.base、java.management、java.xml
  • ExtClassLoader更名为PlatformClassLoader,使用了模块化设计,加载【lib/modules】中平台相关模块,如java.scripting、java.compiler。
  • AppClassLoader加载-cp,-mp指定的类,加载用户类路径中指定的jar包及目录中class

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器PlatformClassLoader去完成。
  • 2、当PlatformClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader加载失败,会用PlatformClassLoader来尝试加载;
  • 4、若PlatformClassLoader也加载失败,则会使用AppClassLoader来加载
  • 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。

目的:

一,性能,避免重复加载;

二,安全性,避免核心类被修改。

方法区Method Area

方法区存储什么

方法区是被所有线程共享。《深入理解Java虚拟机》书中对方法区存储内容的经典描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等:

方法区演进细节

Hotspot中方法区的变化:

方法区(永久代(JDK7及以前)、元空间(JDK8以后))

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

虚拟机栈stack

Stack 栈是什么?

  • 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,每个线程都有自己的栈,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,是线程私有的
  • 线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

栈运行原理

  • JVM对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”或者“后进先出”原则。
  • 一个线程中只能有一个正在执行的方法(当前方法),因此对应只会有一个活动的当前栈帧

当一个方法1(main方法)被调用时就产生了一个栈帧1 并被压入到栈中,栈帧1位于栈底位置

方法1又调用了方法2,于是产生栈帧2 也被压入栈,

方法2又调用了方法3,于是产生栈帧3 也被压入栈,

……

执行完毕后,先弹出栈帧4,再弹出栈帧3,再弹出栈帧2,再弹出栈帧1,线程结束,栈释放。

栈存储什么?

局部变量表(Local Variables)

也叫本地变量表。

作用:存储方法参数和方法体内的局部变量:8种基本类型变量、对象引用(reference)。

可以用如下方式查看字节码中一个方法内定义的的局部变量,当程序运行时,这些局部变量会被加载到局部变量表中。

查看局部变量:

可以使用javap - .class* 命令,或者idea中的jclasslib插件。

注意:以下方式看到的是加载到方法区中的字节码中的局部变量表,当程序运行时,局部变量表会被动态的加载到栈帧中的局部变量表中

操作数栈(Operand Stack)

**作用:**也是一个栈,在方法执行过程中根据字节码指令记录当前操作的数据,将它们入栈或出栈。用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。

动态链接(Dynamic Linking)

**作用:**可以知道当前帧执行的是哪个方法。**指向运行时常量池中方法的符号引用。**程序真正执行时,类加载到内存中后,符号引用会换成直接引用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class DynamicLinkingDemo {

    public void methodA(){
        methodB(); //方法A引用方法B
    }

    public void methodB(){

    }
}

方法返回地址(Return Address)

**作用:**可以知道调用完当前方法后,上一层方法接着做什么,即“return”到什么位置去。存储当前方法调用完毕

完整的内存结构图如下

栈溢出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/**
 * 未设置栈大小默认:11416次
 * 设置VM参数:-Xss256k 2475次
 */
public class StackOOMDemo {

    public static int count = 1;

    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}

常见问题栈溢出:Exception in thread “main” java.lang.StackOverflowError通常出现在递归调用时。

问题辨析:

  • 垃圾回收是否涉及栈内存?

    不涉及,因为栈内存在方法调用结束后都会自动弹出栈。

  • 方法内的局部变量是线程安全的吗?

    当方法内局部变量没有逃离方法的作用范围时线程安全,因为一个线程对应一个栈,每调用一个方法就会新产生一个栈桢,都是线程私有的局部变量,当变量是static时则不安全,因为是线程共享的。

设置栈的大小

// 使用配置,设置栈为1MB,下面可以3选一 -Xss1m
-Xss1024k -Xss1048576 完整的写法是: -XX:ThreadStackSize=1m

堆heap

堆体系概述

堆、栈、方法区的关系

HotSpot是使用指针的方式来访问对象:

  • Java堆中会存放指向类元数据的地址

  • Java栈中的reference存储的是指向堆中的对象的地址

堆空间概述

  • 一个Java程序运行起来对应一个进程,一个进程对应一个JVM实例,一个JVM实例中有一个运行时数据区。
  • 堆是Java内存管理的核心区域,在JVM启动的时候被创建,堆内存的大小是可以调节的。

分代空间

堆空间划分

堆内存逻辑上分为三部分:

  • Young Generation Space 新生代/年轻代 Young/New
  • Tenured generation space 养老代/老年代 Old/Tenured
  • Permanent Space/Meta Space 永久代/元空间 Permanent/Meta

新生代又划分为:

  • 新生代又分为两部分: 伊甸园区(Eden space)和幸存者区(Survivor pace) 。

  • 幸存者区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。

JDK1.7及之前堆空间

JDK1.8及之后堆空间

**注意:**方法区(具体的实现是永久代和元空间)逻辑上是堆空间的一部分,但是虚拟机的实现中将方法区和堆分开了,如下图:

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计