JAVA架构师系列-JVM原理(一)

JVM概念

  • jvm是java的核心和基础,是java编译器和OS平台之间的虚拟处理器。java编译器只需要面向jvm,生成jvm能理解和执行的代码或字节码文件,然后通用jvm将每一条指令翻译成不同的平台机器码,通过特定的平台执行。

JVM生命周期

  1. jvm的诞生:启动一个java程序的时候,就会产生一个jvm实例,任何一个public static void main(String[] args)函数的class文件都可以作为jvm运行的起点。
  2. jvm实例的运行:main()作为程序初始线程的起点,任何其他线程都是由该线程启动。jvm内部有两种线程,分别是守护线程和非守护线程,main()属于守护线程,守护线程通用由jvm自己使用,java程序也可以标明自己创建的线程为守护线程。
  3. jvm实例的消亡:当所有非守护线程都终止时,jvm就会退出。当然也可以选择手动退出,比如使用java.lang.Runtime.getRuntime().exit()或java.lang.System.exit()来退出。

JVM体系结构

在这里插入图片描述

  • jvm将文件.class字节码加载到内存,然后对数据进行校验、转换和解析,并初始化,最终形成可以对虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

类加载器

类加载器的任务就是根据一个类的全限定名来读取类的二进制字节流到jvm中,然后转换为一个与目标类对应的java.lang.Class实例对象。

  • 类从加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。
    在这里插入图片描述
  1. 加载
    加载阶段是“类加载机制”中的一个阶段,虚拟机需要完成三件事:
    • 根据类的全限定名获取类的二进制字节流
    • 将这个字节流代表的静态存储结构转换成方法区的运行时数据结构----模板
    • 在java堆中为这个类生成一个java.lang.Class对象,作为方法区这些类的访问入口
  2. 验证
    验证是连接阶段的第一步,这一步是为了确保class文件的字节流中包含的信息是符合当前虚拟机的要求,并且不会危害到虚拟机自身安全。
    验证阶段主要包含四个验证过程:文件格式验证、元数据验证、字节码验证、符号引用验证。
  3. 准备
    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存将在方法区中进行分配。这个阶段有两个容易产品混淆的知识点,首先这个时候进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量,实例变量是在对象实例化的时候随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值。
    比如:public static int value = 100
    那么变量在准备阶段的初始值是0而不是12,将变量赋值为12是在初始化阶段才会执行。
  4. 解析
    解析阶段是虚拟机常量池内的将符号引用替换为直接引用的过程。
    解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
    • 符号引用:是一组符号用来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标对象即可。
    • 直接引用:是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标对象的句柄。
  5. 初始化
    类加载最后阶段,若类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(前面只初始化了默认的static的变量将在这个阶段进行赋值,成员变量也将被初始化)
在jvm中提供了4种类加载器
  1. 引导类加载器(Bootstrap ClassLoader)
  • 引导类加载器属于JVM的一部分,不由JVM生成,由C++代码实现,无法更新也无法继承。它不会被Java代码所直接使用和控制,开发者一般也无需考虑Bootstrap ClassLoader的细节。
  • Bootstrap ClassLoader是最顶层的类加载器。它的任务是加载JVM运行时所需的核心类和JVM参数,从JVM运行的根路径下的JRE/lib目录加载核心类库,这些核心类库是JVM运行时所必需的类,例如java.lang.String、java.lang.Object等。
  • JDK9之后,引导类加载器是在jvm内部和java类库共同协作实现的类加载器(不再是c++实现),但是为了与之前代码兼容,在java中仍然无法使用和控制。
  1. 扩展类加载器/平台类加载器(Extension ClassLoader/PlatformClassLoader)
  • 扩展类加载器,Java语言实现,由引导类加载器进行加载。 扩展类加载器用于加载JVM的扩展库($JAVA_HOME/jre/lib/ext/目录下的jar文件)。
  • JDK9之后,因为是基于模块化进行构建的,它将原来的rt.jar和tools.jar拆分成了很多的JMOD文件,文件中的Java类库已经天然地满足了可扩展的需求,所以扩展机制已经没有继续存在的价值了。扩展机制被移除,但是扩展类加载器由于向后兼容性的原因被保留,然后被重命名为平台类加载器Platform ClassLoader。
  1. 应用程序类加载器(Application ClassLoader)
    Application ClassLoader就是加载应用程序中的类的类加载器。它负责加载应用程序中的所有类文件,当其他类加载器无法加载时就会使用它加载。在JDK9之后还需要负责加载一些模块的类加载任务。
  2. 自定义类加载器(User ClassLoader)
    用户自己定义的类加载器。
