JAVA—JVM详解

2年前 (2022) 程序员胖胖胖虎阿
414 0 0

JAVA—JVM详解

一、JVM

1、什么是JVM

  • JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
  • 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
  • 通过JVM,Java实现了平台无关性,Java语言在不同平台运行时不需要重新编译,只需要在该平台上部署JVM就可以了。因而能实现一次编译多处运行。

2、JRE和JDK

  • JRE:Java Runtime Environment,也就是JVM的运行平台,联系平时用的虚拟机,大概可以理解成JRE=虚拟机平台+虚拟机本体(JVM)。类似于你电脑上的VMWare+适用于VMWare的Ubuntu虚拟机。这样我们也就明白了JVM到底是个什么。
  • JDK:Java Develop Kit,Java的开发工具包,JDK本体也是Java程序,因此运行依赖于JRE,由于需要保持JDK的独立性与完整性,JDK的安装目录下通常也附有JRE。目前Oracle提供的Windows下的JDK安装工具会同时安装一个正常的JRE和隶属于JDK目录下的JRE。

3、JVM位置

JAVA—JVM详解

4、JVM体系结构

  • 简图:
    JAVA—JVM详解
  • 详图:
    JAVA—JVM详解

二、JVM详解

5、ClassLoader(类加载器)

(1)、类加载的机制的层次结构

每个编写的".java"拓展名类文件都存储着需要执行的程序逻辑,这些".java"文件经过Java编译器编译成拓展名为".class"的文件,".class"文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的".class"文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:
JAVA—JVM详解
在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍

  • 启动(Bootstrap)类加载器
    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

  • 扩展(Extension)类加载器
    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

  • 系统(System)类加载器
    也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

  • 全流程简图
    JAVA—JVM详解

//代码详解
public class TestClassLoader {
    public static void main(String[] args) {

        Person person_01 = new Person();
        Person person_02 = new Person();
        Person person_03 = new Person();

        //发现person_01,person_02,person_03的hashCode一致,代表这三个实例化对象隶属于一个Class,即Person
        System.out.println(person_01.hashCode());
        System.out.println(person_02.hashCode());
        System.out.println(person_03.hashCode());

        //Person实例化对象person_01通过getClass()方法得到Class对象Person
        Class Person = person_01.getClass();
        //Person通过getClassLoader()方法得到系统类加载器
        ClassLoader myClassLoader = Person.getClassLoader();
        System.out.println(myClassLoader.hashCode());
        //加载器对象myClassLoader通过getParent()方法得到拓展类加载器
        ClassLoader myParentClassLoader = myClassLoader.getParent();
        System.out.println(myParentClassLoader.hashCode());
        //加载器对象myGPClassLoader通过getParent()方法得到引导类加载器
        ClassLoader myGPClassLoader = myParentClassLoader.getParent();
        System.out.println(myGPClassLoader.hashCode()); //发现报错,无法通过方法获取引导类加载器
    }

}
class Person{}

(2)、双亲委派机制

  • 双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
    JAVA—JVM详解
  • 双亲委派机制工作原理为如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
  • 双亲委派机制优点:
    • 因为双亲委派是向上委托加载的,所以它可以确保类只被加载一次, 避免重复加载
    • Java的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如java.lang.Integer,类加载器通过向上委托,两个Integer,那么最终被加载的应该是jdk的Integer类,而并非我们自定义的,这样就 避免了我们恶意篡改核心包的风险

