JVM总览

1 JVM的运行机制及构成

1.1 运行机制

  1. JVM是用于运行Java字节码的虚拟机,它运行在操作系统之上,不与硬件设备直接交互;
  2. Java源文件在通过编译器编译后生成相应的.class文件(字节码文件),.class文件又被JVM中的即时编译器和解释器编译成机器码在不同的平台(Windows、Linux、Mac)上运行;
  3. Java在不同平台上运行时不需要重新编译成.class文件(字节码文件)。JVM屏蔽了与具体平台相关的信息,任何平台只要装有针对于该平台的JVM,.class文件(字节码文件)就可以在该平台上运行,这就是“一次编译,多次运行”,即Java的跨平台特性;
  4. 在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据;

Java程序的运行过程

  1. Java源文件被编译器编译成字节码文件;
  2. JVM将字节码文件编译成相应操作系统的机器码;
  3. 机器码调用相应操作系统的本地方法库完成具体的指令操作;

1.2 JVM的构成

在这里插入图片描述

  1. 包括类加载器子系统、运行时数据区、执行引擎和本地接口库;
  2. 类加载器子系统用于将编译好的.class文件加载到JVM中;
  3. 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、虚拟机栈、虚拟机堆、方法区和本地方法区;
  4. 执行引擎包括即时编译器和垃圾回收器,即时编译器(配合解释器)用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象;
  5. 本地接口库用于调用操作系统的本地方法库完成具体的指令操作;

1.3 多线程

  1. JVM允许在一个进程内同时并发执行多个线程;
  2. JVM中的线程与操作系统中的线程是相互对应的,在JVM线程的本地存储、缓冲区分配、同步对象、栈、程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程;
  3. 操作系统负责调度所有线程,并为其分配CPU时间片,在原生线程初始化完毕时,就会调用Java线程的run()执行该线程;
  4. 在JVM线程运行结束时,会释放掉原生线程和Java线程所对应的资源;
  5. 在JVM后台运行的线程主要有:(1)虚拟机线程:在JVM到达安全点时出现;(2)周期性任务线程:通过定时器调度线程来实现周期性操作的执行;(3)GC线程:用来进行垃圾回收的线程;(4)即时编译器线程:即时编译器线程在运行时将字节码动态编译成本地平台机器码;
  6. 信号分发线程:接收发送到JVM的信号并调用JVM方法;

2 JVM的内存区域解析

2.0 概述

  1. JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存;
    在这里插入图片描述
  2. 线程私有区域的生命周期与线程相同,随线程启动而创建,随线程结束而销毁;线程共享区域的生命周期与虚拟机实例相同,随虚拟机实例启动而创建,随虚拟机实例关闭而销毁;
  3. 直接内存也称作堆外内存,它不是JVM运行时数据区的一部分,但在并发编程中被频繁使用;

2.1 程序计数器:线程私有,无内存溢出问题

  1. 当前线程所执行的字节码行号指示器(逻辑);
  2. 通过改变计数器的值来选取下一条需要执行的字节码指令;
  3. 线程私有;
  4. 对Java方法计数,如果是Native方法则计数器的值为Undefined;
  5. 只是计数,不会发生内存泄漏;

2.2 虚拟机栈:线程私有,描述Java方法的执行过程

  1. 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程;
  2. 在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态;
  3. 线程1在CPU1上运行,线程2在CPU2上运行,在CPU资源不够时其他线程将处于等待状态,等待获取CPU时间片;

在这里插入图片描述

2.3 本地方法区:线程私有

本地方法区和虚拟机栈作用类似,区别是虚拟机栈为执行Java方法服务,本地方法区为执行Native方法服务。

2.4 堆:线程共享,也称运行时数据区

  1. 堆线程共享,JVM运行过程中创建的对象和产生的数据都存储在堆中;
  2. 堆是垃圾回收的最主要区域;
  3. 由于现代JVM采用分代收集算法,因此堆从GC的角度还可以细分为:新生代和老年代;

2.5 方法区:线程共享

  1. 方法区也称为永久代,线程共享;
  2. 用于存储常量、静态变量、已加载的类信息、即时编译器编译后的机器码、运行时常量池等数据;

