Java虚拟机JVM知识及面试考点(01)

简介

Java虚拟机(Java Virtual Machine,JVM)是Java平台的核心组成部分之一,它是一个在计算机上运行Java字节码的虚拟机。JVM充当了Java应用程序和底层操作系统之间的中间层,提供了跨平台的特性,使得Java程序可以在不同的操作系统和硬件上运行。

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

这第一篇文章我们描述JVM的主要结构及面试常考点,第二篇主要描述JVM的垃圾回收相关。

JVM结构

结构简介

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

类加载器ClassLoader

类加载器负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

类加载器的种类

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

  • 启动类加载器(Bootstrap)负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
  • 扩展类加载器(Extension)负责加载java平台中扩展功能的一些jar包,包括、$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包,由Java实现的。
  • 应用程序类加载器(AppClassLoader)Java 也叫系统类加载器,负责加载classpath中指定的jar包及目录中class
  • 用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

类加载器工作过程

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 4若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  • 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

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

好处:防止内存中出现多份同样的字节码(安全性角度),可以保证jdk的稳定性和安全性。

执行引擎Execution Engine

Execution Engine执行引擎负责解释命令,提交操作系统执行(把字节码解析成具体指令并提交操作系统执行)。

本地方法接口

调用系统类库的一些接口

运行时数据区

方法区(线程共享的)

  • 线程共享的
  • 保存方法信息、常量、接口信息等
  • jdk1.7之前:使用永久代实现了方法区,常量池就在永久代
  • jdk1.7 逐步去永久代,常量池在堆中
  • jdk1.8 没有永久代,使用元空间实现了方法区,常量池就在元空间

堆(线程共享的)

Heap 堆:一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存逻辑上分为三部分:

  • Young Generation Space 新生区 Young/New
  • Tenure generation space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

也称为:新生代(年轻代)、老年代、永久代(持久代)。 其中JVM堆分为新生代和老年代

新生区/代(年轻代)

新生区是对象的诞生、成长、消亡的区域,一个对象在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的对象都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC ),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再次垃圾回收,满足条件后再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC) ,进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常"OutOfMemoryError"。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

总结

eden(新创建对象) from(幸存对象) to(总是空的),会发送minorGC/YoungGC。

  1. 项目启动时:eden from to都是空的。
  2. 项目在处理请求的过程中,会反复创建对象,这些对象会保存在eden。
  3. eden如果满了,再去创建对象,此时就会引发minorGC,会把幸存对象保存在from,再去清空eden。
  4. 接下来又会向eden中反复创建对象,eden满了,如果还要创建对象,就会引发minorGC(eden 和 from)把幸存对象保存到to中清空eden和from,最后from和to会交换位置。
  5. 反复执行第4步。
  6. 经过15次GC(默认)依然存活的对象会移到养老区。

养老区(老年代)

  • 整个堆中最大的一块区域,存放两类对象:15GC依然存活的对象或者大对象。
  • FullGC、MajorGC:每次majarGC会伴随着一次minorGC。两次MajarGC之后依然无法保存新创建的对象,就会触发OOM:java heap space原因:堆内存太小(-Xms -Xmx) 在程序中创建了大量的大对象。
  • 老年代的对象比较稳定,不会频繁的GC

永久区(永久代,Non-Heap非堆)

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

  • 就是方法区,存放方法 类 接口 常量信息,可能发生OOM:permgen space OOM原因:
  1. 引入了大量的第三方jar包,或者项目本身太大
  2. 永久代设置的太小了
  3. 一个tomcat放入了多个项目
  4. 程序中大量使用了反射
  • JDK更新变化
  1. Jdk1.6及之前: 有永久代,常量池1.6在方法区
  2. Jdk1.7: 有永久代,但已经逐步"去永久代",常量池1.7在堆
  3. Jdk1.8及之后: 无永久代,常量池1.8在元空间(Metaspace)

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

对于HotSpot虚拟机,很多开发者习惯将方法区称之为"永久代(Parmanent Gen)" ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。

常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。

jvm栈(线程私有的)

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建 ,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题 ,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法 都是在函数的栈内存中分配。(第二篇讲JVM垃圾回收机制) 栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。

栈帧中主要保存3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量(本地变量,8种基本数据的变量和对象的引用地址)。
  • 栈操作(Operand Stack):记录出栈、入栈的操作(出栈或者入栈信息)。
  • 栈帧数据(Frame Data):包括类文件、方法等等(方法的定义信息)。

遵循"先进后出"或者"后进先出"原则。

不会产生OOM,产生StackOverflowError,在递归调用时,可能产生内存溢出

PC寄存器(线程私有的)

  • 本质就是一个指针,指向该线程将要执行的方法区中的下一行指令。大小几乎可以忽略不计
  • 指向当前线程将要执行的方法区中的代码指令

本地方法栈

保存native类型方法的一块区域

JVM参数调优

常用JVM参数

  • -Xmn 新生代大小
  • -Xms 初始堆大小
  • -Xmx 最大扩展的堆大小
  • -XX:+PrintGCDetails 输出堆及GC的具体信息

调优工具

  • eclipse中的MAT插件
  • idea或者命令行使用jdk自带工具 jdk/bin/jvisualvm.exe

举例idea分析dump文件

  1. 修改程序启动参数为:-Xmx10m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\test
    -XX:HeapDumpPath:生成dump文件路径。(一定要存在)
  2. 编写测试程序

3. 会生成hprof文件

  1. jdk自带了该类型文件的解读工具:jvisualvm.exe(在jdk的bin目录下)
  1. 双击打开

6. 导入生成的hprof文件(注意后缀)

调优指令

  • jps -l查看正在运行的java进程
  • jinfo -flags 进程号 查看当前java进程的jvm参数
  • jstat -gc 进程号 查看GC统计信息
相关推荐
神奇小汤圆22 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生31 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling32 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅34 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079736 分钟前
Spring Flux方法总结
后端
define952739 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路2 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway