JVM学习笔记1

参考资料:【黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题)】BV1r94y1b7eS

———————————————————↓基础篇↓———————————————————

1.初识JVM

JVM全称是Java Virtual Machine,中文译名Java虚拟机

本质是一个运行在计算机上的程序,职责是运行Java字节码文件

功能:

1.解释和运行

对字节码文件中的指令,实时的解释成机器码,让计算机执行

2.内存管理

自动为对象、方法等分配内存

自动的垃圾回收机制,回收不再使用的对象

3.即时编译

对热点代码进行优化,提升执行效率

(Java如果不做任何优化,性能不如C、C++等语言)

(Java需要实时解释,主要是为了支持跨平台特性)

2.详解字节码文件

JVM的组成

JVM的组成主要有三部分:

类加载器(ClassLoader):加载class字节码文件中的内容到内存中

运行时数据区域(JVM管理的内存):负责管理JVM使用到的内存,比如创建对象和销毁对象

执行引擎(即时编译器、解释器、垃圾回收器):将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能(其中,垃圾回收器需要重点学习和掌握)

字节码文件的组成

(1)以正确的姿势打开文件

推荐使用jclasslib工具查看字节码文件:

其中各部分所记录的信息如下: 

一般信息:魔数、字节码文件对应的Java版本号、访问标识(public/final……)、父类和接口(具体如上图)

常量池:保存了字符串常量、类或接口名、字段名;主要在字节码指令中使用

字段:当前类或接口声明的字段信息

方法:当前类或接口声明的方法信息

属性:类的属性,比如源码的文件名、内部类的列表等

(2)字节码文件的组成

Magic魔数:

文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容

软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会报错

Java字节码文件中,将文件头称为Magic魔数

可以看到,下面图中class文件的前几个字节都是CAFEBABE

主副版本号:

主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号

版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容

常量池:

常量池的作用为避免相同的内容重复定义,节省空间

常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据

字节码指令中通过编号引用到常量池的过程称之为符号引用

方法:

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中

操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置

(3)玩转字节码常用工具

javap -v命令:

javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容,适合在服务器上查看字节码文件内容

直接输入javap查看所有参数

输入javap -v 字节码文件名称,查看具体的字节码信息(如果是jar包,需要先使用jar -xvf命令解压)

jclasslib:

本地文件可以使用jclasslib工具查看

jclasslib也有idea插件版本,可以在代码编译之后实时看到字节码内容

阿里arthas:

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不 改应用代码的情况下,对业务问题进行诊断,提升线上问题排查效率

类的生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程

(1)生命周期概述

主要有五个阶段:(下面会重点讲解前三个阶段)

(2)加载阶段

1.加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息

2.类加载器加载完类之后,JVM会将字节码中的信息保存到方法区中,生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能(比如多态)的信息

3.同时,JVM还会在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息,这样JVM就能很好地控制开发者访问数据的范围

可用JDK自带的hsdb工具查看JVM内存信息,工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中

启动命令:java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

(3)连接阶段

连接阶段有三个环节,如下所示:

验证环节:

验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束,该阶段一般无需程序员参与

主要包含四部分:

1.文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本需求

2.元信息验证,比如类必须有父类(super不能为空)

3.验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法去

4.符号引用验证,例如是否访问了其他类中的private的方法等

准备环节:

准备的主要目的是为静态变量(static)分配内存并设置初始值

准备环节只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其自己的初始值

final修饰的基本数据类型的静态变量,准备环节会直接将代码中的值赋给它

解析环节:

解析的主要目的是将常量池中的符号引用替换为直接引用

符号引用就是在字节码文件中使用编号来访问常量池中的内容

直接引用不再使用编号,而是使用内存中地址访问具体的数据

(4)初始化阶段

初始化阶段会执行静态代码块中的代码,并为静态变量赋值

初始化阶段会执行字节码文件中的clinit部分的字节码指令

clinit方法中的执行顺序与Java中编写的顺序是一致的

会导致类的初始化的方式:

1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化

2.调用Class.forName(String className)

3.new一个该类的对象时

4.执行Main方法的当前类

下面代码的最终输出结果为:

DACBCB

执行main方法先初始化Test1的初始化方法,输出结果DA

