JVM超详解

jdk:java开发工具包(编译工具,打包工具)

jre:java运行时环境(库,类)

jvm:将字节码(class)编译为机器码,运行java程序

编译:源码(.java)->.class

jvm:.class->字节码

概述:jvm虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,是一个跨语言平台(只要是编译为字节码的文件都可以运行)

一.JVM的构成

1.类加载器

概述:这个模块负责从硬盘上加载类(.class文件)到虚拟机,类加载器只负责将类加载进去,是否能执行交给执行引擎

类加载的过程

加载阶段->链接阶段->初始化阶段

1>加载阶段:利用io读取字节码文件

2>链接阶段:验证 字节码是否被污染(格式是否正确),准备解析

3>初始化:对类中一系列静态 成员进行初始化赋值,当类加载完成初始化阶段时,一个类才真正完成加载

类什么时候会被加载?

类被使用时会加载(创建对象,调用类中静态属性/方法,反射机制,执行类中的main方法,子类被加载时也会加载对应的父类)

还有两种情况,类不会被加载

1.只使用类中被final修饰的成员(静态常量),类不会被加载

2.类当作类型使用时(数组类型,泛型)

类加载器专门加载类的类

分类

1>.启动类加载器(c/c++语言实现,嵌入在虚拟机中)

++用于加载java核心类,库(系统中提供的类)++

2>.扩展类加载器(java语言写的)

++继承于Class.Loader,用于加载java.jre.lib.ext.dirs目录下的扩展类,自己也可用写一些类放在此目录下++

3>.应用程序加载器(java语言写的)

++用于加载程序员自己定义的类++

双亲委派机制(类加载的时采用的机制)

java虚拟机对class文件采用的是按需加载的方式,将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,虚拟机采用的是双亲委派模式,即把请求交给父类处理,它是一种任务委派模式,想要先加载系统中的类

当加载一个类时,先让父级的类加载器去加载,优先加载系统中的类,如果系统中没有,才委派给下级类加载器,子级如果找到了就加载该类,如果下级没有找到,就抛出ClassNotFoundException异常

在Java虚拟机中,当一个类被加载时,它的所有父类(包括直接父类和间接父类)都会被加载,一直到 java.lang.Object

为什么要用双亲委派机制?

答:为了安全,避免我们自己写的类替换了系统中的类

如何打破双亲委派机制?

答:可以通过自定义类加载器来打破,写一个类继承ClassLoader类,重写findClass()方法

正常的类加载器会调用一个loaderClass方法,这个方法中存在向上委托的代码,所有走了双亲委派的机制,我们自定义了一个类加载器,重写了该方法,没有写向上委托的代码,所以打破了双亲委派机制

2.运行时数据区

概述:存储运行时数据的区域,类信息(方法区),对象(堆),变量(栈)...

运行时数据区可以分为5个区域:

1.程序计数器

程序计数器用来记录cpu下一条指令的地址,cpu要切换执行许多的程序,再切换之前需要记录程序本次执行的位置,下次回来时,继续接着执行

很小的内存空间(存储的都是地址),运行速度最快的存储区域,每个线程都私有一个自己的程序计数器,生命周期与线程生命周期一致(线程创建启动后计数器开始工作,线程结束后,程序计数器结束),在JVM中唯一一个不会出现内存溢出的区域

特点:内存空间小,运行速度快,线程私有,生命周期与线程一致,不存在内存溢出情况,不会出现垃圾回收

2.虚拟机栈

概述:栈是运行时单位,即栈解决程序的运行问题,每个线程在创建时都会创建一个虚拟机栈,是线程私有的,存在内存溢出问题,如递归调用过多

作用:虚拟机栈是运行的结构设计,管的是方法如何执行(java中总是main方法先执行)

特点:

1>.虚拟机栈是线程私有的,每个线程创建就会创建一个虚拟机栈

2>.方法入栈后,称为一个栈桢(方法的信息)

3>.不存在垃圾回收

4>.存在内存溢出问题,如递归调用次数过多

栈帧:一个方法入栈之后称为栈桢

栈帧的组成结构:局部变量表,方法返回地址,操作数栈(运算在操作数栈中),动态链接,附加信息

3.本地方法栈

java虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法(c/c++)的调用,本地方法栈也是线程私有的,不会出现垃圾回收,存在内存溢出问题

4.堆

作用:java中所创建的对象都存储在堆空间中

特点:所有线程共享堆。堆是运行时数据区中最大的一块内存区域。堆内存的大小是可以调节的(通过参数可以调节)。堆中的对象不会马上被移除,仅仅在垃圾收集时才会被移除(垃圾回收的重点区域)。堆空间存在内存溢出。

堆空间内存划分:

新生区:伊甸园(Eden),幸存者0,幸存者1

老年区:

为什么要分区 :将不同生命周期的对象,存储在不同的区域,针对不同的区域进行不同的垃圾回收算法,频繁的回收新生代,较少回收老年代

垃圾对象:没有任何引用指向的对象

对象内存分配过程

