JVM实战(一):Java代码到底是如何运行起来的

目录

1、Java代码到底是如何运行起来的

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

3、类加载整个流程

4、类加载器和双亲委派机制

5、JVM中有哪些内存区域,分别都是用来干嘛?

1、存放类的方法区

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

3、Java虚拟机栈

4、Java堆内存

5、核心内存区域的全流程串讲

6、其他内存区域

问题1:Tomcat这种Web容器中的类加载器应该如何设计实现?

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

6、JVM的垃圾回收机制是用来干嘛的?为什么要垃圾回收?

6.1、一个方法执行完毕之后会怎么样?

6.2、我们创建的Java对象其实都是占用内存资源的

6.3、不再需要的那些对象应该怎么处理?

7、问题汇总


1、Java代码到底是如何运行起来的

        首先写好一份Java代码,会包含很多的“.java”为后缀的代码文件。 比如OrderService.java,CustomerManager.java,User.java。
当我们写好这些“.java”后缀的代码文件之后,接下来如何部署到线上的机器上去运行?
  •    一般来说,都是把代码给打成“.jar”后缀的jar包,或者是“.war”后缀的war包
  •    然后把打包好的jar包或者是war包给放到线上机器去部署
部署就有很多种途径:
  • 通过Tomcat这类容器来部署代码。
  • 可以是你自己手动通过“java”命令来运行一个jar包中的代码。

编译” :在我们写好的“.java”代码打包的过程中,一般就会把代码编译成“.class”后缀的字节码文件,比如“User.class”,“Hello.class”,”Customer.class“。 然后这个“.class”后缀的字节码文件,他才是可以被运行起来的!

 对于编译好的这些“.class”字节码,是怎么让他们运行起来的呢?

        需要使用诸如 “java -jar”之类的命令来运行我们写好 的代码了。此时一旦你 采用“java”命令,实际上此时就会启动一个JVM进程
         JVM就会来负责运行这些“.class”字节码文件,也就相当于是负责运行我们写好的系统
所以平时我们写好的某个系统在一台机器上部署的时候,你一旦启动这个系统,其实就是启动了一个JVM,由它来负责运行这台机器上运行的这个系统。

        接着,JVM要运行这些“.class”字节码文件中的代码,首先得把这些“.class”文件中包含的各种类给加载进来。
         “类加载器”: 采用类加载器把编 译好的那些“.class”字节码文件给加载到JVM中 ,然后供后续代码运行来使用。 
        最后一步,JVM就会基于自 己的字节码执行引擎,来执行加载到内存里的我们写好的那些类了 。 比如你的代码中有一个“main()”方法,那么JVM就会从这个“main()”方法开始执行里面的代码。 他 需要哪个类的时候,就会使用类加载器来加载对应的类,反正对应的类就在“.class”文件中

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

        我们首先从“.java”代码文件,编译成“.class”字节码文件然后类加载器把“.class”字节码文件中的类给加载到JVM中。接着是JVM来执行我们写好的那些类中的代码,整体是这么个顺序。
一个类从加载到使用,一般会经历下面的这个过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
JVM在执行我们写好的代码的过程中,一般在什么情况下会去加载一个类呢? 也就是说,啥时候会从“.class”字节码文件中加载这个类到JVM内存里来。
就是在你的代码中用到这个类的时候
比如下面你有一个类(Kafka.class),里面有一个“main()”方法作为主入口。 那么 一旦你的JVM进程启动之后,它一定会先把你的这个类(Kafka.cass)加载到内存里, 然后从“main()”方法的入口代码开始执行。

接着假设上面的代码中,出现了如下的这么一行代码:

        代码中明显需要使 用“ReplicaManager”这个类去实例化一个对象,此时必须得把“ReplicaManager.class”字节码文件中的这个类加载到内存 里来啊
        所以这时就会 触发JVM通过类加载器,从“ReplicaManager.class”字节码文件中加载对应的类到内存里来使 用,这样代码才能跑起来。

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

3、类加载整个流程

(1)验证阶段
        根据Java虚拟机规范,来校验加载进来的“.class”文件中的内容,是否符合指定的规范。 假如说,你的“.class”文件被人篡改了,里面的字节码压根儿不符合规范,那么JVM是没法去执行这个字节码的!
        所以把“.class”加载到内存里之后,必须先验证一下,校验他必须完全符合JVM规范,后续才能交给JVM来运行。

(2)准备阶段
        我们写好的那些类,其实都有一些类变量比如下面的这个“ReplicaManager”类:

        假设你有这么一个“ReplicaManager”类,他的“ReplicaManager.class”文件内容刚刚被加载到内存之后,会进行验证,确认这个字节码文件的内容是规范的

        接着就会进行准备工作。
        这个准备工作,其实就 是给这个“ReplicaManager”类分配一定的内存空间 。然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始
        比如上面的示例里,就会给“flushInterval”这个类变量分配内容空间,给一个“0”这个初始值。

 (3)解析阶段

         符号引用替换为直接引用的过程

(4)三个阶段的小结
        其实这三个阶段里, 最核心的大家务必关注的,就是“准备阶段”
        因为这个阶段是给加载进来的类分配好了内存空间,类变量也分配好了内存空间,并且给了默认的初始值。
4、核心阶段:初始化
在初始化阶段,就会正式执行我们的类初始化的 代码了。
那么什么是类初始化的代码呢?

         对于“flushInterval”这个类变量,我们是打算通过Configuration.getInt("replica.flush.interval")这段代码来获取一个值,并且赋值给他的

但是在准备阶段会执行这个赋值逻辑吗?
        NO!在准备阶段,仅仅是给“flushInterval”类变量开辟一个内存空间,然后给个初始值“0”罢了。
        在“初始化” 阶段来执行。在这个阶段,就会执 行类的初始化代码,比如上面的 Configuration.getInt("replica.flush.interval") 代码就会在这里执行,完成一个配置项的读取,然后赋值给这个类变量“flus hInterval”。
        另外比如下图的 static静态代码块 ,也会在这个阶段来执行。
        类似下面的代码语义,可以理解为类初始化的时候,调用“loadReplicaFromDish()”方法从磁盘中加载数据副本,并且放在静态变量“replicas”中:

看看类的初始化的规则了。什么时候会初始化一个类?

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

        如果你要“new ReplicaManager()”初始化这个类的实例,那么会加载这个类,然后初始化这个类但是初始化这个类之前,发现AbstractDataManager作为父类还没加载和初始化,那么必须先加载这个父类,并且初始化这个父类。这个规则,大家必须得牢记。

4、类加载器和双亲委派机制

        实现上述过程,那必须是依靠类加载器来实现的
        那么Java里有哪些类加载器呢?简单来说有下面几种:
(1)启动类加载器
        Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的
相信大家都知道,如果你要在一个机器上运行自己写好的Java系统,无论是windows笔记本,还是linux服务器,是不是都得装一下JDK? 那么在你的Java安装目录下,就有一个“lib ”目录,这里就有Java最核心的一些类库,支撑你的Java系统的 运行。
所以 一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库
(2)扩展类加载器
        Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录。这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。
那么你的JVM一旦启动,是不是也得从Java安装目录下,加载这个“lib\ext”目录中的类?
(3)应用程序类加载器
        Application ClassLoader,这类加载器 就负责去加载“ClassPath”环境变量所指定的路径 中的类 。其实你大致就理解为 去加载你写好的Java代码吧,这个类加载器就 负责加载你写好的那些类到内存里。
(4)自定义类加载器
        除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。
(5)双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一 层是自定义类加载器:

基于这个亲子层级结构,就有一个双亲委派的机制什么意思呢?

  •  假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载 。
  •   如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
    • 比如你的JVM现在需要加载“ReplicaManager”类,此时应用程序类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到个类吗?
    • 然后扩展类加载器直接问自己的爸爸,启动类加载器,你能加载到这个类吗?
    • 启动类加载器心想,我在Java安装目录下,没找到这个类啊,自己找去
    • 然后,就下推加载权利给扩展类加载器这个儿子,结果扩展类加载器找了半天,也没找到自己负责的目录中有这个类。这时他很生气,说:明明就是你应用程序加载器自己负责的,你自己找去。
    • 然后应用程序类加载器在自己负责的范围内,比如就是你写好的那个系统打包成的jar包吧,一下子发现,就在这里! 然后就自己把这个类加载到内存里去了。
        这就是所谓的 双亲委派模型:先找父亲去加载,不行的话再由儿子来加载 。 这样的话,可以避 免多层级的加载器结构重复加载某些 类。

问题:Tomcat本身就是用Java写的,他自己就是一个JVM。 我们写好的那些系统程序,说白了,就是一堆编译好的.class文件放入一个war包,然后在Tomcat中来运行的。 那么,Tomcat的类加载机制应该怎么设计,才能把我们动态部署进去的war包中的类,加载到Tomcat自身运行的
JVM中,然后去执行那些我们写好的代码呢?

5、JVM中有哪些内存区域,分别都是用来干嘛?

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

我们的代码运行起来时,是不是需要执行我们写的一个一个的方法?那么运行方法的时候,方法里面有很多变量之类的东西,是不是需要放在某个内存区域里?接着如果我们写的代码里创建一些对象,这些对象是不是也需要内存空间来存放?

1、存放类的方法区

        方法区是在JDK 1.8以前的版本里,代表JVM中的一块区域。主要是放从“.class”文件里加载进来的类,还会有一些类似常量池的东西放在这个区域里。
        在JDK 1.8以后,这块 区域的名字叫做“Metaspace”,可以认为是“元数据空间”: 存放我们自己写的各种类相关的信息。
        举个例子,假设我们有一个“Kafka.class”类和“ReplicaManager.class”类,类似下面的代码。

这两个类加载到JVM后,就会放在这个方法区中,大家看下图:

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

        首先会存在于“.java”后缀的文件里,这个文件就是java源代码文件。通过编译器,把“.java”后缀的源代码文件编译为“.class”后缀的字节码文件。这个“.class”后缀的字节码文件里,存放的就是对你写出来的代码编译好的字节码了。 字节码才是计算器可以理解的一种语言,而不是我们写出来的那一堆代码。 字节码看起来大概是下面这样的:

这段字节码就是让大家知道“.java”翻译成的“.class”是大概什么样子的。
比如“0: aload_0”这样的,就是“字节码指令”,他对应了一条一条的机器指令,计算机只有读到这种机器码指令,才知道具体应该要干什么。
        比如字节码指令可能会让计算机从内存里读取某个数据,或者把某个数据写入到内存里去,都有可能,各种各样的指令就会指示计算机去干各种各样的事情。
我们写好的Java代码会被翻译成字节码,对应各种字节码指令。
现在Java代码通过JVM跑起来的第一件事 情就明确了, 首先Java代码被编译成字节码指令,然后字节码指令一定会被一条一条执行,这样才能实现我们写好的代码执 行的效果。
        所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎 ,去执行我们写的代码编译出来的代码指令,如下图。

        那么在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器

这个 程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指 令:

        JVM是支持多个线程的,所以其实你写好的代码可能会开启多个线程并发执行不同的代码,所以就会有多个线程来并发的执行不同的代码指令。

        因此每个线程都 会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了