类加载器的内部机制----双亲委派模式

在这里插入图片描述

  • JDK9前的双亲委派模式
    当某个类加载器需要加载.class文件时,它首先不会自己加载,而是把这个任务委派给父级加载器去完成,只有父级加载器没有加载到这个类的时候才会由子级加载器去加载。保证了数据的安全性和唯一性。

  • JDK9后的双亲委派模式
    JDK9之后为了模块化的支持,对双亲委派模式做了以下更改:

    1. 扩展类加载器被平台类加载器(Platform ClassLoader)取代。
      JDK 9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件),
      其中的 Java 类库就已天然地满足了可扩展的需求,那自然无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。
    2. 平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。
      现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。
    3. 启动类加载器现在是在 Java 虚拟机内部和 Java 类库共同协作实现的类加载器(以前是 C++实现)。
      为了与之前的代码保持兼容,所有在获取启动类加载器的场景(如 Object.class.getClassLoader)中仍然会返回 null 来代替,而不会得到 BootClassLoader 的实例。
    4. 类加载的委派关系也发生了变动。
      当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
    • 在java模块化系统明确规定了三个类加载器负责的加载模块:
      引导类加载器:

      java.base                        java.security.sasl
      java.datatransfer                java.xml
      java.desktop                     jdk.httpserver
      java.instrument                  jdk.internal.vm.ci
      java.logging                     jdk.management
      java.management                  jdk.management.agent
      java.management.rmi              jdk.naming.rmi
      java.naming                      jdk.net
      java.prefs                       jdk.sctp
      java.rmi                         jdk.unsupported
      

      平台类加载器:

      java.activation*                jdk.accessibility
      java.compiler*                  jdk.charsets
      java.corba*                     jdk.crypto.cryptoki
      java.scripting                  jdk.crypto.ec
      java.se                         jdk.dynalink
      java.se.ee                      jdk.incubator.httpclient
      java.security.jgss              jdk.internal.vm.compiler*
      java.smartcardio                jdk.jsobject
      java.sql                        jdk.localedata
      java.sql.rowset                 jdk.naming.dns
      java.transaction*               jdk.scripting.nashorn
      java.xml.bind*                  jdk.security.auth
      java.xml.crypto                 jdk.security.jgss
      java.xml.ws*                    jdk.xml.dom
      java.xml.ws.annotation*         jdk.zipfs
      

      应用类加载器:

      jdk.aot                         jdk.jdeps
      jdk.attach                      jdk.jdi
      jdk.compiler                    jdk.jdwp.agent
      jdk.editpad                     jdk.jlink
      jdk.hotspot.agent               jdk.jshell
      jdk.internal.ed                 jdk.jstatd
      jdk.internal.jvmstat            jdk.pack
      jdk.internal.le                 jdk.policytool
      jdk.internal.opt                jdk.rmic
      jdk.jartool                     jdk.scripting.nashorn.shell
      jdk.javadoc                     jdk.xml.bind*
      jdk.jcmd                        jdk.xml.ws*
      jdk.jconsole
      

