【JVM 学习笔记01】:基础知识

本文详细介绍了JVM的运行机制,包括类加载的全过程、内存区域划分以及垃圾回收机制。讲解了从字节码文件加载到内存,到对象在堆内存中的占用,再到方法执行完毕后的状态。同时,讨论了类加载器的双亲委派机制,以及Tomcat的类加载机制如何处理war包中的类。最后,探讨了如何估算对象在堆内存中的内存占用以及垃圾回收的原理。
摘要由CSDN通过智能技术生成

一、JVM 的运行机制

1.1 JVM整体的运行流程和原理?

第一步,把代码给打成“.jar”后缀的jar包,或“.war”后缀的war包,这个打包的过程一般会把代码编译成“.class”后缀的字节码文件,这个“.class”后缀的字节码文件,才可以被JVM运行起来!

第二步,就是把你打包好的jar包或者是war包给放到线上机器去部署。
部署有多种途径,但是最基本的一种方式,就是通过Tomcat这类容器来部署代码,也可以是手动通过 “java”命令来运行一个jar包中的代码。(采用“java -jar”命令,实际上此时就会启动一个JVM进程。)

第三步,JVM要运行这些“.class”字节码文件中的代码,首会采用类加载器把编译好的那些“.class”字节码文件给加载到JVM中,然后供后续代码运行来使用。

第四步,JVM会基于自己的字节码执行引擎,来执行加载到内存里的我们写好的那些类了,比如你的代码中有一个“main()”方法,那么JVM就会从这个“main()”方法开始执行里面的代码。他需要哪个类的时候,就会使用类加载器来加载对应的类,对应的类就在“.class”文件中。

在这里插入图片描述

思考题:如何对“.class”文件处理保证不被人拿到以后反编译获取公司源代码?

首先编译时,采用一些小工具对字节码加密,或者做混淆等处理。可以付费购买商业级的字节码文件加密的产品。然后在类加载的时候,对加密的类,考虑采用自定义的类加载器来解密文件即可,这样就可以保证源代码不被人窃取。

1.2 JVM在什么情况下会加载一个类?

一个类从加载到使用,一般会经历下面的这个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

在代码中用到这个类的时候,就会从“.class”字节码文件中加载这个类到JVM内存里来。

简单概括一下:
首先你的代码中包含“main()”方法的主类一定会在JVM进程启动之后被加载到内存,开始执行你的“main()”方法中 的代码
接着遇到你使用了别的类,比如“ReplicaManager”,此时就会从对应的“.class”字节码文件加载对应的类到内存里来。

1.3 从实用角度出发,来看看JVM的验证、准备和初始化的过程。

在这里插入图片描述

(1)验证阶段
简单来说,这一步就是根据Java虚拟机规范,校验加载进来的“.class”文件中的内容,是否符合指定的规范,后续才能交给JVM来运行。

(2)准备阶段
这个准备工作,其实就是给类分配一定的内存空间,然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值,比如上面的示例里,就会给“flushInterval”这个类变量分配内容空间,给一个“0”这个初始值。如下:
在这里插入图片描述
(3)解析阶段
这个阶段实际上是把符号引用替换为直接引用的过程,这个过程涉及到JVM的底层。

总结如下:
在这里插入图片描述

核心:初始化阶段

public class ReplicaManager {

    public int flushInterval= Configuration.getInt("replica.flush.interval");
    
}

在准备阶段,不会执行这个赋值逻辑,为“ReplicaManager”类分配好内存空间,给“flushInterval”类变量开辟一个内存空间,然后给个初始值“0”。
那么这段赋值的代码什么时候执行呢?答案是在“初始化”阶段来执行。
在这个阶段,就会执行类的初始化代码,比如上面的 Configuration.getInt(“replica.flush.interval”) 代码就会在这 里执行,完成一个配置项的读取,然后赋值给这个类变量“flushInterval”。

1.4 什么时候会初始化一个类?

一般来说有以下一些时机:比如“new ReplicaManager()”来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象出来;
或者是包含“main()”方法的主类,必须是立马初始化的。
此外,这里还有一个非常重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类。

