文章目录
- JVM--java Virtual Machine
- JVM当时学习的存在位置
- JVM概述(什么是JVM)
- 为什么学习JVM?
- 虚拟机
- JVM作用
- JVM组成部分
- 类加器
- 作用
- 类加载过程
- 类什么时候会被加载(初始化)
- 类加载器
- 双亲委派机制
- 打破双亲委派机制
- 运行时数据区
- 1.程序计数器
- 2.本地方法栈
- 3.Java虚拟机栈
- 4.堆
- 方法区
- 本地方法接口
- 执行引擎
- 垃圾回收(GC)
- 垃圾回收概述
- 垃圾回收相关算法
- **引用计数算法**(在现代的JVM中并没有被使用)
- **可达性分析算法/根搜索算法**
- **对象的 finalization 机制**
- 垃圾回收阶段的算法
- 垃圾回收器
JVM–java Virtual Machine
JVM当时学习的存在位置
JDK,JRE,JVM
JDK:JRE+java开发工具
JRE(java运行环境):JVM+java核心类库
java开发工具:编译工具,打包工具
JVM-------- \ | /
JVM概述(什么是JVM)
为什么学习JVM?
就个人而言,学习JVM是为了面试的需要
学习JVM是我能更深入的理解Java这门语言,理解Java语言底层的执行过程,为后期写出优质代码做好准备。
很多时候一个问题需要深入到字节码层次去分析才能得到准确的结论,而字节码就是JVM的一部分。并且以后项目上线去排查一些程序log日志中无法呈现的问题,如:内存溢出,GC太频繁导致高延迟问题(这块可以由自己后期引导)
虚拟机
虚拟机就是一个虚拟的计算机,相当于一款软件,用来执行一系列虚拟的计算机指令,虚拟机分为 系统虚拟机(VMware)和程序虚拟机(JVM)。
JVM就是Java虚拟机,是一种执行Java字节码文件的虚拟计算机(最终解释为机械执行),拥有独立的运行机制。Java虚拟机是Java技术的核心,所有的Java程序都运行在Java虚拟机的内部。
JVM作用
总的来说,JVM是运行Java字节码的虚拟机,运行并管理Java源码文件所生成的Class文件
Java虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机械码指令执行。
负责将字节码加载到内存中(运行时数据区),负责存储数据,把字节码翻译成机械码,执行,垃圾回收
特点
一次编译到处运行,自动内存管理,自动垃圾回收功能
现在的 JVM 不仅可以执行 java 字节码文件,还可以执行其他语言编译后的字节码文件,是一 个跨语言平台.
JVM组成部分
1.类加器(负责加载字节码文件)
2.运行时数据区(管理并分配内存)(存储运行时数据,堆,java虚拟机栈(运行java自己的方法),方法区,程序计数器,本地方法栈)
3.执行引擎(更底层,把字节码翻译成机械码)
4.本地方法接口
5.垃圾回收
- 程序在执行前先要把Java代码转换成字节码(class文件)文件,JVM需要把字节码通过一定方式的类加载器(ClassLoader)把文件加载到内存的运行时数据区(Runtime Data Area),而字节码文件是JVM的一套指令集规范,并不能直接由底层操作系统区执行,因此需要特定的命令解析器**执行引擎(Execution Engine)将字节码翻译成底层系统指令再交给CPU去执行,这个过程中需要调用其他语言的接口本地库接口(Native Interface)**来实现整个程序的功能。
- 通常需要程序员调试分析的区域就是”运行时数据区“,具体一点就是”运行时数据区“里面的Heap(堆)模块。
类加器
作用
字节码刚开始会在硬盘之中,需要类加载器加载到内存中。
负责从硬盘/网络中加载字节码信息到内存中(运行时数据区的方法区中)
类加载过程
加载
使用IO读取字节码文件,转换并存储,为每个类创建一个Class类的对象并存储在方法区中
链接(验证,准备,解析)
验证:检查被加载的类内部结构是否正确,对字节码文件的格式进行验证,判断文件是否被污染并对基本的语法格式进行验证。如果都验证通过,就会进入准备阶段。
准备:为静态的变量进行内存分配,并设置默认初始值(在初始化阶段才真正赋值自己定义的);不包含用final修饰的static常量,在编译时进行初始化。
public static int value = 123;value 在准备阶段后的初始值是 0,而不是 123
静态常量在编译期间就初始化
解析:将符号引用转为直接引用(就是将一个类的名字转为真正的地址)
将字节码中的表现形式转为内存中的表现形式(内存地址)
初始化
类的初始化,为类中的定义的静态变量进行赋值
public static int value = 123;value 在初始化阶段后值是 123.
类什么时候会被加载(初始化)
JVM规定,每个类或者接口被首次主动使用时才对其进行初始化
1.在类中运行main方法
2.创建对象
3.使用类中的静态变量,静态方法
4.反射 Class.forName(“类的地址”);
5.子类被加载
有两种情况类不会被初始化:
- static final int b = 20; 编译期间赋值的静态常量
- System.out.println(User.b);
- User[] users = new User[10]; 作为数组类型
类加载器
三个系统自带的类加载器
引导类加载器,自定义类加载器(继承实现ClassLoader类)
具体的负责加载类的一些代码
引导类加载器, 用C/C++语言开发的, JVM底层的开发语言,负责加载java核心类库。与java语言无关的。
自定义类加载器
只要继承了ClassLoader类都属于自定义类加载器
- 扩展类加载器
java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现,继承ClassLoader类.
从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库
-
应用程序类加载器
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现. 派生于 ClassLoader 类.
加载程序中自己开发的类
- tomcat中自己定义类加载器继承ClassLoader类
双亲委派机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,二十把这个请求委托给父类的加载器去执行,如果父加载器还存在其父加载器,则继续向上委托,最终将到达顶层的启动类加载器,如果父类加载器可以完成类的加载任务,就成功返回,若无法完成加载任务,子加载器才会尝试自己去加载。如果都加载失败,则抛出异常ClassNotFoundException。
目的:为了先确保加载系统类
优点:安全,可以避免用户自己编写的类替换Java的核心类库,并避免类重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
双亲委派机制,是java提供的类加载的规范,但不是强制不能改变的.
我们可以通过自定义的类加载器,改变加载方式.
打破双亲委派机制
可以通过继承ClassLoader类,重写loadClass/findClass方法,实现自定义的类加载
典型的tomcat中,加载部署在tomcat中的项目时,就使用的是自己的类加载器
运行时数据区
Java虚拟机在执行Java程序时,会把它管理的内存划分为若干不同的数据区域。这区域各有各的用途以及生命周期。
- 有的区域随着虚拟机的启动而一直存在。(线程共享)
- 有的区域则依赖用户线程的启动和结束分别创建和销毁。(线程私有)
1.程序计数器
在执行指令的时候,线程会来回切换,当切回来的时候需要直到执行下一条指令的地址,程序计数器就是用来记录当前需要执行的下一条指令的地址。
- 程序计数器是一块很小的内存空间,用来记录每个线程运行的指令的位置
- 是线程私有的,每个线程都拥有一个程序计数器,生命周期与线程一致
- 是运行时数据区唯一一个内存不会溢出的空间
- 运行速度最快
2.本地方法栈
如果执行native()方法,也就是本地方法(使用C、C++写的方法),其中涉及的信息就存在这。
- 用来运行本地方法的区域
- 是线程私有的
- 空间大小可以调整
- 可能出现栈溢出
3.Java虚拟机栈
对应的当前线程的信息,执行对象过程中定义的变量/属性都会存储在这。
基本作用特征:
栈是运行单位,管理方法的调用运行
是用来运行java方法的区域
是线程私有的
可能出现栈溢出
运行原理:
先进后出的结构
最顶部的称为当前栈帧
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧
栈帧结构:
一个栈帧包含:
局部变量表(存储在方法中声明的变量)
操作数栈(实际计算运行过程)
动态链接
void A(){
B();//B方法的地址
}
方法返回地址
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈可能会出现内存溢出
java栈,本地栈大小是可以调整的
4.堆
基本作用特征
在字节码编译为机器代码的过程中,会产生对象,存储在堆中
是存储空间,用来存储对象,是内存空间最大的一块儿区域,
在jvm启动时就被创建,大小可以调整(jvm调优)
本区域是存在垃圾回收的.是线程共享的区域
堆空间的分区:(堆空间的分区是为以后垃圾回收做准备)
年轻代(新生区/新生代)
伊甸园区(对象刚刚创建存储在此区域)
幸存者1区
幸存者2区
老年代(老年区)
为什么要分区
可以根据对象的存活的时间放在不同的区域,可以区别对待.
频繁回收年轻代,较少回收老年代.
创建对象,在堆内存中分布
1.新创建的对象,都存储在伊甸园区
2.当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象,移动到幸存者1区,
3.之后创建的新对象还是存储在伊甸园区,再次垃圾回收到来时,将伊甸园中的存活对象移动到幸存者2区,
同样将幸存者1区的存活对象移动到幸存者2区,每次保证一个幸存者区为空的,相互转换.
4.每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历过15次回收,仍然存活,就会被移动到老年代
垃圾回收次数,在对象头中有一个4bit的空间记录 最大值只能是15,
5.老年区回收次数较少,当内存空间不够用时,才会去回收老年代.
堆空间的配置比例
默认的新生代与老年代的比例:1:2 可以通过 -XX:NewRatio=2 进行设置
如果项目中生命周期长的对象较多,就可以把老年代设置更大
在新生代中,伊甸园和两个幸存者区比例:8:1:1
可以通过 -XX:SurvivorRatio=8 进行设置
对象垃圾回收的年龄 -XX:MaxTenuringThreshold=
分区收集思想 Minor GC、Major GC、Full GC
部分收集、整堆收集
- 新生区收集(Minor GC/Yang GC): 只是新生区(Eden ,S0,S1)的垃圾收集 ,回收比较频繁
- 老年区收集(Major GC/Old GC):只是对老年区的垃圾收集,回收的次数较少
- 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集,尽量避免
整堆收集出现的情况
System.GC();时 程序猿几乎不用
老年区空间不足
方法区空间不足
开发期间要避免整堆收集
堆空间的参数设置
官方文档
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
字符串常量池
在jdk7之后,将字符串常量池的位置从方法区转移到了堆空间中
因为方法区的回收在整堆收集时发生,回收频率低
堆空间回收频率高
方法区
作用: 主要用来存储加载的类信息, 以及即时编译期编译后的信息, 以及运行时常量池
特点: 在jvm启动时创建,大小也是可以调整, 是线程共享,也会出现内存溢出.
方法区,堆,栈交互关系
方法区存储类信息(元信息)
堆中存储创建的对象
栈中存储对象引用
方法区大小设置
-XX:MetaspaceSize 设置方法区的大小
windows jdk默认的大小是21MB
也可以设置为-XX:MaxMetaspaceSize 的值是-1,级没有限制. 没有限制 就可以使用计算机内存
可以将初始值设置较大一点,减少了FULL GC发生
方法区的内部结构
类信息
以及即时编译期编译后的信息
以及运行时常量池(指的就是类中各个元素的编号)
方法区的垃圾回收
在FULL GC时方法区发生垃圾回收.
主要是回收类信息, 类信息回收条件比较苛刻,满足以下3点即可:
1.在堆中,该类及其子类的对象都不存在了
2.该类的类加载器不存在了
3.该类的Class对象不存在了
也可以认为类一旦被加载就不会被卸载了.
特点
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈,堆,方法区可能会出现内存溢出
java栈,本地栈,堆,方法区大小是可以调整的
堆,方法区是线程共享的,是会出现垃圾回收的
本地方法接口
什么是本地方法接口
简单地讲,一个 Native Method 是一个Java调用非Java代码的接囗。一个 Native Method 是这样一个Java方法:该方法的实现由非Java语言实现,比如C。
用native关键字修饰的方法称为一个本地方法,没有方法体
hashCode();
为什么用本地方法
Java在有些层次的任务用Java实现起来不容易
java语言需要外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可。
java的JVM本身开发也是在底层使用到了C语言
执行引擎
执行引擎包含:解释器(2种)、JIT即时编译器、GC
解释器
用来执行方法区/内存里面所存储的字节码指令。
JVM设计初期,仅仅为满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而实现了在运行时采用逐行解释字节码到机器码执行程序的方案。
JVM发展史中,共有两套解释执行器:
古老的字节码解释器:在执行时通过纯软件代码翻译字节码的执行,效率非常低下。
现在普遍使用的模板解释器:将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,提高了解释器的性能。
JIT即时编译器
如果遇到经常执行的字节码指令,只要执行过一次,后续再来执行就不需要翻译,可以直接取出对应的机器指令,性能更快,提高了执行效率。
所以JIT编译器就是将字节码指令直接编译为机器指令,只会处理一些频繁/热点的字节码指令。
由于解释器在设计和实现上非常简单,且相当低效。为了解决这个问题,JVM提供了即时编译技术:即时编译可以将整个函数体编译成为机器码,有效到避免函数体被解释执行,而是重复执行时直接执行编译后的机器码即可,大大提示了执行效率。
作用: 将加载到内存中的字节码(不是直接运行的机器码), 解释/编译为不同平台的机器码.
.java —编译–>.class 在开发期间,由jdk提供的编译器(javac)进行源码编译 (前端编译)
.class(字节码)----解释/编译—> 机器码 (后端编译,在运行时,由执行引擎完成的)
解释器: 将字节码逐行解释执行, 效率低
编译器(JIT just in time 即时编译器): 将字节码编译,缓存起来,执行更高效, 不会立即使用编译器
将一些频繁执行的热点代码进行编译,并缓存到方法区中,以后执行效率提高了.
程序启动后,先使用解释器立即执行,省去了编译时间
程序运行一段时间后,对热点编译缓存,提高后续执行效率
采用的解释器和编译器结合的方案.
垃圾回收(GC)
垃圾回收概述
java支持自动垃圾回收,有些语言不支持需要手动
第一个使用自动垃圾回收的不是Java语言,
概念:垃圾收集和内存的动态分配
什么内存需要回收(什么样的对象是垃圾)
垃圾就是指在运行程序中没有任何引用指向的对象,这个对象就是垃圾,如果不及时清理可能会导致内存溢出
为什么要进行垃圾回收
如果不进行垃圾回收,内存迟早都会被消耗完
不断的分配内存空间而不进行回收,就像不停的生产生活垃圾而从来不进行打扫一样
垃圾回收也可以清除内存里的记录碎片,将这些随便进行整理占用的内存移动到堆的一端,方便JVM将整理出的内存分配给新的对象(数组必须是连续空间)。
内存溢出和内存泄露
内存溢出:在程序运行过程中,经过垃圾回收处理之后创建的对象还是不能存储在内存中,即内存已经全部占用,剩余内存不足以用来存储新创建的对象
内存泄露:打开了使用对象的东西,但是没有关闭,导致垃圾处理时认为对象处于运行状态,不会被回收处理,IO流 close jdbc连接 close没有关闭,生命周期很长的对象,一些已经不用的对象,但是垃圾回收器不能断定为垃圾,这些对象就默默的占用内存,称为内存泄露,大量的此类对象存在,也是导致内存溢出的原因
自动内存管理
好处: 解放程序员, 对内存管理更合理,自动化.
不好的: 对程序员管理内存的能力降低了, 解决问题能力变弱了, 不能调整垃圾回收的机制
垃圾回收相关算法
标记阶段
作用:判断对象是否是垃圾对象,是否有引用指向对象
相关的标记算法:引用计数算法和可达性分析算法
引用计数算法(在现代的JVM中并没有被使用)
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就 +1 ;当引用失效时,计数器值就-1 ;任何时刻计数器为 0 的对象就是不可能再被使用的。
有个计数器来记录对象的引用数量
String s1 = new String("aaa");
String s2 = s1; //有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1
引用计数法原理简单,判定效率也很高,但单纯的引用计数就很难解决对象之间相互循环引用的问题。
例如,存在两个对象象objA和objB,他们都有字段instance,令objA.instance=objB;objB.instance=objA。除此之外,这两个对象再无任何引用,实际上这两个对象已 经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
缺点:
需要维护计数器,占用空间,频繁操作需要事件开销
无法解决循环引用问题. 多个对象之间相互引用,没有其他外部引用指向他们,计数器都不为0,不能回收,产生内存泄漏.
可达性分析算法/根搜索算法
实现思路: 从一些为根对象(GCRoots)的对象出发去查找,与根据对象直接或间接连接的对象就是存活对象,不与根对象引用链连接的对象就是垃圾对象.
GC Roots 可以是哪些元素?
在虚拟机栈中被使用的.
在方法中存储的静态成员指向的对象
作为同步锁使用的 synchronized
在虚拟机内部使用的对象
对象的 finalization 机制
当一个对象被标记为垃圾后,在真正被回收之前,会调用一次Object类中finalize(). 是否还有逻辑需要进行处理.
自己不要在程序中调用finalize(),留给垃圾回收器调用.
有了finalization机制的存在,在虚拟机中把对象状态分为3种:
1.可触及的 不是垃圾,与根对象连接的
2.可复活的 判定为垃圾了,但是还没有调用finalize(),(在finalize()中对象可能会复活)
3.不可触及的: 判定为垃圾了,finalize()也被执行过了,这种就是必须被回收的对象
垃圾回收阶段的算法
标记–复制算法:
将内存分为大小相等的两块,每次只是用其中的一块,当这一块内存用完了,九江当前内存中存活着的对象复制到另一块上面,然后将正在使用的内存空间中的垃圾对象清除。
优点: 减少内存碎片
缺点: 如果内存中多数对象存活,则需要复制的对象数量多,效率低.
适用场景: 存活对象少 新生代适合使用标记复制算法
标记-清除算法
标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
清除不是真正的把垃圾对象清除掉,
将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可.
缺点:
执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
回收后有碎片产生,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-压缩算法(标记-整理)
标记-整理算法:其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:
- 移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行。
由以上三种算法可以看出:是否移动对象都存在弊端,移动的话内存回收更复杂,不移动则内存分配会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整 个程序的吞吐量来看,移动对象会更划算。
此外就出现了另一种解决方案:
- 可以不在内存分配和访问上增加太大额外负担,做法是让虚 拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。
垃圾回收器
垃圾回收器是垃圾回收的实际实现者,垃圾回收算法是方法论
垃圾回收器分类
-
按照线程数量分
单线程垃圾回收器:Serial Serial old
多线程垃圾回收器:Parallel
-
按照工作模式分为
独占式:垃圾回收线程执行时,其他线程暂停
并行式:垃圾回收线程可以和用户线程同时执行
-
按工作的内存区间
年轻代垃圾回收器,老年代垃圾回收器
-
垃圾回收期性能指标
暂停时间(在垃圾回收过程中,其他线程暂停)
吞吐量
回收的速度
占用内存大小
CMS垃圾回收器
Concurrent Mark Sweep 并发标记清除
支持垃圾回收线程与用户线程并发(同时)执行
初始标记:独占式的暂停用户线程
并发标记:垃圾回收线程与用户线程并发(同时)执行
重新标记:独占式的暂停用户线程
并发清除:垃圾回收线程与用户线程并发(同时)执行 进行垃圾对象的清除
优点:可以作到并发收集
弊端:使用标记清除算法,会产生内存碎片,并发执行影响到用户线程,无法处理浮动垃圾
三色标记:
由于CMS有并发执行过程,所以在标记垃圾对象时有不确定性。
所以在标记时,将对象分为三种颜色(3中状态)
黑色: 例如GCRoots确定是存活的对象
灰色:在黑色对象中关联的对象,其中还有未扫描完的,之后还需要再次进行扫描
白色:与黑色,灰色对象无关联的,垃圾收集算法不可达的对象
标记过程:
1.先确立GCRoots,把GCRoots标记为黑色
2.与GCRoots关联的对象标记为灰色
3.再次遍历灰色,灰色变为黑色,灰色下面有关联的对象,关联的对象变为灰色
4.最终保留黑色,灰色,回收白色对象
可能会出现漏标,错标问题
G1(Garbage-First)垃圾优先
将堆内存各个区又分为较小的多个区域,对这些区域进行检测,对某个区域中垃圾数量大的区域优先回收。
也是并发收集的