首先java中新创建的对象都存储在新生区(伊甸园区),当垃圾回收发生时,会将伊甸园中存活对象移动到幸存者0区,清空伊甸园区

继续创建对象,当下一次垃圾回收执行时,将伊甸园区和幸存者0区存活的对象放在幸存者1区,清空伊甸园区和幸存者0区,每一次只使用一个幸存者区

当一个对象默认回收15次后依然存活,那么就把该对象移动到老年区

当一个对象经历15次垃圾回收,依然存活那么就把该对象移入老年区

为什么回收次数最大是15次就要移到老年区:对象头中有一块区域负责记录分代年龄,占四个bit位,最大表示为15(1111),可以通过参数更改

堆空间的大小如何设置(JVM调优)

5.方法区(元空间)

概述:是一个被线程共享的内存区域,其中主要存储加载的类字节码,class/method/field等元数据,static final 常量,static常量等数据

方法区的大小也可以通过参数设置

方法区存在内存溢出,当加载的类信息很多时,空间不足就会报错

方法区垃圾回收 :方法区的垃圾回收主要是卸载类信息,但类信息回收条件很苛刻

类被回收需要满足下列三个条件:

1.该类所有的实例都已经被回收,该类派生的子类也都被回收

2.加载该类的类加载器已经被回收

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类

本地方法接口:

什么是本地方法:native关键字修饰的方法,不是用java语言实现的

为什么要使用本地方法:与java环境外交互,java语言是应用层语言,java语言没有权限直接操作硬件设备(内存,硬盘),操作系统就为上层的语言写好了一些操作硬件的接口,上层语言只需要调用这些系统接口即可,底层实现由操作系统处理

3.执行引擎

概述:将加载进来的一些字节码(汇编指令)文件转为机器码文件,是java虚拟机核心组成部分之一

前端编译:通过javac命令,把.java文件编译为.class文件

后端编译:运行时由执行引擎把字节码编译为机器码

java是半解释半执行语言

为什么执行引擎设计为半编译半解释将字节码翻译为机器码

将高级语言翻译为机器码一半有两种方式:1.解释执行 2.编译执行

解释执行:html,css,js,sql,python

解释执行有一个特点:一行一行由解释器解释执行

缺点:执行效率低 优点:不需要编译,可以由解释器直接解释执行

编译执行:c,c++,java,需要先整体编译为字节码,编译后执行效率高

缺点:编译需要等待 优点:执行效率快

执行引擎可以对某些执行频繁的热点代码进行追踪,将热点代码编译后缓存起来,下次使用时省去编译时间

程序刚开始运行时,先通过解释器解释执行,提高响应速度,当热点代码编译后,使用编译执行,提高后续程序执行速度

5.垃圾回收

垃圾对象 :在程序运行时,一个对象没有任何引用指向时就是垃圾对象

垃圾对象如果不及时清理,长期占用内存空间,就会导致新创建的对象空间不足,最终发生内存溢出错误

手动内存管理好处:对内存管理更精确,用时申请,用完销毁

手动内存管理坏处:麻烦,容易造成内存泄露(申请的内存忘记回收)

现在的语言都支持自动内存管理(自动垃圾回收)

垃圾回收重点区域:堆,方法区(卸载类信息,条件苛刻)

堆里面频繁回收新生代,较少回收老年代

内存溢出和内存泄露

内存溢出(out of memory oom):应用系统中存在无法回收的内存或使用的内存过多,内存空间不够了,后续创建的对象放不下了,此时程序会崩溃终止

内存泄露:一个对象已经不再被使用 ,本应该被回收掉,但是由于某些原因不能被回收,这就使垃圾对象占用了我们的内存空间,这种现象称为内存泄露

例如:数据库连接,io,socket需要我们手动close释放对象,创建的对象没有引用指向,丢失也是内存泄露

垃圾回收算法

1.标记阶段

标记阶段的目的:主要是为了判断对象是否是垃圾对象,对堆空间中的对象进行应该标记,

那一些对象是有用的,那一些对象已经是垃圾对象

判断对象是否为垃圾对象一般有两种方式:引用计数器算法和可达性算法

1>引用计数器算法(目前未被使用)

对象中有一个计数器,用来记录有几个引用变量指向对象,当计数器值为0,对象为垃圾对象,虽然实现比较简单,但是也存在下列问题

为什么不使用引用计数器算法?

1.需要单独字段存储计数器字段,增加存储开销

2.每次赋值都需要更新计数器,增加时间开销

3.无法处理循环引用的情况(致命原因,会出现内存泄露),导致java的垃圾回收器中没有使用这类算法

什么是循环引用?

A对象中有个b,B对象中有个c,C对象中有个a,他们三个互相引用,引用计数器都为1,但是如果p丢失,那么外界无法找到这三个对象,造成内存泄露(相当于单链表的头指针丢失)

2>可达性分析算法(根搜索算法)

可达性分析算法以根(活跃对象)为起点 开始搜索,按照从上到下的方式搜索被根对象所连接的目标是否可达 ,解决了引用计数器算法中循环引用的问题,避免了内存泄露

