栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JVM整体讲解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JVM整体讲解

1.类加载 1.1 先来搞清楚几个概念
  • 编译

    我们写好".java"文件之后,要打包成".jar"或者".war"文件放到服务器去部署。

    这里非常关键的一步就是编译,就是把我们的".java"文件编译成".class"的字节码文件,这样才可以被运行起来。

  • 类加载器

    编译好的".class"字节码文件在哪里可以运行呢?

    java -jar xxx.jar的时候其实启动了JVM进程,它来负责运行我们编译好的".class"文件。

    此时,类加载器就会把编译好的".class"字节码文件加载到JVM中,以供后续代码的运行。

  • 字节码执行引擎

    JVM基于自己的字节码执行引擎,来执行加载到内存里的字节码文件。

    比如:代码中的"main()"方法,JVM就会从这个"main()"开始执行里面的代码,需要哪个类的时候就会用类加载器加载对应的类。

    所以springboot启动的时候会把main方法中的SpringApplication类加载进来,也就是把所有被spring容器管理的类全部加载进来,至于其他没有被spring管理的类,此时不会被加载,在用到的时候才会被加载。

1.2 JVM加载类的过程

  • 加载

    上面提到类在被使用的时候会被字节码执行引擎加载到JVM中,这是第一阶段。

  • 连接–验证

    简单来说,这一步是根据java虚拟机规范,来校验加载进来的".class"文件中的内容,是否符合规范。

  • 连接–准备

    这个准备阶段,其实就是给加载进来的类对象以及类变量分配一定的空间,然后再给一个默认的初始值。

  • 连接–解析

    这个阶段就是把符号引用替换为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)。

  • 初始化

    为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。

  • 使用

    主动使用

    JVM必须在每个类“首次 主动使用”的时候,才会初始化这些类。
    (1) 创建类的实例
    (2) 读写某个类或者接口的静态变量
    (3) 调用类的静态方法
    (4) 同过反射的API(Class.forName())获取类
    (5) 初始化一个类的子类
    (6) JVM启动的时候,被标明启动类的类(包含Main方法的类)
    只有当程序使用的静态变量或者静态方法确实在该类中定义时,该可以认为是对该类或者接口的主动使用。

    **被动使用:**除了主动使用的6种情况,其他情况都是被动使用,都不会导致类的初始化。

    JVM规范允许类加载器在预料某个类将要被使用的时候,就预先加载它。如果该class文件缺失或者存在错误,则在程序“首次 主动使用”的时候,才报告这个错误。(linkage Error错误)。如果这个类一直没有被程序“主动使用”,就不会报错。

    类加载机制与接口:
    (1) 当Java虚拟机初始化一个类时,不会初始化该类实现的接口。
    (2) 在初始化一个接口时,不会初始化这个接口父接口。
    (3) 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。

  • 卸载

    (1)有JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载 器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的。
    (2) 由用户自定义类加载器加载的类,是可以被卸载的。

1.3 类加载机制
  • 双亲委派机制

    当一个类加载器收到类加载请求的时候,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。

    所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。

    当当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。

    (1) 当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。

    (2) 所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。

  • 类加载器

    JVM自带的类加载器(3种)

    (1)根类加载器(Bootstrap):
    a、C++编写的,程序员无法在程序中获取该类
    b、负责加载虚拟机的核心库,lib目录下,比如java.lang.Object
    c、没有继承ClassLoader类

    (2)扩展类加载器(Extension):
    a、Java编写的,从指定目录中加载类库
    b、父加载器是根类加载器
    c、是ClassLoader的子类
    d、如果用户把创建的jar文件放到指定目录中lib/ext,也会被扩展加载器加载。
    (3)系统加载器(System)或者应用加载器(App):
    a、Java编写的
    b、父加载器是扩展类加载器
    c、从环境变量或者class.path中加载类
    d、是用户自定义类加载的默认父加载器
    e、是ClassLoader的子类

    用户自定义的类加载器
    (1)Java.lang.ClassLoader类的子类
    (2)用户可以定制类的加载方式
    (3)父类加载器是系统加载器
    (4)编写步骤:
    a、继承ClassLoader
    b、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化 为Class对象

2.JVM运行时数据区

Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、创建和销毁的时间。有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束来建立和销毁。Java虚拟机所管理的内存包括以下几个运行时数据区域,如图:

  • 程序计数器:

    指向当前线程正在执行的字节码指令。线程私有。

  • 虚拟机栈:

    虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
    (1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
    a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。
    b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。
    c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
    d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
    (2)线程私有

  • 本地方法栈:

    调用本地native的内存模型,线程私有

  • 方法区:

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,线程共享

    运行时常量池:

    a、是方法区的一部分
    b、存放编译期生成的各种字面量和符号引用
    c、Class文件中除了存有类的版本、字段、方法、接口等描述信息,还有一项是常量池,存有这个类的 编译期生成的各种字面量和符号引用,这部分内容将在类加载后,存放到方法区的运行时常量池中。

  • 堆(Heap):

    (1)Java堆是虚拟机管理的内存中最大的一块
    (2)Java堆是所有线程共享的区域
    (3)在虚拟机启动时创建
    (4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组
    (5)Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”

3.JMM java内存模型

1、 Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。
2、 主要目的是定义程序中各个变量的访问规则。
3、 Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。
(1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。
(2) 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。
(3) 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。

4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制。
(1)JMM决定一个线程对变量的写入何时对另一个线程可见。
(2)线程之间共享变量存储在主内存中
(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。
(4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
5、可见性、有序性:
(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。
(2)保证线程的有序执行,这个为有序性。(保证线程安全)
6、内存间交互操作:
(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。
(4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。
(6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。
(7)store(存储):把工作内存的变量的值传递给主内存
(8)write(写入):把store操作的值入到主内存的变量中

4.堆内存划分

Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用metaSpace代替。
1、新生代:
(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1
(3)内存不足时发生Minor GC
2、老年代:
(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
**3、Perm:**用来存储类的元数据,也就是方法区。
(1)Perm的废除:在jdk1.8中,Perm被替换成metaSpace,metaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。
(2)metaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
4、堆内存的划分在JVM里面的示意图:

5.GC回收
  • 判定对象是否要回收:

**1、可达性分析法:**通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)
2、 以下对象会被认为是root对象:
(1) 虚拟机栈(栈帧中本地变量表)中引用的对象
(2) 方法区中静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中Native方法引用的对象
3、 对象被判定可被回收,需要经历两个阶段:
(1) 第一个阶段是可达性分析,分析该对象是否可达
(2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
4、 方法区中的垃圾回收:
(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
(2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:
A、 该类的所有实例被回收
B、 加载该类的ClassLoader被回收
C、 该类的Class对象没有被引用

  • 常见的垃圾回收算法:

1、Mark-Sweep(标记-清除算法):

(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
(2)优缺点:实现简单,容易产生内存碎片

2、Copying(复制清除算法):

(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。

3、Mark-Compact(标记-整理算法):

(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):

思想:把堆分成新生代和老年代。(永久代指的是方法区)

(1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。
(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/354417.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号