然后执行两次Test1的构造方法,输出CBCB

clinit指令在特定情况下不会出现,比如:

如下几种情况是不会进行初始化指令执行的。

1.无静态代码块且无静态变量赋值语句

2.有静态变量的声明,但是没有赋值语句

3.静态变量的定义用final关键字,这类变量会在连接阶段的准备环节直接进行初始化

另外:

直接访问父类的静态变量,不会触发子类的初始化

子类的初始化clinit调用之前,会先调用父类的clinit初始化方法

上述代码的执行过程:

1.调用new创建对象,需要初始化B02,优先初始化父类

2.a = 1

3.子类初始化,a = 2

类加载器

类加载器(ClassLoader)是JVM提供给应用程序去实现获取类和接口字节码数据的技术

类加载器只参与加载过程中的字节码获取并加载到内存这一部分

(1)类加载器的分类

主要分为两类,一类是Java代码中实现的,另一类是Java虚拟机底层源码实现的

启动类加载器(Bootstrap ClassLoader):

是由Hotspot虚拟机提供的、使用C++编写的类加载器

Java中的默认类加载器:

扩展类加载器和应用程序加载器都是JDK提供的、使用Java编写的类加载器

他们的源码都位于sun.misc.Launcher中,是一个静态内部类,继承自URLClassLoader

扩展类加载器(Extension Class Loader):

JDK中提供的、使用Java编写的类加载器,默认加载Java安装目录/jre/lib/ext下的类文件

应用程序类加载器(Application Class Loader):

JDK中提供的、使用Java编写的类加载器,默认加载classpath下的类文件

(2)双亲委派机制

在Java中如何使用代码的方式去主动加载一个类?

方法1:使用Class.forName方法,使用当前类的类加载器去加载指定的类

方法2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载

每个Java实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器,可以理解为它的上级,并不是继承关系

应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空

启动类加载器使用C++编写,没有上级类加载器

在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器

如果类加载器的parent为null,则会提交给启动类加载器处理

如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载(所以看上去是自顶向下尝试加载)

第二次再去加载相同的类,仍然会向上进行委派,如果某个类加载器加载过就会直接返回

双亲委派机制指的就是:自底向上查找是否加载过,再由顶向下进行加载

双亲委派机制解决的三个问题:

1.重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?

根据双亲委派机制,启动类加载器的优先级是最高的,因此由启动类加载器来加载。

2.String类能覆盖吗:在自己的项目中去创建一个java.clang.String类,会被加载吗?

不会,而是会交由启动类加载器加载在rt.jar包中的String类。

3.类加载器的关系:这几个类加载器彼此之间存在关系吗?

应用类加载器的父类加载器是拓展类加载器,扩展类加载器没有父类加载器,但是会委派给启动类加载器加载。

双亲委派机制的作用:

1.保证类加载的安全性

通过双亲委派机制,让顶层的类加载器去加载核心类,避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。

2.避免重复加载

双亲委派机制可以避免同一个类被多次加载,上层的类加载器如果加载过类,就会直接返回该类,避免重复加载。

(3)打破双亲委派机制

打破双亲委派机制有三种方式:

1.自定义类加载器

自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除

Tomcat就通过这种方式实现了应用之间类隔离:

一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类

如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了

Tomcat使用了自定义类加载器来实现应用之间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。

先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法

双亲委派机制的核心代码就位于loadClass方法中

打破双亲委派机制的核心就是将下边这一段代码重新实现

两个自定义类加载器加载相同限定名的类,不会冲突吗?

不会冲突,在同一个JVM中,只有相同类加载器+相同的类限定名才会被认为是同一个类

2.线程上下文类加载器

JNDI、JDBC、JCE、JAXB和JBI等框架使用了SPI机制+线程上下文类加载器

3.Osgi框架的类加载器

历史上Osgi框架实现了一套新的类加载器机制,允许同级类加载器之间互相调用

(4)JDK9之后的类加载器

JDK8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java

由于JDK9引入了module的概念,类加载器在设计上发生了很多变化:

1.启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。

Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。

启动类加载器依然无法通过Java代码获取到,返回的依然是null,保持了统一

2.扩展类加载器被替换成了平台类加载器(Platform Class Loader)

平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了 BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。

平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值