使用可达性分析算法,内存中存活的对象都会被直接或者间接的连接着,如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象可以标记为垃圾对象

可达性分析算法在标记中,会导致所有java线程停顿(确保一致性)

那些对象是活跃对象(根GCRoots)?

1.虚拟机栈中引用的对象(入栈后等待运行)

2.方法区中类静态属性引用的对象(类中定义的全局成员变量)

3.所有被同步锁synchronized持有的对象

4.java虚拟机内部的引用(系统中的类,Class对象,异常对象)

对象的finalization机制

final finally finalize

final:修饰关键字

finally:无论是否处理异常都会执行finally中的代码

finalize方法(Object中的方法,子类可以重写):

在对象首次被判定为垃圾对象后,在回收该对象前,由垃圾收集器调用finalize方法,可以在finalize方法中执行一些最终操作(自定义逻辑处理)

finalize方法只会被调用一次,如果重写该方法时又调用了该对象,该对象就不被标记为垃圾对象,下一次被判定为垃圾对象后,不会调用finalize方法

对象生成还是死亡?

由于finalize方法的存在,虚拟机中的对象一般有三种状态

1.可触及的:从根结点可以连接到的(可达性算法),对象被使用着,不是垃圾对象

2**.可复活**的:对象首次被判断为垃圾对象,finalize方法未被调用,有可能会复活(在finalize中被引用)

3.不可触及的:被判定为垃圾对象,且finalize方法被调用过了(不可能复活)

垃圾回收阶段算法

标记-复制算法

它将可用内存按容量划分为两块 (空闲,正在使用),每次时候只使用其中的一块,在垃圾回收时将正在使用内存中的存活对象(可达性算法)复制到未被使用的内存块中 ,之后清除正在使用的内存块中的所有对象(只剩下垃圾对象),然后互换两个内存块角色,最后完成垃圾回收

使用场景:适合存活对象较少,垃圾对象较多(新生代)

这种回收算法,可用减少内存中的碎片(将存活对象,按空间连续性移动)

标记-清除算法

保持存活对象原地不动,清除垃圾对象,适合老年代 ,因为老年代对象存活时间长,也有的对象内存比较大减少移动次数会产生内存碎片(内存空间不连续)

标记-整理算法

将所有存活的对象压缩到内存中的一端 ,按顺序排放,减少内存空间的产生,但是要移动存活对象。之后,清理边界外所有的对象,适用于老年代

区别

复制算法:

划分出来两块内存空间,将存活对象移动到另一块内存区域(有序排放),清理原区域所有对象

优点:不产生内存碎片 缺点:要移动对象 适用场景:年轻代

清除算法:

不移动存活对象,清除所有垃圾对象

优点:不需要移动存活对象 缺点:产生内存碎片 适用场景:老年代

整理算法:

将所有存活对象按顺序排放在内存一端,清除剩余区域所有空间

优点:不产生内存碎片 缺点:需要移动存活对象 适用场景:老年代

垃圾收集器

内存回收的实践者,垃圾回收算法是内存回收的理论者

由于jdk的版本处于高速迭代过程中,垃圾收集器也衍生了很多版本

按线程分类:单线程垃圾收集器和多线程垃圾收集器

单线程垃圾收集器:只有一个线程进行垃圾回收,会暂停其他用户线程

多线程垃圾收集器:有多个线程进行垃圾回收,会暂停其他用户线程

STW:指的是GC事件过程中,会产生应用程序停顿

按工作模式分类:独占式和并发式

独占式垃圾收集器:垃圾收集器执行时,用户线程暂停

并发式垃圾收集器:垃圾收集器执行时,不影响用户线程

按照工作内存内存分类:新手代垃圾收集器和老年代垃圾收集器

GC性能指标

吞吐量:

暂停时间(STW):用户线程暂停的时间越短越好

jdk8中内置的垃圾收集器

相关推荐
Ialand~7 小时前
深度解析 Rust 的数据结构:标准库与社区生态
开发语言·数据结构·rust
在坚持一下我可没意见7 小时前
Java 网络编程:TCP 与 UDP 的「通信江湖」(基于TCP回显服务器)
java·服务器·开发语言·笔记·tcp/ip·udp·java-ee
杜子不疼.7 小时前
【Rust】异步处理器(Handler)实现:从 Future 本质到 axum 实战
android·开发语言·rust
学习编程之路7 小时前
Rust内存对齐与缓存友好设计深度解析
开发语言·缓存·rust
无限进步_7 小时前
C语言字符串连接实现详解:掌握自定义strcat函数
c语言·开发语言·c++·后端·算法·visual studio
Han.miracle7 小时前
Java的多线程——多线程(二)
java·开发语言·线程·多线程
阿登林8 小时前
Unity3D与Three.js构建3D可视化模型技术对比分析
开发语言·javascript·3d
cherryc_8 小时前
JavaSE基础——第十二章 集合
java·开发语言
集成显卡8 小时前
Bun.js + Elysia 框架实现基于 SQLITE3 的简单 CURD 后端服务
开发语言·javascript·sqlite·bun.js