当前位置: 首页 > news >正文

理解JVM

目录

区域划分

程序计数器

java虚拟机栈

本地方法栈

java堆 

方法区

运行时常量池

对象的创建

对象的内存布局

垃圾回收

对象是否存活

引用计数算法

可达性算法

回收方法区

垃圾收集算法

标记-清除算法

复制算法 

标记-整理算法

​编辑分代收集算法

垃圾收集器

Serial 收集器

ParNew收集器

Paraller Scavenge收集器

Serial Old收集器

Paraller Old收集器

CMS收集器

G1收集器

 类文件结构

class类文件结构

类加载

类加载的时机

类加载过程

加载

        连接

       初始化

双亲委派模型

双亲委派模型

区域划分

Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在 Java 虚拟机启动时创建的,并且仅在 Java 虚拟机退出时销毁。其他数据区域按线程计算。每线程数据区域是在创建线程时创建的,并在线程退出时销毁。

程序计数器

程序计数器是一块较小的内存空间,用于记录指令的位置。

java程序编译生成class文件,class文件经由虚拟机运行。JVM工作时,就是根据程序计数器的值,来知道要执行哪一条指令。

因为java是支持多线程的,那么每一个线程都需要维护一个程序计数器,记录当前线程的执行指令的位置

java虚拟机栈

java虚拟机栈和程序计数器一样,都是线程私有的,生命周期和线程相同

java虚拟机栈描述的是方法执行的内存模型:方法运行时,创建栈帧,存储方法运行所需要的各种数据,一个方法的运行到结束,就是创建栈帧到销毁栈帧的过程

如果线程请求的栈深度大于了虚拟机允许的深度,那么就会抛出StackOverFlowError异常,就是我们递归可能出现的栈溢出

栈存放局部变量和方法调用

本地方法栈

本地方法栈和虚拟机栈的执行功能是非常相似的,不过就是虚拟机栈执行java方法,而本地方法栈则是为虚拟机使用的Native方法服务

java堆 

堆是虚拟机中内存最大的一块,是所有线程共享的一块区域。在虚拟机创建时创建,用于存放对象实例,几乎所有的对象实例都要在堆上分配内存。

假设对象不断在创建,并且没有垃圾回收机制来回收,那么堆区就会溢出

堆存放成员变量和new出来的对象

方法区

方法区和堆一样,是所有线程共享的区域。用于存储已经被虚拟机加载的类信息,常量,静态变量等

存放类信息,而static修饰的变量也是属于类的,也存放在方法区

运行时常量池

方法区的一部分,Class文件中除了类的版本,字段,方法,接口等信息之外,还有一个信息是常量池,用于存放编译期间生成的各种字面常量和符号引用,这部分内容将会在类加载之后进入运行时常量池

对象的创建

java是面向对象的,在语言层次上,我们使用new关键字创建对象,而在虚拟机层次上又是怎样的过程呢?

我们new一个对象,会先在常量池中查看这个参数是否已经被加载过,如果没有,就先要执行类加载过程。

在类加载结束之后,接下来虚拟机会给对象分配内存(对象所需要的空间大小在类加载时确定),也就是在堆中划分出区域来存放对象信息

内存分配成功后,要将分配的内存空间初始化为0(这也就是成员变量不赋值时,默认0的原因)

然后对对象进行必要的设置,例如这个对象是哪一个类的实例,如何找到类的数据信息等,而这些信息保留在对象头当中

对象的内存布局

对象在内存中存储的布局可以分为:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)

  • 对象头包括两个部分的内容:

1、存储对象自身的运行数据,例如哈希码,锁标志状态等,这些数据的长度在32位和64位的虚拟机中分别是32bit和64bit,官方称为“Mark Wold”。

2、类型指针:虚拟机通过这个指针来确定当前对象是哪一个类的实例

  • 实例数据:对象真正存储的有效信息,也即是代码定义的各种类型的字段内容
  • 对齐填充:HotSpot VM 要求对象大小必须是8字节的整数倍,使用对齐填充满足这一要求

垃圾回收

java内存分配中,程序计数器,本地方法栈,java虚拟机栈都是属于线程的,随线程而灭。

但是java堆和方法区则是所有线程都可以使用的,这里面的对象是运行时生成的,我们要对这里的内容进行回收

垃圾回收(GC)可以实现自动的垃圾回收

对象是否存活

 我们要释放的,肯定是“死去”的对象,如何分辨对象“是死是活”呢

引用计数算法

很多教科书判断对象是否存活的算法如下:

在对象中添加一个引用计数器,每当有一个地方引用到了这个对象,计数器+1,引用失效时,计数器-1,当计数器=0,表示这个对象没有被引用。

 优点:一旦这个对象没有被任何对象引用时,就可以被回收了

缺点:要维护引用计数器,浪费开销;无法解决循环引用的问题

主流的java虚拟机没有采用这个方法,因为它无法解决对象之间循环引用的问题

class Text{
Text t=null;
}

//在堆上创建Text()对象,t1引用了这个对象,引用计数器+1 为1
Text t1=new Text();
//在堆上创建Text()对象,t2引用了这个对象,引用计数器+1 为1
Text t2=new Text();
//t1.t引用了t2实例,也就是说t2实例再次被引用  引用计数器+1 为2
t1.t=t2;
//t2.t引用了t1实例,也就是说t1实例再次被引用  引用计数器+1 为2
t2.t=t1;

//t1不在引用Text对象,引用计数器-1 为1
t1=null;
//t2不在引用Text对象,引用计数器-1 为1
t2=null;

这时,t1和t2的引用计数器都不是0,不可回收,就导致了内存泄漏

(1) 内存泄露:内存泄露是指有的内存地址太过碎片化而无法被利用,我们都知道一个对象创建的时候开辟的内存空间是连续的,所以太过碎片化的内存空间就没办法利用。内存泄露多了也会导致内存溢出。

(2) 内存溢出:内存溢出是指内存已经装满了,无法再装下更多的对象了。

可达性算法

主流的语言都是通过可达性分析来判断对象是否存活的。通过一系列的称为“GC Roots”的对象做为起始点,从这些起点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明这个对象是不可用的

 在java中,可以作为GC Roots的对象包括以下几种:

1、虚拟机栈(栈帧中的本地变量表)引用的对象

2、方法区中类静态属性引用的对象

3、方法区中常量引用的对象

4、本地方法栈中Native方法引用的对象

回收方法区

java虚拟机规范中说过:可以不要求在方法区实现垃圾收集,因为在方法区实现垃圾回收的效率很低

方法区回收主要是两部分内容:废弃常量和无用的类

废弃常量和回收java堆中的对象非常相似,只要常量池中的常量,没有被引用,那么在回收时就可以被回收了

判定一个类是无用的类,条件比较苛刻:

1、java堆中不存在这个类的实例

2、加载这个类的ClassLoader已经被回收

3、这个类的Class对象没有被访问,无法通过反射来引用这个类

类卸载的效率很低,虚拟机可以回收,但是不代表会进行回收

垃圾收集算法

标记-清除算法

首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象

标记的过程就是可达性算法

stop-the-Word:在标记时需要暂停JVM用户进程

 问题就是:1、效率问题:标记和清除过程效率都很低 2、会产生大量不连续的内存片段

复制算法 

将可用内存按照容量划分为大小相等的两块,每次只能使用其中一块,当这一块的内存用完了,将还存活的对象复制到另一块上,将使用过的内存一次性清理掉

这样每次清理内存,都是整个半区清理,不会产生大量的不连续片段

缺点:可用内存逻辑上只有一半了;如果要复制的内存过大,就会产生很大的消耗 

标记-整理算法

不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界之外的内存

分代收集算法

  • 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法

一般可以分为新生代和老年代;新生代中,每一次垃圾收集时如果大量对象死去了,选择复制算法;老年代中因为对象存活率高,就选择标记-整理或者标记-清除算法

持久代中主要存放java类的类信息,和垃圾回收的java对象关系不大,但是年轻代和老年代对垃圾回收的影响是比较大的

 1、几乎所有刚刚生成的对象都是放在年轻代的,年轻代就是为了尽可能快速的收集那些生命周期短的对象

年轻代分三个区,一个Eden区,两个Survivor区,大部分对象在Eden区生成

当Eden区满时,还存活的对象被复制到一个Survivior区,当这个Survivor区满时,这个区的存活对象被复制到另一个Survivor区,这个Survivor区也满了时,从第一个Survivior区复制过来的并且仍然存活的对象,将被复制到老年代

2、老年代:年轻代经过了N次垃圾回收中仍然存活的对象,就会被放到老年代,所以老年代里存放的都是一些生命周期长的对象

3、大对象(需要大量连续内存的java对象)可能直接进入老年代

垃圾收集器

收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集器都可能有很大区别

Serial 收集器

最基本,发展历史最悠久的收集器,曾经是虚拟机新生代收集的唯一选择。它是一个单线程的收集器,但是这个“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作。更重要的是,它进行垃圾收集时,必须暂停其他所有的工作线程

SWT就是虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对于用户来说是非常不好的

ParNew收集器

ParNew收集器是Serial收集器的多线程版本

Paraller Scavenge收集器

一个新生代收集器,多线程的收集器。和其他收集器的关注点不同,其他收集器目的是尽可能缩短垃圾回收时用户线程的停顿时间,而Paraller Scavenge收集器目的在于达到一个可控制的吞吐量(吞吐量=用户代码的执行时间/(用户代码的执行时间+垃圾回收时间)),也即是运行用户代码的时间和CPU消耗时间的比值

停顿时间短,越适合与用户交互的程序;高吞吐量适用于高效率利用cpu的场景

Serial Old收集器

Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法

Paraller Old收集器

Paraller Scavenge收集器的老年代版本,多线程收集器,使用“标记-整理”算法

CMS收集器

CMS收集器是一种以获取最短停顿时间为目标的收集器,目前很大一部分的java应用集中在互联网站或者B/S系统的服务器上

CMS收集器是基于“标记-清除”算法实现的

G1收集器

当代收集器技术发展的最前沿成果之一

面向服务端应用的垃圾收集器

 类文件结构

对于java虚拟机而言,执行的是.class文件

class文件是一种特定的二进制文件格式,内包含了java虚拟机指令集和符号表以及若干其他辅助信息

class类文件结构

一个class文件对应着唯一一个类或者接口的定义信息

class文件是一组以8字节为基础单位的二进制流

根据java虚拟机规定,Class文件格式采用类似于c语言结构体的伪结构存储数据,这种伪结构只有两种类型:无符号数和表 

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯性以_info结尾,用于描述有层次关系的复合结构数据

1、magic:每个Class文件的头四个字节称为魔数(magic number ),用于确定这个class文件是否是可以被虚拟机接收的class文件

class文件的魔数是0xcafe babe

 2、接着魔数的四个字节是版本号;minor version是次版本号,major version是主版本号。主要版本号和次要版本号共同决定了文件格式的版本。如果文件的主要版本号为 M,次要版本号为 m,则我们将其文件格式的版本表示为 M.m。

3、常量池:接下来的是常量池入口。由于常量池中常量的个数是不确定的,所以设置 constant_pool_count,用于记录常量个数(为了使用0表述“不引用任何常量池”的意义,这个计数是从1开始的)

常量池主要存放两大常量:字面量和符号引用

字面量类似于我们常说的final修饰的常量,字面值等

符号引用则属于编译原理的概念,包含三种:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符

常量池中的每一项常量都是一个表

  constant_pool[constant_pool_count-1];

这是一个结构表 ,表示各种字符串常量、类和接口名称、字段名以及结构及其子结构中引用的其他常量。

jdk1.7之后已经有了14种表结构了,每一种表结构表示不同含义,例如constant_utf8_info表示utf-8编码的字符串,constant_integer_info表示整型字面量……,这14种表的第一位都是一个u1的标志位(tag),代表当前常量属于哪一种常量类型

4、访问标志

access_flags,用于识别一些类或者接口的访问信息:包括这个class是类还是接口,是否是public的,是否是abstract的,是否被声明final……

5、类索引,父类索引、接口索引集合

类索引this_class,父类索引super_class是u2类型的数据(因为java只支持单继承)

而接口索引集合 interfaces[interfaces_count]是u2类型的集合,interfaces_count用于记录集合数据的多少(一个类实现多个接口)

这几个属性,描述了类的继承关系

6、字段表集合

字段表 fields[fields_count];这些结构表示由此类或接口类型声明的所有字段,包括类变量和实例变量。

7、方法名集合

记录方法的相关信息  methods[methods_count];

这些结构表示此类或接口类型声明的所有方法,包括实例方法、类方法、实例初始化方法  以及任何类或接口初始化方法 

8、属性表集合

class文件、字段表、方法表都可以携带属性表,用于描述其他信息

类加载

虚拟机要将class文件内的信息加载进入内存,对数据进行校验,解析,初始化,最终形成可以被虚拟机使用的java类型数据,这些过程就是类加载

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存,分为7个阶段

加载-验证-准备-解析-初始化-使用-卸载,其中验证,准备,解析统称为连接

java虚拟机没有强制要求什么时候进行加载,但是在初始化时有要求

1、new一个类,或者访问类的静态方法时,如果没有初始化,那么先初始化

2、使用反射时,如果类没有使用过,要进行初始化

3、执行某一个类的main方法时,初始化

4、初始化子类时,发现父类没有初始化,先初始化父类

类加载过程

加载

完成三件事情

1、通过类的全限定名获取定义此类的二进制字节流

2、将这个字节流的静态存储结构转化为方法区的运行时数据结构

3、内存中生成一个Class对象

相当于找到class对象,读取.class文件数据,利用这些数据初始化出一个class对象

连接

第一步:验证

确保class文件的信息符合虚拟机要求并且不会对虚拟机造成攻击

第二步:准备

正式给类变量分配内存并且设置初始值(0,null)

第三步:解析

虚拟机将常量池中的符号引用替换为直接引用

初始化

根据程序员设置的值来初始化类变量

双亲委派模型

java虚拟机的角度来说,只会存在两种不同的类加载器

1、启动类加载器(Bootstrap ClassLoader)使用c/c++实现,是虚拟机的一部分

2、所有其他的类加载器,由java实现,独立于虚拟机之外

在开发人员的角度看来,主要分为3种

1、启动类加载器(Bootstrap ClassLoader):负责加载java标准库内的类

2、扩展类加载器(Extension ClassLoader):负责加载jdk扩展的类

3、应用程序类加载器(Application ClassLoader):负责加载当前项目目录中的类

双亲委派模型

下图展示的这种类加载器之间的层次关系,称之为类加载器的双亲委派模型

 如果一个类加载器收到了类加载的请求,它不会先自己尝试加载这个类,而是将这个请求委派给父类加载器,每层都会如此,最终所有的请求都会交给启动类加载器;只有父类加载器反馈自己无法完成加载请求,子加载器才会尝试加载

这样设计的好处就是,如果用户自己编写了一个和标准库同名的类,比如Object类,无论哪一个类加载器要加载这个类,都会交给启动类加载器,这样加载的都会是标准库的Object类;如果不使用双亲委派模型,那么可能类加载器加载的是我们编写的Object类,这样系统中就会存在我们写的Object类和标准库的Object类,程序会混乱

相关文章:

  • @GlobalLock注解作用与原理解析
  • [RK3568][Android11]内核Oops日志分析
  • 【VUE项目实战】64、CND优化ElementUI以及首页内容定制
  • 杂事之所忆
  • 全连接神经网络百度百科,全连接神经网络的作用
  • 你有真正了解过国产开源框架APM工具——SkyWalking技术有多牛吗
  • R语言奇异值分解
  • Python的一些Pythnoic【我自己没读完,待看待再次整理】
  • 【代码随想录】栈与队列专栏(java版本)
  • tsconfig 配置文件各字段详解
  • java毕业设计能源控制系统mybatis+源码+调试部署+系统+数据库+lw
  • 数据分析-numpy1
  • 汇率价格统一,当前购买Fabfilter价格更便宜了
  • BP神经网络需要训练的参数,bp神经网络建模步骤
  • 【Game Of AutoTest】3、游戏自动化测试的框架设计
  • [译]如何构建服务器端web组件,为何要构建?
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 【JavaScript】通过闭包创建具有私有属性的实例对象
  • 【面试系列】之二:关于js原型
  • Apache Pulsar 2.1 重磅发布
  • Docker下部署自己的LNMP工作环境
  • es6(二):字符串的扩展
  • js递归,无限分级树形折叠菜单
  • Material Design
  • python 学习笔记 - Queue Pipes,进程间通讯
  • Unix命令
  • vue-cli在webpack的配置文件探究
  • 聚簇索引和非聚簇索引
  • 面试遇到的一些题
  • 前端学习笔记之原型——一张图说明`prototype`和`__proto__`的区别
  • 容器化应用: 在阿里云搭建多节点 Openshift 集群
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 使用SAX解析XML
  • 数据仓库的几种建模方法
  • 移动端 h5开发相关内容总结(三)
  • nb
  • # Swust 12th acm 邀请赛# [ A ] A+B problem [题解]
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • #Z0458. 树的中心2
  • #每日一题合集#牛客JZ23-JZ33
  • (1)虚拟机的安装与使用,linux系统安装
  • (动态规划)5. 最长回文子串 java解决
  • (二)linux使用docker容器运行mysql
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (三) prometheus + grafana + alertmanager 配置Redis监控
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (详细版)Vary: Scaling up the Vision Vocabulary for Large Vision-Language Models
  • (转)Mysql的优化设置
  • (转)关于pipe()的详细解析
  • (转)项目管理杂谈-我所期望的新人
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .net core 6 集成和使用 mongodb