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的特点:一次编译到处运行、自动内存管理、自动垃圾回收
-
类加载器子系统:将字节码文件(.class)加载到内存中的方法区
-
运行时数据区:
- 方法区:存储已被虚拟机加载的类的元数据信息(元空间)。也就是存储字节码信息。
- 堆:存放对象实例,几乎所有的对象实例都在这里分配内存。
- 虚拟机栈(java栈):虚拟机栈描述的是Java方法执行的内存模型**。每个方法被执行的时候都会创建一个**栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 本地方法栈:本地方法栈则是记录虚拟机当前使用到的native方法。
- 程序计数器:当前线程所执行的字节码的行号指示器。
-
本地方法接口:虚拟机使用到的native类型的方法,负责调用操作系统类库。(例如Thread类中有很多Native方法的调用)
-
执行引擎:包含解释器、即时编译器和垃圾收集器 ,负责执行加载到JVM中的字节码指令。
注意:
- 多线程共享方法区和堆;
- Java栈、本地方法栈、程序计数器是每个线程私有的。
执行引擎Execution Engine
Execution Engine执行引擎负责解释命令(将字节码指令解释编译为机器码指令),提交操作系统执行。
JVM执行引擎通常由两个主要组成部分构成:解释器和即时编译器(Just-In-Time Compiler,JIT Compiler)。
- 解释器:当Java字节码被加载到内存中时,解释器逐条解析和执行字节码指令。解释器逐条执行字节码,将每条指令转换为对应平台上的本地机器指令。由于解释器逐条解析执行,因此执行速度相对较慢。但解释器具有优点,即可立即执行字节码,无需等待编译过程。
- 即时编译器(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)
**作用:**可以知道当前帧执行的是哪个方法。**指向运行时常量池中方法的符号引用。**程序真正执行时,类加载到内存中后,符号引用会换成直接引用。
|
|
方法返回地址(Return Address)
**作用:**可以知道调用完当前方法后,上一层方法接着做什么,即“return”到什么位置去。存储当前方法调用完毕
完整的内存结构图如下
栈溢出
|
|
常见问题栈溢出: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及之后堆空间
**注意:**方法区(具体的实现是永久代和元空间)逻辑上是堆空间的一部分,但是虚拟机的实现中将方法区和堆分开了,如下图: