JVM篇

1.Java文件从编译到执行的过程

简单总结来说,我认为就4个步骤:编译->加载->解释->执行

  • 编译:将源码文件编译成JVM可以解释的class文件。编译过程中会对源代码程序做语法分析、语义分析、注解处理等操作,最后才生成字节码文件。
  • 加载:将编译后的class文件加载到JVM中。在加载阶段有可以细化为几个步骤:装载->连接->初始化
    • 装载:为了节省内存的开销哦,并不会一次性把所有的类都装载进JVM,而是等到有需要的时候才进行装载。(比如new和反射的方式);class类时通过类加载器加载进JVM中,为了放置内存中出现多分同样的字节码,使用了双亲委派机制(它不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上);JDK中的本地方法一般有根加载器(Bootstrp loader)装载,JDK中内部实现的扩展类一般由扩展加载器(ExtClassLoader)装载,而程序中的类文件则由系统加载器(AppClassLoader)实现装载。可以总结为,查找并加载类的二进制数据,在JVM堆中创建一个类的对象,并将类相关信息存储在JVM方法区中。

      通过装载这个步骤,现在已经把class文件装载到JVM中,并创建出对应的class对象以及类信息存储至方法区中了。

    •  

      连接:这个阶段可以总结为,对class的信息进行验证、为类变量分配内存空间并对其赋默认值。由可以细化为几个步骤:验证->准备->解析。
      • 验证:验证类是否符合Java规范和JVM规范。
      • 准备:为类的静态变量分配内存,初始化为系统的初始值。
      • 解析:将符号引用转为直接引用的过程。
    • 初始化:这个阶段可以理解为为类的静态变量赋予正确的初始值。过程大概就是收集class的静态变量、静态代码块、静态方法等等,随后从上往下开始执行
  • 解释:这个阶段可以理解为:把字节码转换成操作系统可以识别的指令。在解释阶段会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器(JIT)。JVM会对热点代码(即调用频繁的代码)做编译,非热点代码直接进行解释。使用热点探测来检测是否为热点代码,一把有两种方式:计数器和抽样。HotSPot使用的是计数器,为每个方法准备了两类计数器:方法调用计数器和回边计数器,这两个计数器都有个确定的阙值,超过就会出发JIT编译。
  • 执行:操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令

2.双亲委派机制

为了防止内存中存在多分同样的字节码,使用了双亲委派机制。JDK中核心的类库一般由根加载器(Bootstrp loader)装载,jre目录下的一些扩展jar通常用扩展加载器(ExtClassLoader)装载,应用程序中的主函数类一般由系统加载器(AppClassLoader)装载。

2.1.如何打破双亲委派机制?

自定义ClassLoader,重写loaderClass方法(不依照往上寻找类加载器),那么就算打破双亲委派机制。

Tomcat就是一个常用的场景。初学时部署项目时,我们是把war包放到tomcat的webapp下即可,这意味着一个tomcat可以运行多个web应用程序。假设有两个web应用程序,它们都有一个类,并且类全限定名都一样,但是具体实现时不一样的。tomcat为了保证它们时不冲突的,给每个web应用创建一个类加载实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载应用目录下的类,如果当前找不到才一层层网上找。

3.JVM内存结构

JVM内存结构往往指的时JVM定义的运行时数据区域,简单来说分为5大块:线程共享:方法区、堆;线程私有:程序计数器、虚拟机栈、本地方法栈。

  • 程序计数器:Java是多线程语言,假设线程数大于CPU数时,就可能会发生线程切换现象,切换就意味着终端和恢复,那么就需要一块区域存放当前线程的执行情况。所以程序计数器就是用于记录各个线程执行的字节码地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)
  • 虚拟机栈:每个线程在创建的时候都会创建一个虚拟机栈,每个方法调用都会创建一个栈帧,每个栈帧会包含几块内容:局部变量表、操作数栈、动态链接、返回地址。它保存了局部变量 、部分变量的计算并参与了方法的调用和返回。
  • 本地方法栈:本地方法栈和虚拟机栈功能类似,不过本地方法栈用于管理本地方法的调用。一般本地方法指的是非Java方法
  • 方法区:jdk8已经用元空间来替代方法区,方法区主要用来存放已被虚拟机装载的类相关信息:类信息、常量池。类信息包括了类的版本、字段、方法、接口和父类等信息;常量池又分为静态常量池和运行时常量池,静态常量池主要存储字面量以及符号引用等信息,也包含字符串常量池;运行时常量池存储的时类加载时生成的直接引用等信息。元空间相较之前最大的区别就是存储不在虚拟机中,而是使用本地内存。常量池在jdk7之后又被转移到了堆内存进行存储。
  • :堆被划分为新生代和老年代,新生代又被进一步划分为Eden区和Survivor区,最后Survivor区由From Survivor区和To Survivor区组成。将堆内存划分为这几块区域主要跟内存回收有关,也就是垃圾回收机制有关。

4.垃圾回收机制

只要对象不再被使用,那我们就可以该对象就是垃圾,对象所占用的空间就可以被回收。

4.1.如何判断对象不在使用?

常用算法有两种:引用计数法和可达性分析法

  • 引用分析法:当引用对象被引用则+1,但对象引用失败则-1,当计数器为0时,说明对象不在被应用,可以被回收。缺点就是如果对象存在循环依赖,那就无法定位该对象是否该被回收。
  • 可达性分析法:它从GC Roots开始向下搜索,当对象到GC Roots都没有任何引用相连时,说明对象时不可用的,可以被回收。其中GC Roots必须是一组比较活跃的引用,从GC Roots出发,程序直接或间接引用能够找到可能正在被使用的对象。

举例说明:

虚拟机栈内的栈帧的局部变量表中存储着引用,如果栈帧位于虚拟机栈的栈顶,就一颗说明这个栈帧是活跃的,换句话说,就是线程正在被调用的,那么栈帧中指向堆的对象引用是不是就是活跃的。那么从当前活跃的栈帧指向堆里的对象引用就可以是GC Roots。当然也不单单这么一小块,比如类的静态变量的引用也可以是GC Roots等等。

4.2.垃圾回收

垃圾回收第一步就是标记,标记那些没有被GC Roots引用的对象,标记完我们就可以选择直接清除不被GC Roots关联的,但是标记完直接清除存在很明显的问题,直接清除会有内存碎片的问题,因为清除的垃圾有可能空间是不连续的,那么我们可以把存活的对象移到一边,垃圾放到一边再进行清除,这样就没有内存碎片了,我们称之为整理。这里我们简单讲述下分代垃圾回收的过程。

分代回收器有两个分区:老年代和新生代。新生代默认的空间占比是总空间的1/3,老年代默认占比是2/3.

新生代使用的是复制算法。新生代里有3个区Eden、To Survivor和From Survivor,它们的默认占比是8:1:1,执行流程如下:

当年轻代的Eden区分配满的时候,就会触发年轻代的GC--Minor GC

  • 在Eden区执行了第一次GC后,存活的对象会被移动到To Survivor区,年龄+1
  • 交换From Survivor区和To Survivor区的指针。
  • 继续产生对象,存入Eden区,内存不够进行第二次Minor GC,清除Eden区和From Survivor区的垃圾,并将From Survivor区和Eden区存活的移动到To Survivor区,年龄+1
  • 重复步骤2,3,直到年龄达到一定的阙值,就把这个对象晋升到老年代,当经过一段时间,老年代内存不足,会进行一次Full GC,回收所有对象。

 

posted @ 2023-04-12 17:19  XIAOBAI001  阅读(28)  评论(0)    收藏  举报