3、Java虚拟机栈

        Java代码 在执行的时候,一定是线程来执行某个方法 中的代码。哪怕就是下面的代码,也会有一个main线程来执行main()方法里的代码 。在main线程执行main()方法的代码指令的时候,就会通过main线程对应的程序计数器记录自己执行的指令位置。

       

         在方法里,我们经常会定义一些方法内的局部变量。比如在上面的main()方法里,其实就有一个“replicaManager”局部变量,他是引用一个ReplicaManager实例对象的,关于这个对象我们先别去管他,先来看方法和局部变量。

        因此,JVM必须有一块区域是来保存 每个方法内的局部变量等数据的,这个区域就是Java虚拟机栈,每个线程都有自己的Java虚拟机栈 ,比如这里的m ain线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量。
        如果线程执行了一个方法,就会对这个方法调用创建对应的一个 栈帧 。 栈帧里就有这个方法的 局部变量表 、操作数栈、动态链接、方法出口等 东西。
        局部变量:比如main线程执行了main()方法,那么就会给这个main()方法创建一个栈帧,压入main线程的Java虚拟机栈,同时在main()方法的栈帧里,会存放对应的“replicaManager”局部变量

        然后假设main线程继续执行ReplicaManager对象里的方法,比如下面这样,就在“loadReplicasFromDisk”方法里定义了一个局部变量:“hasFinishedLoad”

        那么main线程在执行上面的“loadReplicasFromDisk”方法时,就会为“loadReplicasFromDisk”方法创建一个栈帧压入线程自己的Java虚拟机栈里面去。 然后在栈帧的局部变量表里就会有“hasFinishedLoad”这个局部变量:

        接着如果“loadReplicasFromDisk”方法调用了另外一个“isLocalDataCorrupt()”方法 ,这个方法里也有自己的局部变量。比如下面这样的代码:

        这个时候会给“isLocalDataCorrupt”方法又创建一个栈帧,压入线程的Java虚拟机栈里。 而且“isLocalDataCorrupt”方法的栈帧的局部变量表里会有一个“isCorrupt”变量,这是“isLocalDataCorrupt”方法的局部变量

        接着如果“isLocalDataCorrupt”方法执行完毕了,就会把“isLocalDataCorrupt”方法对应的栈帧从Java虚拟机栈里给出栈 。然后如果“loadReplicasFromDisk”方法也执行完毕了,就会把“loadReplicasFromDisk”方法也从Java虚拟机栈里出栈。

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

4、Java堆内存

        main线程执行main()方法的时候,会有自己的程序计数器。此外,还会依次把main()方法,loadReplicasFromDisk()方法,isLocalDataCorrupt()方法的栈帧压入Java虚拟机栈,存放每个方法的局部变量。
        Java堆内存: 存放我们在代码中创建的各种对象

        上面的“new ReplicaManager()”这个代码就是创建了一个ReplicaManager类的对象实例,这个对象实例里面会包含一些数据,这个“ReplicaManager”类里的“replicaCount”就是属于这个对象实例的一个数据。类似ReplicaManager这样的对象实例,就会存放在Java堆内存里。

        Java堆内存区域里会放入类似ReplicaManager的对象,然后我们因为在main方法里创建了ReplicaManager对象的,那么在线程执行main方法代码的时候,就会在main方法对应的栈帧的局部变量表里,让一个引用类型的“replicaManager”局部变量来存放ReplicaManager对象的地址

        局 部变量表里的“replicaManager”指向了Java堆内存里的ReplicaManager 对象

5、核心内存区域的全流程串讲

 

  • 首先,你的JVM进程会启动,就会先加载你的Kafka类到内存里。然后有一个main线程,开始执行你的Kafka中的main()方法。 main线程是关联了一个程序计数器的,那么他执行到哪一行指令,就会记录在这里大家结合上图中的程序计数器来理解一下。
  • 其次,就是main线程在执行main()方法的时候,会在main线程关联的Java虚拟机栈里,压入一个main()方法的栈帧。
  • 接着会发现需要创建一个ReplicaManager类的实例对象,此时会加载ReplicaManager类到内存里来。
  • 然后会创建一个ReplicaManager的对象实例分配在Java堆内存里,并且在main()方法的栈帧里的局部变量表引入一个 “replicaManager”变量,让他引用ReplicaManager对象在Java堆内存中的地址。
    接着,main线程开始执行ReplicaManager对象中的方法,会依次把自己执行到的方法对应的栈帧压入自己的Java虚拟机栈,执行完方法之后再把方法对应的栈帧从Java虚拟机栈里出栈。