6、沙箱安全机制

  • 沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。
  • 组成沙箱的基本组件:
    • 1.字节码校验器(bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
    • 2.类裝载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
      防止恶意代码去干涉善意的代码;
      守护了被信任的类库边界;
      将代码归入保护域,确定了代码可以进行哪些操作。

7、本地方法栈(Native)

  • 程序中使用:private native void start0();
    1.凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层c语言的库!
    2.会进入本地方法栈,然后去调用本地方法接口将native方法引入执行
  • 本地方法栈(Native Method Stack)
    内存区域中专门开辟了一块标记区域: Native Method Stack,负责登记native方法,在执行引擎( Execution Engine )执行的时候通过本地方法接口(JNI)加载本地方法库中的方法。
  • 本地方法接口(JNI)
    本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,然后在内存区域中专门开辟了一块标记区域: Native Method Stack,负责登记native方法,在执行引擎( Execution Engine )执行的时候通过本地方法接口(JNI)加载本地方法库中的方法。

8、PC程序计数器(了解)

程序计数器: Program Counter Register

  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计。
  • 为什么需要程序计数器?
    • 记录要执行的代码位置,防止线程切换重新执行字节码执行引擎修改程序计数器的值

9、方法区(Method Area)

  • 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
  • 静态变量(static)、常量(final)、类信息(构造方法、接口定义)(Class)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

10、栈

  • 栈:后进先出,每个线程都有自己的栈,栈内存主管程序的运行,生命周期和线程同步,线程结束,栈内存也就是释放
  • 对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就结束.
  • 栈内存中运行:8大基本类型+对象引用+实例的方法.
  • 栈运行原理:栈桢
  • 栈满:StackOverflowError
    JAVA—JVM详解
  • 栈细分4部分
    JAVA—JVM详解
例:int a=7
  • 局部变量表:存放局部变量(a)
  • 操作数栈:存放操作数(7)
  • 动态链接:将符号引用转成直接引用(符号引用就是你知道调用了谁,直接引用就是你拿到可要调用的方法的地址)
  • 方法出口:方法结束

11、堆(重点)

  • 一个JVM只有一个堆内存,堆内存的大小是可以调节的,类加载器读取类文件后,一般会把类,方法,常量,变量,我们所有引用类型的真实对象,放入堆中。

  • 堆内存细分为三个区域:

    • 新生区(伊甸园区):Young/New
    • 养老区old
    • 永久区Perm
      JAVA—JVM详解

新生区:类的诞生,成长和死亡的地方

分为:

  • 伊甸园区:所有对象都在伊甸园区new出来
  • 幸存0区和幸存1区:轻GC之后存下来的

老年区(养老区):多次轻GC存活下来的对象放在老年区

  • 真理:经过研究,99%的对象都是临时对象!

永久区

JAVA—JVM详解
注意:

元空间:逻辑上存在,物理上不存在 ,因为:存储在本地磁盘内,不占用虚拟机内存
JAVA—JVM详解
默认情况下,JVM使用的最大内存为电脑总内存的四分之一,JVM使用的初始化内存为电脑总内存的六十四分之一.

总结:

  • 栈:基本类型的变量,对象的引用变量,实例对象的方法
  • 堆:存放由new创建的对象和数组
  • 方法区:Class对象,static变量,常量池(常量)

三、JVM调优

12、使用JPofiler工具分析OOM原因

下载地址:https://www.ej-technologies.com/download/jprofiler/version_92

13、常见JVM调优参数

JAVA—JVM详解

四、垃圾回收(GC)

  • 无法手动垃圾回收,只能手动提醒,等待JVM自动回收
  • GC的作用区在堆(Heap)和方法区中
  • JVM进行GC时,并不是统一对这三区域(新生区,幸存区,老年区)统一回收,回收都是新生代
    • 轻GC(普通GC)只针对于新生区,偶尔作用幸存区(在新生区满的情况下)
    • 重GC(全局GC)全局释放内存

14、常见垃圾回收算法

  • 1.引用计数算法
    原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。

  • 2.复制算法
    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中,同时回收未使用的对象。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理。
    优点:不会出现碎片化问题
    缺点:需要两倍内存空间,浪费
    JAVA—JVM详解

  • 3.标记-清除算法
    此算法执行分两阶段。第一阶段从引用根节点开始标记所用存活的对象,第二阶段遍历整个堆,把未标记的对象清除。
    优点:不会浪费内存空间
    缺点:此算法需要暂停整个应用,同时,会产生内存碎片
    JAVA—JVM详解

  • 4.标记-压缩算法
    此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有存活的对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
    此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
    JAVA—JVM详解

  • 总结:

    • 内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
    • 内存效率整齐度:复制算法=标记压缩算法>标记清除算法
    • 内存利用率:标记清除算法=标记压缩算法>复制算法

15、分代回收策略

JAVA—JVM详解

  • 1.绝大多数刚刚被创建的对象会存放在Eden区
  • 2.当Eden区第一次满的时候,会触发MinorGC(轻GC)。首先将Eden区的垃圾对象回收清除,并将存活的对象复制到S0,此时S1是空的。
  • 3.下一次Eden区满时,再执行一次垃圾回收,此次会将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1,此时S0变为空。
  • 4.如此反复在S0和S1之间切换几次(默认15次)之后,还存活的对象将他们转移到老年代中。
  • 5.当老年代满了时会触发FullGC(全GC)

MinorGC

  • 使用的算法是复制算法
  • 年轻代堆空间紧张时会被触发
  • 相对于全收集而言,收集间隔较短

FullGC

  • 使用的算法一般是标记压缩算法
  • 当老年代堆空间满了,会触发全收集操作
  • 可以使用 System.gc()方法来显式的启动全收集
  • 全收集非常耗时

16、垃圾收集器

垃圾回收器的常规匹配:
JAVA—JVM详解

  • 1.串行收集器(Serial)
    Serial 收集器是 Hotspot 运行在 Client 模式下的默认新生代收集器, 它的特点是:单线程收集, 但它却简单而高效
    JAVA—JVM详解

  • 2.并行收集器(ParNew)
    ParNew 收集器其实是前面 Serial 的多线程版本
    JAVA—JVM详解

  • 3.Parallel Scavenge 收集器
    与 ParNew 类似, Parallel Scavenge 也是使用复制算法, 也是并行多线程收集器. 但与其
    他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge 更关注系统吞吐量:
    系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  • 4.Serial Old 收集器
    Serial Old 是 Serial 收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法

  • 5.Parallel Old 收集器
    Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多线程和“标记-整理”算
    法, 吞吐量优先

  • 6.CMS 收集器(Concurrent Mark Sweep)
    CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器),
    基于”标记-清除”算法实现, 整个 GC 过程分为以下 4 个步骤:
    初始标记(CMS initial mark)
    并发标记(CMS concurrent mark: GC Roots Tracing 过程)
    重新标记(CMS remark)
    并发清除(CMS concurrent sweep: 已死对象将会就地释放, 注意:此处没有压缩)

  • 7.G1 收集器
    G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

为什么要垃圾回收时要设计STW(stop the world)?

  • 如果不设计STW,可能在垃圾回收时用户线程就执行完了,堆中的对象都失去了引用,全部变成了垃圾,索性就设计了STW,快速做完垃圾回收,再恢复用户线程运行。

五、JMM(java内存模型)

JMM(java内存模型)Java Memory Model,本身是一个抽象的概念,不是真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JAVA—JVM详解
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程读/写共享变量的副本。

JMM内存模型三大特性

  • 1、原子性
    使用 synchronized 互斥锁来保证操作的原子性
  • 2、可见性:
    volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。
    synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
    final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
  • 3、有序性
    源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的命令。
    重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    处理器在进行重排时必须考虑数据的依赖性,多线程环境线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

参考博客,参考视频

版权声明:程序员胖胖胖虎阿 发表于 2022年11月12日 上午11:56。
转载请注明:JAVA—JVM详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...