运行时数据区

  • 方法区----线程共享
  • java堆----线程共享
  • java栈(虚拟机栈)----线程非共享
  • 程序计数器----线程非共享
  • 本地方法栈----线程非共享
  1. jvm在运行的时候会分配好方法区和java堆,jvm每遇到一个线程,就会为其分配一个程序计数器、本地方法栈、java栈,直到线程结束,才会将三者的内存释放掉。
  2. 程序计数器、java栈、本地方法栈的生命周期与所属线程的生命周期相同,线程终止就会进行释放,方法区和java堆的生命周期是跟着java程序的生命周期,因此gc的回收主要是针对线程共享区(方法区和java堆)进行的。
  • java堆
    • java中的堆是用来存储对象实例和数组(数组的引用是放在java栈中),堆是所有线程共享的,因此在其上进行对象内存分配的时候是需要加锁的,导致每次new对象的开销比较大。jvm只有一个堆,堆是java垃圾回收器管理的主要区域,java垃圾回收机制会自动进行处理。
    • 堆分为老年代和年轻代,新创建的对象会分配到年轻代,而老年代存放生命周期比较长久的对象。
    • 年轻代又分为一个Eden区和两个Survivor区,新分配的对象会先放到Eden区,Survivor区是作为Eden区和Old区的缓冲,当在Survivor区的对象进行多次GC后还存在,则会被转移到Old区。当一个对象大于Eden区小于Old区的时候,会直接放到Old区,如果对象大于Old区,则会直接抛出OutOfMemoryError(OOM)
  • 方法区(永久代、元空间)
    • 在方法区中存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的变量、类中的Field信息、类中的方法信息以及编译器编译后的代码等。
    • 在JDK8之前,使用永久代来实现方法区,JDK8以后,永久代的概念被废弃了,改用在本地内存中实现的元空间来代替。好处是元空间会在运行的时候动态调整,只要没有超过当前进程可用的内存上限,就不会出现溢出的问题。
    • 方法区又称为非堆,因为在内存模型中它存储的数据和堆有些许不同。
  • java栈
    • java栈又称为虚拟机栈,也就是我们通常所说的栈。JVM栈是线程私有的,每个线程创建的时候都会创建自己的JVM栈。java栈是java方法执行的内存模型。
    • 栈帧是一个数据结构,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • java栈存储的是一个个栈帧,每个方法被执行的时候会创建一个栈帧,进入虚拟机栈,一个栈帧对应一个方法。
    • 每个方法结束对应着栈帧的出栈,入栈表示方法被调用,出栈表示执行完毕或返回异常。
    • 活动线程的虚拟机栈里最顶部的栈帧表示正在执行的方法,这个栈帧也叫做“当前栈帧”。
    • 同一时间、同一条线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,执行引擎所运行的字节码指令都是只针对当前栈帧进行操作。
  • 程序计数器
    • 程序计数器也称为PC寄存器,在jvm中,多线程是通过线程轮流切换来获取CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,为了能够使每个线程在线程切换之后还能恢复到原来的执行位置,每个线程都需要有自己独立的程序计数器,并且互不干扰,否则就会影响到程序的正常执行次序。所以程序计数器是每个线程私有的。
    • 在jvm规范中规定,如果线程执行的是非native(本地)方法,则程序计数器保存的是当前需要执行指令的地址,如果线程执行的是native方法,则程序计数器中的值是undefined。
    • 由于程序计数器存储的数据不会随着程序的执行而变化,因此程序计数器不会发生内存溢出现象的(OutOfMemory)。
  • 本地方法栈
    • 本地方法栈和虚拟机栈非常类似,区别在于虚拟机栈执行的是java方法,本地方法栈执行的是native方法服务,存储的也是本地方法的局部变量表、本地方法的操作数栈等信息。
    • 本地方法栈是在程序调用或jvm调用本地方法接口的时候启用。
    • 本地方法都不是用java编写的,一般都是用C语言或其他语言编写的,本地方法也不由jvm去执行,因此本地方法的运行不受jvm管理;栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
    • 本地方法栈也会在深度溢出或扩展失败的时候,分别抛出StackOverflowError 和 OutOfMemoryError 异常。
    • 在HotSpot虚拟机中,直接将本地方法栈和java栈合二为一了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值