6、其他内存区域

         native方法去调用本地操作系统里面的一些方法,可能调用的都是c语言写的方法,或者一些底层类库。比如下面这样的:public native int hashCode();
        在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种native方法的局部变量表之类的信息。
还有一个区域,是不属于JVM的,通 过NIO中的allocateDirect这种API,可以在Java堆外分配内存空 间。然后,通过Java虚拟机里的DirectByteBuffer来引用和操作堆外内存空间。
其实很多技术都会用这种方式,因为有一些场景下,堆外内存分配可以提升性能。

问题1:Tomcat这种Web容器中的类加载器应该如何设计实现?

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

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

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

一个对象对内存空间的占用,大致分为两块:

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

6、JVM的垃圾回收机制是用来干嘛的?为什么要垃圾回收?

6.1、一个方法执行完毕之后会怎么样?

        一旦方法里的代码执行完毕,那么方法就执行完毕了,也就是说loadReplicasFromDisk()方法就执行完毕了。

        一旦你的loadReplicasFromDisk()方法执行完毕,此时就会把loadReplicasFromDisk()方法对应的栈帧从main线程的Java虚拟机栈里出栈

此时一旦loadReplicasFromDisk()方法的栈帧出栈,那么大家会发现那个栈帧里的局部变量,“replicaManager”也就没有了。 也就是说,没有任何一个变量指向Java堆内存里的“ReplicaManager”实例对象了。

6.2、我们创建的Java对象其实都是占用内存资源的

        Java堆内存里的那个“ReplicaManager”实例对象已经没有人引用他了。这个对象实际上已经没用了,内存资源是有限的。 一般来说,我们会在一台机器上启动一个Java系统,机器的内存资源是有限的,比如就4个G的内存 。然后我们启动的Java系统本质就是一个JVM进程,他负责运行我们的系统的代码。
        那么这 个JVM进程本身也是会占用机器上的部分内存资源,比如占用2G的内存资 源。
那么我们在JVM的Java堆内存 中创建的对象,其实本质也是会占用JVM的内存资源的,比如“ReplicaManager”实例对象,会占用500字节的内 存。
我们在Java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限。

6.3、不再需要的那些对象应该怎么处理?

        既然“ReplicaManager”对象实例是不需要使用的,已经没有任何方法的局部变量在引用这个实例对象了,而且他还空占着内存资源,那么 我们应该怎么处理呢?
JVM的垃圾回收机制
JVM本身是有 垃圾回收机制的,他是一个后台自动运行的线程。 只要启动一个JVM进程,他就会自带这么一个垃圾回收的后台线程 。 这个线程会在后台不断检查JVM堆内存中的各个实例对象:

        如果某个实例对象没有任何一个方法的局部变量指向他,也没有任何一个类的静态变量,包括常量等地方在指向他。那么这个垃圾回收线程,就会把这个没人指向的“ReplicaManager”实例对象给回收掉,从内存里清除掉,让他不再占用任何内存资源。这样的话,这些不再被人指向的对象实例,即JVM中的“垃圾”,就会定期的被后台垃圾回收线程清理掉,不断释放内存资源: 

7、问题汇总

1、新建的实例在堆内存,实例变量也是在堆内存

2、类加载是按需加载,可以一次性加载全部的类吗?

答: 如果是默认的类加载机制,那么是你的代码运行过程中,遇到什么类加载什么类。如果你要自己加载类,那么需要写自己的类加载器。

3、还是没有明白 jvm和平时运行在机器上的系统之间是什么关系呢

答: 其实很简单,你运行在机器上的系统,其实就是一个JVM进程,JVM进程会执行你系统里写好的那些代码

好文:

JVM知识点 | ProcessOn免费在线作图,在线流程图,在线思维导图

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值