JVM概述
为什么学习JVM?
-
提高程序性能:了解JVM的内存管理、垃圾回收、类加载等机制,可以帮助我们编写出更高效的Java程序,避免内存泄漏、内存溢出等问题。
-
调优与故障排查:学习JVM可以帮助我们更好地进行程序调优和故障排查。例如,通过分析堆栈信息,可以定位到程序中的性能瓶颈和内存泄漏等问题。
-
跨平台性:Java程序之所以能够实现“一次编写,到处运行”,很大程度上归功于JVM。了解JVM的跨平台原理,可以帮助我们更好地理解Java程序在不同平台上的运行情况。
JVM作用:
Java 虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,每一条Java指令,Java虚拟机中都有详细定义,如怎么取操作数, 怎么处理操作数,处理结果放在哪儿。它可以将Java程序翻译成操作系统能够理解的形式,从而让操作系统知道如何执行Java程序的各种操作......
人话:将字节码(.class文件)装载到虚拟机内,将字节码编译、解释为机器码
JVM的构成:
类加载系统
负责从硬盘上加载字节码文件到JVM中
运行时数据区
按照不同的数据分区进行储存(方法区、堆、栈、本地方法栈、程序计数器)
执行引擎
将字节码再次编译、解释为机器码
本地库接口
负责调用本地库接口
JVM结构--类加载子系统
类加载子系统概述
类加载子系统负责从文件系统(硬盘)或者网络中加载.class文件。classLoader只负责.class 文件的加载,至于他是否可以运行,则由Execution Engine决定。
人话:类加载子系统就是负责将.class文件从本地加载到JVM中的。
类加载过程
1.加载
以二进制字节流的方式加载字节码,在内存中生成一个class对象,将静态存储(硬盘)传出为运行时存储(内存)。
2.链接
验证:
验证字节码格式是否正确(如:语法是否正确、是否符合Java规范)
准备:
为类的静态(static)属性分配内存,并设置默认初始值。
不包含final修饰的static常量,该部分在编译时就已经初始化了
解析:
将静态文件中的指令符号引用替换成内存中的直接引用
.class文件中的符号指令 --(转换)--> 方法区中地址引用
3.初始化
对类变量(静态变量(static修饰的))进行赋值。
类什么时候会被初始化?
- 使用类中的静态变量、静态方法
- 在一个类中运行main
- 创建对象
- 使用反射加载一个类
- 当初始化一个类的子类时(优先加载父类)
注意:当使用某个类中的静态常量时,类不会进行初始化,因为该常量在编译时期就已经初始化了
当类在加载阶段初始化完成,才说明类的整个加载过程结束
这里我们用 static int a = 10;举个例子:
加载(将该Java文件转为class文件) ---->
链接(){
验证(看是否为Java代码、代码格式有没有问题...) ---->
准备:static int a = 0; ---->
解析:将符号指令转换为地址引用
} ---->
初始化:a = 10;(整个加载过程结束)
我们看到的:static int a = 10;
类加载器(继承ClassLoader类)
真正实施类加载的具体的东西
类加载器的分类
宏观(站在JVM的角度)
引导类加载器(启动类加载器)
其他类加载器,这些类加载器由Java语言实现,独立存在于虚拟机外部,并全部继承于java.long.ClassLoader.
细分(站在java开发人员的角度)
引导类加载器(启动类加载器)
java中系统提供的类,都是由启动类加载器加载的
扩展类加载器
jre/lib/ext子目录下的加载类库
应用程序类加载器(系统类加载器)
加载我们自己定义的类,加载用户路径上的所有类
自定义类加载器
我们自己写的一个类,继承ClassLoader
e.g:tomcat中就有自己定义的类加载器
如果想查看一个类的类加载器,可以调用getClassLoader()
e.g:String.class.getClassLoader()//这里返回null,表示该类是用启动类加载器加载的
双亲委派机制
如果我自己写一个String类,那么程序加载期间,他是应该调用系统中的String类呢还是我自己写的String类呢?下面让我们一起看一下类加载子系统中的双亲委派机制。
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
- 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
- 如果均加载失败,就会抛出 ClassNotFoundException 异常。
人话:当加载一个类时,系统会先让上一级类加载器去加载,直到找到启动类加载器,如果上一级找到了类,就用上级类加载器去加载,如果找不到,则主机向下委托,使用自己类加载的类,如果都找不到,则报异常
看完这个相信大家已经可以理解什么是双亲委派机制了吧,这时候就会有反应快的朋友就会提出一个问题:既然都是要向上委派,那为什么不直接从启动类加载器直接往下找,这样岂不是更省事吗?
这里也是JVM中的一个小优化:从下往上委派的时候,会在每一层判断之前有没有加载过这个类,如果有的话,就不用再次向上向下委派,直接返回就行。
如何打破双亲委派机制
1.通过创建自定义的类加载器,并在其中重写
loadClass
方法,可以实现自定义的类加载逻辑。在这个方法中,可以改变类的加载顺序或者直接加载特定版本的类,从而绕过双亲委派机制。2.利用反射机制:Java的反射机制允许程序在运行时动态地加载和调用类。通过反射可以直接加载指定类加载器中的类,而不遵循双亲委派机制。
JVM结构--运行时数据区
运行时数据区组成概述
JVM的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
程序计数器
程序计数器是用来存储吓一跳指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。
- 它是一款很小的内存空间,也是运算速度最快的存储空间。
- 在JVM规范中,每个线程都有他自己的程序计数器,是线程私有的,声明周期与线程的生命周期保持一致。
- 程序计数器会存储当前线程正在执行的java方法的JVM地址
- 是java虚拟机中唯一没有OOM(OutOfMemoryError)情况的区域。(即:不会出现内存溢出)
虚拟机栈
- 每个线程创建时都会创建一个虚拟机栈。(即线程私有)
- 运行时单位,主管java程序的运行,负责将命令转化为栈帧。
- 调用方法-->入栈 运行完成-->出栈
- 一个方法入栈后,可以看做一个栈帧。(栈帧对应方法)
- 没有GC(垃圾回收),但是有内存溢出的可能。
栈帧的构成:
局部变量表:存储方法中定义的变量、参数
操作数栈:计算过程完成的地方
方法返回地址:方法执行完成后,返回之前调用它的地方(无所谓有没有返回值)
本地方法栈
- java虚拟机栈管理java方法的调用,本地方法栈用于管理本地方法的调用
- 线程私有,会出现内存溢出
- 管理本地方法的地方
本地方法:
java中用native关键字修饰的方法叫本地方法,没有方法体
java中常用的本地方法:
hashcode();
getclass();
clone();
notify();
wait();
堆
- 所有的对象实例都应当在运行时分配在堆上。
- 堆也是 Java内存管理的核心区域,是 JVM管理的最大一块内存空间。
- 所有的线程共享 Java堆。
- 堆是 GC(垃圾收集器)执行垃圾回收的重点区域。
- 堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
堆内存区域划分
新生代:
伊甸园区(Eden):刚创建的对象放在伊甸园区。
幸存者1(Survivor1):存放在伊甸园区和另一个幸存者经过垃圾回收后的存活下来的对象。
幸存者2(Survivor2):同上,两个幸存者区交替使用,始终有一个区域空闲。
老年代:
存放周期长、内存大的对象
- 经过15次垃圾回收后依然存活的对象,从新生代转移到老年代
其中区域比例:
老年代: 新生代 = 2 : 1
伊甸园:幸存者1:幸存者2 = 8 : 1 : 1
为什么要分区??
根据不同的对象存活时间进行划分,生命周期较长的放在老年代,用不同的算法进行扫描,减少垃圾回收频率和扫描次数,提高垃圾回收效率,提高内存分配效率。
对象创建过程以及在内存中的分布:
- 新生对象 --> 伊甸园区
- 垃圾回收 --> 幸存者1
- 垃圾回收 --> 幸存者2
- ......
- ......
- 直到15次回收后,如果该对象还活着,则移动到老年代,GC扫描、回收频率减慢
堆空间的参数设置
JVM调优,就是根据程序实际运行的需要进行参数设置,调整各个区间的比例大小。
懒得写了,粘一下,有需要的可以去官网查看
官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html-XX:+PrintFlagsInitial
查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails输出详细的 GC处理日志
垃圾回收中的名词
Minor GC:针对新生代垃圾回收。 频繁回收新生代
Major GC:针对老年区的垃圾回收。 较少回收老年代
Full GC:整堆收集(尽量避免)。
什么时候触发整堆收集?
1.老年区放满的时候
2.方法区放满的时候
方法区(Non-Heap)
方法区主要存储加载到虚拟机的类信息。
方法区的大小可以调整。
方法区是线程共享的,会出现内存溢出。
方法区一旦占满,便会触发 Full GC。
方法区的垃圾回收
方法区是存在垃圾回收的,不过条件比较苛刻
方法区主要存储类信息
类什么时候会被卸载:{
1.该类的所有对象及子类对象都不存在
2.加载该类的类加载器不存在
3.该类的class对象没有被其他地方引用
}
方法区、栈、堆的关系
本地方法接口
什么是本地方法???
被native关键字修饰的方法,本地方法不是java语言实现的,是由操作系统实现的
一个本地方法是一个java调用非java代码的接口
本地方法 <---> 接口
关键字native可以与其他所有的java标识符连用,but关键字 abstract 除外
java中为什么会用到本地方法?
因为上层语言(高级语言)没有堆底层硬件直接操作的权限,需要调用操作系统的提供的接口进行访问。
执行引擎
负责将装载到虚拟机中的字节码(.class文件)解释/编译为机器码
.java------->javac------->.class 前端编译
.class----->机器码 后端编译
什么是解释器??什么是JIT编译器???
解释器是一种能够将源代码逐行转换为机器语言并执行的程序。
解释器的主要作用是将java语言转化为机器语言,并且是逐行转换和执行的。这意味着解释器在执行程序时不会一次性将整个程序转换成机器码,而是读取、解析然后立即执行每一行代码。
JIT编译器是在程序运行时将代码编译成机器语言以提高执行效率的编译器。
JIT编译器(Just In Time Compiler)则是Java虚拟机中的一个组件,它的目的是通过将热点代码编译成机器语言来提高程序的性能。JIT编译器在程序运行过程中实时工作,仅在需要时才编译代码,从而减少编译所需的时间并将输入的字节码优化为高效的机器指令。
优缺点:
解释器: 优:节省编译时间 缺点:效率低
编译器: 优:执行效率高 缺点:编译花费时间
为什么java是半编译半解释语言?
Java的执行引擎采用半解释半编译的方式将字节码转化为机器码,刚开始的时候采用逐行解释执行,程序运行过程中会将热点代码编译并缓存起来,两者结合,提高效率。