在这里插入图片描述

  1. JVM把GC分代收集扩展至方法区,即使用永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存;
  2. 方法区的垃圾回收主要针对常量池的回收和类的卸载,因此可回收的对象很少;
  3. 常量保存在运行时常量池中,在即时编译后,机器码的内容将在执行阶段(类加载完成后)也被保存在运行时常量池中;

3 JVM运行时内存解析

3.0 概述

  1. JVM的运行时内存包括JVM堆和方法区,从GC的角度可以将JVM堆分为新生代和老年代,方法区称为永久代;
  2. 新生代默认占堆空间1/3,老年代默认占堆空间2/3;
  3. 新生代分Eden区、ServivorFrom区和ServivorTo区,Eden默认占新生代8/10,ServivorFrom默认占新生代1/10,ServivorTo默认占新生代1/10;

3.1 新生代:Eden区、ServivorFrom区和ServivorTo区

  1. JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存;

  2. 由于JVM会频繁创建对象,因此新生代会频繁触发MinorGC进行垃圾回收;

  3. Eden区:Java新创建的对象首先存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过XX:PretenureSizeThreshold设置大小。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收;

  4. ServivorTo区:保留上一次MinorGC时的幸存者;

  5. ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者;

  6. 新生代的GC过程称作MinorGC,采用复制算法实现:

    1. 把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,同时年龄+1;当年龄达到老年代标准,则将其移动到老年代;如果ServivorTo区的内存空间不够,也直接将其移动到老年代;
    2. 清空Eden和ServivorFrom区中的对象;
    3. 将ServivorTo区和ServivorFrom区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区;

3.2 老年代

  1. 老年代主要存放有较长生命周期的对象和大对象;
  2. 老年代的GC过程称作MajorGC,在老年代,对象比较稳定,MajorGC不会被频繁触发;
  3. 在进行MajorGC之前,JVM会先进行一次MinorGC,使得有些新生代对象晋升入老年代,导致老年代空间不足时触发MajorGC;
  4. MajorGC采用标记清除算法,该算法首先扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间;
  5. 因为要先扫描标记再回收,所以MajorGC耗时较长;
  6. MajorGC的标记清除算法容易产生内存碎片;

3.3 永久代

  1. JVM把GC分代收集扩展至方法区,即使用永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存;
  2. 永久代的垃圾回收主要针对常量池的回收和类的卸载,因此可回收的对象很少;
  3. JDK1.8之后,永久代被元空间取代,但元空间位于本地内存中,而不是在虚拟机内存中;原来的永久代数据被划分到了元空间和堆中,元空间存储类的元信息,静态变量和常量池等放入堆中;

方法区是一个JVM规范,永久代和元空间都是其一种实现方式。

4 垃圾回收与算法

4.1 如何确定垃圾

4.1.1 引用计数法

  1. 在Java中,如果要操作对象,必须先获取该对象的引用因此可以通过引用计数法来判断一个对象是否可以被回收;
  2. 为对象添加一个引用时,引用计数+1;在为对象删除一个引用时,引用计数-1;如果一个对象的引用计数为0,表示该对象没有被引用,可以被回收;
  3. 引用计数法容易产生循环引用问题。循环引用指两个对象互相引用,导致它们的引用一直存在,而不能被回收;

在这里插入图片描述

  1. 正是因为循环引用的存在,因此Java虚拟机不使用引用计数算法;

4.1.2 可达性分析

  1. 为了解决引用计数法的循环引用问题,Java采用可达性分析来判断对象是否可以被回收;
  2. 可达性分析通过根搜索算法(GC Roots Tracing)来实现。根搜索算法以一系列GC Roots对象作为起点向下搜索,在一个对象到任何GC Roots都没有可达路径时,则称该对象是不可达的;
  3. 不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然不可达,则将被回收;
  4. 根搜索算法主要针对栈中的引用、方法区中的静态引用和JNI中的引用展开分析;

在这里插入图片描述

4.2 常用的垃圾回收算法

4.2.1 标记清除算法

  1. 分为标记和清除两个阶段;
  2. 标记阶段标记存活的对象,清除阶段清除未被标记的对象;

在这里插入图片描述

  1. 由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题;

4.2.2 复制算法

  1. 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后把这一块全部清理,下一次分配内存时使用另一块,如此循环往复;