类初始化的时机如下:
1.当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
2.当调用某个类的静态方法时
3.当使用某个类或接口的静态字段时
4.调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
5.当初始化某个子类时
6.当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
不满足上面6种情况,就不会做初始化。

1.5 类加载器和双亲委派机制

(1)启动类加载器
Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的。
所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。

(2)扩展类加载器
Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录,这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。

(3)应用程序类加载器
Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类
其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。

(4)自定义类加载器
除了上面那几种之外,还可以自定义类加载器,去根据需求加载类。

(5)双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
在这里插入图片描述
双亲委派的机制:
就是假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。

双亲委派的机制可以避免多层级的加载器结构重复加载某些类。

双亲委派模型设计的出发点,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚 拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
也就是说,判断2个类是否“相等”,只有在这2个类是由同一个类加载器加载的前提下才有意义,否则即使这2个类来源于 同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这2个类必定不相等。
基于双亲委派模型设计,那么Java中基础的类,Object类似Object类重复多次的问题就不会存在了,因为经过层层传递,加 载请求最终都会被Bootstrap ClassLoader所响应。加载的Object类也会只有一个,否则如果用户自己编写了一个 java.lang.Object类,并把它放到了ClassPath中,会出现很多个Object类,这样Java类型体系中最最基础的行为都无法保 证,应用程序也将一片混乱。

二、JVM 的内存区域划分

2.1 到底什么是JVM的内存区域划分?

JVM在运行代码时,使用多块内存空间的,不同的内存空间用来放不同的数据, 然后配合我们的代码流程,才能让我们的系统运行起来。

2.2 JVM中有哪些内存区域

1. 存放类的方法区

这个方法区是在JDK 1.8以前的版本里,代表JVM中的一块区域。主要是放从“.class”文件里加载进来的类,还会有一些类似常量池的东西放在这个区域里。

但是在JDK 1.8以后,这块区域的名字改了,叫做“Metaspace”,可以认为是“元数据空间”这样的意思。当然这里主要还是存放我们自己写的各种类相关的信息。

如上述例子中:
在这里插入图片描述

2. 执行代码指令用的程序计数器

我们写好的Java代码会被翻译成字节码,对应各种字节码指令。然后字节码指令一定会被一条一 条执行,这样才能实现我们写好的代码执行的效果。
所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令。
程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令。

JVM是支持多个线程的,当开启多个线程并发执行不同的代码,就会有多个线程来并发的执行不同的代码指令。
因此每个线程都会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了
在这里插入图片描述

3. Java虚拟机栈

方法里,我们经常会定义一些方法内的局部变量,因此,JVM必须有一块区域是来保存每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈。

每个线程都有自己的Java虚拟机栈,比如这里的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的 局部变量。
如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧,栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西。

JVM中的“Java虚拟机栈”这个组件的作用:调用执行任何方法时,都会给方法创建栈帧然后入栈。
在栈帧里存放了这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关的信息,方法执行完毕之后就出栈。

在这里插入图片描述

4. Java堆内存

是Java堆内存,这里就是存放我们在代码中创建的各种对象的。

相当于你可以认为局部变量表里的“replicaManager”指向了Java堆内存里的ReplicaManager对象

核心内存区域的全流程

在这里插入图片描述
在这里插入图片描述
首先,JVM进程会启动,就会先加载Kafka类到内存里。然后有一个main线程,开始执行你的Kafka中的main()方法。

main线程是关联了一个程序计数器的,那么他执行到哪一行指令,就会记录在这里.

其次,就是main线程在执行main()方法的时候,会在main线程关联的Java虚拟机栈里,压入一个main()方法的栈帧。
接着会发现需要创建一个ReplicaManager类的实例对象,此时会加载ReplicaManager类到内存里来。

然后会创建一个ReplicaManager的对象实例分配在Java堆内存里,并且在main()方法的栈帧里的局部变量表引入一个 “replicaManager”变量,让他引用ReplicaManager对象在Java堆内存中的地址。

接着,main线程开始执行ReplicaManager对象中的方法,会依次把自己执行到的方法对应的栈帧压入自己的Java虚拟机栈

执行完方法之后再把方法对应的栈帧从Java虚拟机栈里出栈。

其他内存区域

本地方法区:其实在JDK很多底层API里,比如IO相关的,NIO相关的,网络Socket相关的.
从内部的源码,会发现很多地方都不是Java代码了,而是走的native方法去调用本地操作系统里面的一些方法, 可能调用的都是c语言写的方法,或者一些底层类库
比如下面这样的:public native int hashCode();
在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种native方法的局部变量表之类的信息。

堆外内存:还有一个区域,是不属于JVM的,通过NIO中的allocateDirect这种API,可以在Java堆外分配内存空间。然后,通过Java虚拟机里DirectByteBuffer来引用和操作堆外内存空间。
其实很多技术都会用这种方式,因为有一些场景下,堆外内存分配可以提升性能。

思考题:我们创建的那些对象,到底在Java堆内存里会占用多少内存空间呢?

这个其实很简单,一个对象对内存空间的占用,大致分为两块:
一个是对象自己本身的一些信息
一个是对象的实例变量作为数据占用的空间
比如对象头,如果在64位的linux操作系统上,会占用16字节,然后如果你的实例对象内部有个int类型的实例变量,他会占用4个字节,如果是long类型的实例变量,会占用8个字节。如果是数组、Map之类的,那么就会占用更多的内存 了。
另外JVM对这块有很多优化的地方,比如补齐机制、指针压缩机制。

三、JVM的垃圾回收机制

1.一个方法执行完毕后会怎样?

一旦方法执行完毕,此时就会把该方法对应的栈帧从线程的 Java虚拟机栈里出栈,那么栈帧里的局部变量, 也就没有了。
也就是说,没有任何一个变量指向Java堆内存里的“ReplicaManager”实例对象了

我们在Java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限。

2. JVM的垃圾回收机制?

处理不需要使用的对象实例,JVM本身是有垃圾回收机制的,他是一个后台自动运行的线程
你只要启动一个JVM进程,他就会自带这么一个垃圾回收的后台线程。
这个线程会在后台不断检查JVM堆内存中的各个实例对象
在这里插入图片描述
如果某个实例对象没有任何一个方法的局部变量指向他,也没有任何一个类的静态变量,包括常量等地方在指向他。
那么这个垃圾回收线程,就会把这个没人指向的“ReplicaManager”实例对象给回收掉,从内存里清除掉,让他不再占用任何内存资源。
这样的话,这些不再被人指向的对象实例,即JVM中的“垃圾”,就会定期的被后台垃圾回收线程清理掉,不断释放内存资源。

我们在Java堆内存中分配的那些对象,到底会占用多少内存?一般怎么来计算和估算我们的系统创建的对象对内存占用的一个 压力呢?

Object Header(4字节) + Class Pointer(4字节)+ Fields(看存放类型),但是jvm内存占用是8的倍数,所以结果要向上取整到8的倍数。

Tomcat的类加载机制应该怎么设计,才能把我们动态部署进去的war包中的类,加载到Tomcat自身运行的 JVM中,然后去执行那些我们写好的代码呢?

首先Tomcat的 类加载器体系如下图所示,他是自定义了很多类加载器的。

在这里插入图片描述
Tomcat自定义了Common、Catalina、Shared等类加载器,其实就是用来加载Tomcat自己的一些核心基础类库的。

然后Tomcat为每个部署在里面的Web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类。至于Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器。
Tomcat是打破了双亲委派机制的
每个WebApp负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中的所有class文 件,不会传导给上层类加载器去加载。

tomcat需要破坏双亲委派模型的原因:
(1)tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;
(2)同一个第三方类库的相同版本在不同web应用可以共享
(3)tomcat自身依赖的类库需要与应用依赖的类库隔离 (3)jsp需要支持修改后不用重启tomcat即可生效 为了上面类加载隔离 和类更新不用重启,定制开发各种的类加载器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值