在这里插入图片描述

  1. 复制算法内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用但内存空间被压缩到原来的一半,因此存在大量的资源浪费;
  2. 同时,在系统中有大量长时间存活的对象时,这些对象将在区域1和区域2之间来回复制,进而影响系统的运行效率;
  3. 因此,该算法适合区域中的对象为“朝生夕死”的状态,比如新生代的GC过程;

4.2.3 标记整理算法

  1. 标记整理算法结合了标记清除算法和复制算法的优点;
  2. 标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端对象并释放内存;
  3. 因此既解决了内存碎片化的问题又避免了内存空间利用不足的问题;

在这里插入图片描述

4.2.4 分代收集算法

  1. 无论是标记清除算法、复制算法还是标记整理算法,都不能做到适合所有类型对象的回收(短生命周期、长生命周期、小对象、大对象);
  2. 因此,针对不同对象类型,JVM采用了不同的垃圾回收算法,即分代收集算法;
  3. 分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆分为新生代和老年代;
  4. 新生代使用复制算法,老年代使用标记清除算法或标记整理算法;
  5. 新生代之所以采用复制算法,是因为在新生代每次进行垃圾回收时都有大量对象被回收,需要复制(存活的对象)很少,不存在大量对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量短生命周期的对象;
  6. 老年代主要存放生命周期较长的对象或大对象,每次只有少量对象被回收,因此在老年代采用标记清除算法或标记整理算法;

5 Java中的4种引用类型

  1. Java中对对象的操作是通过该对象的引用实现的;
  2. Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引用;

在这里插入图片描述

5.1 强引用

  1. Java中最常见的就是强引用,在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用;
  2. 被强引用关联的对象不会被回收,因此强引用是造成Java内存泄露的主要原因;

5.2 软引用

  1. 通过SoftReference类实现;
  2. 如果一个对象只有软引用,则在系统空间不足时该对象将被回收;

5.3 弱引用

  1. 通过WeakReference类实现;
  2. 如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收;

5.4 虚引用

  1. 通过PhantomReference类实现;
  2. 为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知,起哨兵作用;

6 JVM的类加载机制

6.1 类加载阶段

  1. 类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类,如果一次性加载,会占用很多内存;
  2. 类加载分为5个阶段:加载、验证、准备、解析、初始化;
  3. 在类初始化完成后,就可以使用该类的信息,在一个类不再被需要时可以从JVM中卸载;

在这里插入图片描述

6.1.1 加载

  1. 指JVM读取class文件,并根据class文件描述创建Class对象的过程;
  2. 步骤:(1)通过类的完全限定名称获取定义该类的二进制字节流;(2)将该字节流表示的静态存储结构转换为方法区的运行时存储结构;(3)在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口;
  3. 读取class文件既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过自动代理的方式获取;

6.1.2 验证

确保 class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

6.1.3 准备

  1. 为类变量分配内存并设置初始值,使用的是方法区的内存(实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中);
  2. 初始值指不同数据类型的默认值;
  3. static修饰和static final修饰不同:
    1. public static int value = 1000;value在准备阶段为0,将value赋值为1000是在初始化时完成的;
    2. public static final int value = 1000;value在准备阶段被赋值为1000;

6.1.4 解析

将常量池中的符号引用替换为直接引用。

6.1.5 初始化

  1. 主要通过执行类构造器的< client >方法对类进行初始化;
  2. < client >方法是在编译阶段由编译器自动收集类中静态语句快和静态变量的赋值操作组成的;
  3. 只有在父类的< client >方法都执行成功后,子类中的< client >方法才可以被执行;
  4. 在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成< client >方法

6.2 类加载器

  1. JVM提供了3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器;

在这里插入图片描述

  1. 启动类加载器:负责加载%Java_HOME%/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库;
  2. 扩展类加载器:负责加载%Java_HOME%/lib/ext目录中的类库,java.ext.dirs系统变量加载指定路径中的类库;
  3. 应用程序类加载器:负责加载用户路径(CLASSPATH)上的类库;
  4. 自定义类加载器(可选):可以通过继承java.lang.ClassLoader来实现;

6.3 双亲委派机制

  1. JVM通过双亲委派机制对类进行加载;
  2. 双亲委派机制指一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载;
  3. 如果最终找不到该类,JVM抛出ClassNotFound异常;

在这里插入图片描述

  1. 好处:1 避免了Java的核心API被篡改;2 可以避免类的重复加载;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值