JVM的笔记

面试题学习

仅作学习记录,非原创,已在文末标注原文出处

🍊 : JVM(java virtual machine) 解释性语言的解释器

在这里插入图片描述

1.为什么要学习,或者说是使用JVM

  1. 语言无关性

    • JS(Web)、Scala(类java8)、Java(后端)、Koltin(安卓)多种语言可以经过编译器变成规范的字节码文件,都可以在JVM上运行,小众语言借助别的流行语言很强的虚拟机,也可以有很好的运行性能

    • 不同平台都可以编写,经过Linux版的JVM可以在linux上运行,经过Windows版的JVM可以在Windows上运行,实现跨平台性

    • 由于语言无关性,奠定了JVM有很强的生态圈,即很多人愿意开源,参与维护更新

  2. 内存管理

    在C++中,使用new来动态分配内存以创建对象时,必须在不再需要对象时手动使用delete来释放内存。如果不手动释放内存,就会造成内存泄漏,导致程序运行时占用的内存持续增加,最终可能导致程序崩溃或系统性能下降。

    为什么java不会呢,因为java有垃圾回收机制,比如:

    1. GC判断是否可以回收–可达性分析:Java的垃圾回收器通过可达性分析算法来确定对象是否可被访问。如果对象不可被任何活跃的线程引用到,则该对象将被标记为垃圾对象。
    2. 何时会进行–垃圾回收器的运行:垃圾回收器会周期性地或在内存资源紧缺时运行,它会清理未被引用的对象,并释放它们所占据的内存空间。
    3. 垃圾回收算法
      • 标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
      • 复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代
      • 标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代

    在Java中,开发者一般无需显式地去释放内存。垃圾回收器会在适当的时机自动执行内存回收,这样可以避免内存泄漏和其他与手动内存管理相关的问题

在这里插入图片描述

2.学习JVM的什么

  • 数据区
  • 内存结构
  • 回收机制
  • 垃圾收集器
  • JVM参数
  • 类加载器

1.数据区

在这里插入图片描述

  1. 堆(Heap)
  • 存放内容:堆是一块用于动态分配内存的区域,用来存储程序运行时动态分配的对象、数据结构

  • 大小:堆的大小通常比栈大,且在程序启动时就会分配一定的空间。

  • GC堆:java堆是垃圾收集器管理的主要区域

  • 内存回收角度:java堆可以分为新生代和老生代。

    • 新生代(Young Generation)
      • 对象生命周期:新生代主要用于存放新创建的对象。大部分对象在创建后很快就会变得不可达,即成为垃圾对象。
      • 垃圾回收频率:由于新生代中的对象生命周期较短,垃圾回收器会频繁地对其进行垃圾回收。这样可以快速地识别并回收大部分不再使用的对象,以避免堆空间的过度占用。
      • 垃圾回收算法:新生代通常使用复制算法(Copying Algorithm)进行垃圾回收。该算法将新生代分为两个区域:Eden区和两个Survivor区(通常为From和To)。对象首先被分配到Eden区,经过一次垃圾回收后,仍然存活的对象会被复制到Survivor区。多次回收后仍然存活的对象会被移到老生代
    • 老生代(Old Generation)
      • 对象生命周期:老生代主要用于存放长时间存活的对象,包括从新生代晋升过来的对象和程序中长时间存在的对象。
      • 垃圾回收频率:由于老生代中的对象生命周期较长,垃圾回收器会相对较少地对其进行垃圾回收。这样可以减少垃圾回收的开销,提高程序的性能。
      • 垃圾回收算法:老生代通常使用标记-清除(Mark and Sweep)算法或标记-整理(Mark and Compact)算法进行垃圾回收。这些算法会标记所有可达的对象,并清除或整理掉不可达的对象,以释放内存空间。
  • 数据结构:在堆上分配的内存空间是按需动态分配的,对象的大小可以在运行时确定。

  • 访问:线程共享

  • 从内存结构看

    • 实际上,Java中不存在所谓的本地堆(Native Heap)1的概念。Java程序中所有的对象都是在Java堆上进行分配和管理的。

      Java堆是Java虚拟机管理的一块内存区域,用于存储对象实例和数组等数据结构。所有通过new关键字创建的对象都会在Java堆上进行分配,而且Java虚拟机会负责自动管理这些对象的生命周期和内存释放。

      另一方面,Java中存在直接内存(Direct Memory)的概念。直接内存是一种在堆外进行分配的内存,它不受Java堆大小的限制,并且可以利用操作系统的零拷贝特性。直接内存可以通过ByteBuffer.allocateDirect()方法进行分配,并通过Buffer.clean()方法手动释放。

      在这里插入图片描述

      总结起来,Java程序中使用Java堆来分配和管理对象内存,而直接内存是一种特殊的堆外内存,可以通过ByteBuffer类进行分配和释放。对于一般的Java开发,我们主要关注的是Java堆的管理,而直接内存通常用于特定的场景,例如需要高性能的I/O操作或与本地代码进行交互等。

ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接内存分配申

ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
在进行10000000次分配操作时,堆内存:分配耗时:126ms
在进行10000000次分配操作时,直接内存:分配耗时:7059ms
在进行1000000000次读写操作时,堆内存:读写耗时:997ms
在进行1000000000次读写操作时,直接内存:读写耗时:515ms
   

2.栈(Stack)

  • 存放内容:虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量操作数栈,动态链接(调用的其他方法),出口(return)等。

    在这里插入图片描述

  • 大小:栈的大小通常比堆小,每个线程的栈的大小固定,由JVM参数设定(java -Xss1M YuorClassName),随着程序执行的过程中动态变化。

  • 生命周期:栈中的数据是自动分配和释放的。

  • 数据结构:遵循先进后出(FILO)的原则。

  • 访问:栈中的数据可以通过栈顶指针进行访问,具有快速的访问速度,栈帧按照先入后出的顺序在栈中访问,栈区属于线程私有

3.方法区

  • 存放内容线程共享,存储Java虚拟机加载的类信息、常量、静态变量、即使编译后的代码等数据,好像都是强符号,而且在可达性分析算法中,GC Roots,就是以局部变量、静态变量、常量等作为节点
  • 别名:非堆,内存分配不够,抛出OutOfMemoryError异常

4.程序计数器

  • 内容:程序按照一系列指令完成任务,计数器保存线程当前执行指令的地址码,每个线程都有各自的计数器,互不影响,独立存储

  • 功能:因为是保存指令的地址码,可以记忆当前的进度,即使多线程切换,也能恢复

2.垃圾回收

  1. 定义垃圾: 没有引用指向的内存对象,因为程序已经用不上了,jvm会在程序运行过程不断进行自动的垃圾回收(GC—Garbage Collection)

  2. 优点:无需内存管理

  3. 缺点:即使程序员可以判断哪个部分内存已经无用,调用了System.gc手动执行回收垃圾,它是否执行回收,什么时候执行也是一样未知的,因为不可达的对象并不会马上直接回收,而垃圾收集器的回收过程的自动的,不能强制执行某个对象,即不能delete()

  4. 分代

    • 新生代
      • 细分为Eden和两个(From、To)Survivor区域,默认空间分配8:1:1,其中总是有一块Survivor是空闲的,将空闲块Survivor所占10%的内存空间用于存放复制算法回收后存活的对象,Eden和另一个Survivor的90%的空间用来创建对象
      • 每次GC都有较多对象回收,便采用了复制算法回收,只有少量的复制成本就可以完成回收
      • Minor 新生代垃圾回收动作,频繁,快,触发条件:eden区不够用
    • 老年代
      • 一个区域,新生代和老年代堆空间比例:1:3
      • Major 老年代垃圾回收动作,通常伴随Minor执行,较慢
      • Major触发条件:
        1. 每次晋升到老年代的对象平均大小>老年代剩余空间
        2. MinorGC后存活的对象超过了老年代剩余空间
        3. 永久代空间不足
        4. 执行System.gc()
        5. CMS GC异常
        6. 堆内存分配很大的对象
    • 永久代:jvm的方法区,类信息,静态变量,常量
  5. 判断对象是否存活

    • 引用计数法:

      • 每个对象创建的时候绑定一个计数器,引用指向该对象计数器++,指向它的引用被删除计数器–,计数器为0,对象死亡
      • 优点:判断效率高,实现简单
      • 缺点:对象之间相互循环引用,主流已经不再使用
    • 可达性分析:

      • 从GC Roots开始搜索,路径表示引用链,如果一个对象链到GC Roots的搜索中不相连,表示该对象不可用,没有引用的对象

      在这里插入图片描述

      • GC Roots对象:

        1、虚拟机栈(栈帧中的本地变量表)中引用的对象:
        方法中定义的局部变量:例如,方法中声明的对象引用变量。
        方法(函数)参数:方法的参数也会被存储在局部变量表中,如果参数是对象引用类型,则会引用相应的 对象。
        中间计算结果:在方法执行过程中产生的临时变量或计算结果,如果是对象引用类型,则会引用 相应的对象。
        2、方法区中类“静态属于引用的对象”;
        3、方法区中”常量引用的对象“;
        4、本地方法栈中”JNI指针“(即一般说的Native方法)引用的对象。

7.垃圾回收策略:

1、标记清除算法

- 优点: 解决循环引用的问题、必要时才回收
- 缺点:回收时,应用需要挂起、标记和清除效率不高,尤其是扫描的对象比较多时、造成内存碎片、如上图
- 应用场景:一般和标记整理算法一样应用于老年代,因为对象生命周期长

2、标记整理算法

- 优点:解决了标记清除的内存碎片 问题
- 缺点:算是清除算法的升级,但是仍然具备上述一些的缺点,而且移动了对象,需要更新引用
- 应用场景:如上

3、复制算法

- 优点:对象不多的情况下,性能高,解决内存碎片和标记整理中的引用更新问题
- 缺点:分配的内存只有部分使用,另一部分空闲
- 应用场景:新生代中,存活的对象并不多,使用复制算法拷贝时效率比较高

8.垃圾收集器:

在这里插入图片描述

特点

  1. 多线程
  2. 并行
  3. 并发:垃圾收集和程序同时进行
  4. 分代,收集范围:新生代和老年代
  5. 独立管理GC堆(新生和老年),不需要和其他收集器配合
  6. 采用不同方式处理不同时期的对象
  7. 可以面向服务端应用,针对大内存、多cpu的机器
  8. 采用标记整理+复制算法策略

使用方式:

-XX:+UserG1GC 指定G1收集器

-XX:InitiatingHeapOccupancyPercent 当java堆达到占用率参数值,标记整理算法开始并发标记阶段

-XX:MaxGCPauseMillis 垃圾回收时,应用被挂起的时间

-XX:G1HeapRegionSize	堆每个区域大小,不同的区域采用独特的垃圾回收策略,新生代,老年代,可看上面标注整理算法图中一系列框,即为不同区域,

3.JVM参数配置

常用的设置

  1. -Xms: 初始堆大小,JVM启动时,给定堆空间大小

  2. -Xmx: 最大堆大小,初始化堆空间不足,最大可以扩展到多少

  3. -Xm: 堆中年轻代大小

  4. -Xss: 设置每个线程的堆栈大小,JDK5后每个线程java栈大小为1M

  5. -XX:线程堆栈大小

    java -Xmx512m -jar yourapp.jar
    
    java -Xss2M -jar yourapp.jar
    
    docker run -d -e ES_JAVA_OPTS="-Xms512m -Xmx1g" your-elasticsearch-image 
    docker中使用jvm调参,内存占小些
    

网上调优的建议:

  1. 初始化堆(-Xmx)和最大堆(-Xmx)大小相同,默认jvm堆是电脑内存的1/4,可以减少程序运行时垃圾回收次数

  2. 并发线程数 (-XX:ParallelGCThreads, -XX:ConcGCThreads): 根据服务器的 CPU 核心数来设置并发垃圾回收线程数,以充分利用硬件资源

  3. 类加载优化: 可以通过设置 -XX:+UseConcMarkSweepGC 来减少类加载器的停顿时间

  4. 启用即时编译器 (-XX:+UseCompressedOops, -XX:+TieredCompilation): 启用即时编译器(JIT)可以提高代码执行效率

  5. 初始化堆和最大堆内存越大,吞吐量越高

  6. 多线程,使用并行GC收集器,大部分情况默认用G12就挺好

  7. 设置新生代和老年代的比例最好为1:2或者1:3

4.类加载的机制及过程

1、过程

据数据区–上面所描述在方法区存储类信息,类通过类加载器后,经过加载、链接、初始化的过程,存储在方法区,通过堆中的Class对象访问

  1. 加载(Loading): 加载是指将类的二进制数据读入 JVM 的内存中,一般来说,这些数据来源于 Class 文件、网络等,这些数据转化为方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区加载入的相关类数据的访问入口
  2. 验证(Verification): 验证是指对类的二进制数据进行合法性检查,包括文件格式、语义、字节码和符号引用等方面的验证。
  3. 准备(Preparation): 准备是指为类的静态变量分配内存并设置初始值,例如将数字类型的静态变量初始化为 0,将对象类型的静态变量初始化为 null。
  4. 解析(Resolution): 解析是指将符号引用转换为直接引用的过程,例如将类名转换为对应的 Class 对象、方法名转换为对应的直接地址等。
  5. 初始化(Initialization): 初始化是指执行类构造器 <clinit> 方法的过程,该方法由编译器自动生成,会对静态变量进行赋值和执行静态块中的代码等。
  6. 使用(Usage): 使用是指在应用程序中使用该类的过程,包括创建实例、调用方法等。

其中,前三个步骤(加载、验证和准备)统称为链接(Linking),而第四个步骤(解析)可以与前三个步骤进行合并。在类加载过程中,如果遇到错误,将会抛出 ClassNotFoundException、NoClassDefFoundError、LinkageError 等异常。感觉和计算机系统里面学的动态链接过程相似

2、😿机制

类加载机制主要包括以下几个方面:

  1. 🦀 双亲委派模型(Parent Delegation Model): 在类加载过程中,JVM 使用双亲委派模型,即当一个类需要被加载时,先委托给父类加载器去加载,只有在父类加载器无法加载该类时,才由子类加载器尝试加载。这样可以确保类加载的顺序和唯一性,避免类重复加载和类冲突
  2. 类加载器分类: Java 类加载器主要分为三种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。不同类加载器负责加载不同位置的类文件,形成了层次化的加载结构。
  • 1 根类加载器(bootstrap class loader)
    它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • 2 扩展类加载器(extensions class loader)
    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器
  • 3 系统类加载器(system class loader)
    被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。(Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,)
  1. 加载、链接和初始化阶段: 类加载主要包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和初始化(Initialization)五个阶段。在加载阶段,将类的字节码加载到内存中;在链接阶段,对类进行验证、准备和解析;在初始化阶段,执行类的初始化操作,包括静态变量初始化和静态代码块执行。
  2. 延迟加载(Lazy Loading): 类在第一次被使用时才会被加载,这种延迟加载的机制可以提高程序的性能和资源利用率。
  3. 动态加载(Dynamic Loading): Java 支持通过反射机制在运行时动态加载类,从而实现灵活的类加载和动态扩展功能。

总的来说,Java 类加载机制是一种灵活且安全的机制,通过双亲委派模型、类加载器分类和加载、链接、初始化阶段等步骤,实现了对类的动态加载和管理,确保了程序的安全性和稳定性。深入理解类加载机制有助于我们更好地设计和编写 Java 程序,并解决潜在的类加载问题。

结合Java反射理解,下面是java反射的解释

Java 反射(Reflection)是指在运行时动态地获取一个类的信息,并对其进行操作的能力。通过反射,我们可以在运行时获取类的属性、方法和构造器等信息,还可以调用对象的方法、修改对象的属性值、创建对象实例等。

Java 反射的主要作用有以下几点:

动态获取类信息: 可以在运行时动态获取类的属性、方法、构造器等信息,避免在编译时硬编码。

动态创建对象: 可以在运行时动态创建对象,例如通过 Class.newInstance() 或 Constructor.newInstance() 方法来创建对象。

动态调用方法: 可以在运行时动态调用对象的方法,例如通过 Method.invoke() 方法来调用对象的方法。

动态修改属性值: 可以在运行时动态修改对象的属性值,例如通过 Field.set() 方法来修改对象属性的值。

反射的实现基于 Java 的反射 API,主要包括以下类:

Class 类:表示一个类或接口的元数据,可以获取类的属性、方法、构造器等信息。
Constructor 类:表示类的构造器,可以用来创建对象实例。
Method 类:表示类的方法,可以用来调用对象的方法。
Field 类:表示类的属性,可以用来访问和修改对象的属性。
需要注意的是,反射虽然具有灵活性和动态性等优点,但也会加重系统负担,影响性能。因此,在使用反射时需要谨慎考虑其适用场景和性能开销。

态调用方法: 可以在运行时动态调用对象的方法,例如通过 Method.invoke() 方法来调用对象的方法。

动态修改属性值: 可以在运行时动态修改对象的属性值,例如通过 Field.set() 方法来修改对象属性的值。

反射的实现基于 Java 的反射 API,主要包括以下类:

Class 类:表示一个类或接口的元数据,可以获取类的属性、方法、构造器等信息。
Constructor 类:表示类的构造器,可以用来创建对象实例。
Method 类:表示类的方法,可以用来调用对象的方法。
Field 类:表示类的属性,可以用来访问和修改对象的属性。
需要注意的是,反射虽然具有灵活性和动态性等优点,但也会加重系统负担,影响性能。因此,在使用反射时需要谨慎考虑其适用场景和性能开销。

参考资料:
1、一篇文章掌握整个JVM,JVM超详细解析!!
2、不是我吹,看完这个JVM面试题讲解30集,面试被问我都能吹一个小时
chatgpt回答
-XX:MaxGCPauseMillis这个是什么、为什么一个堆会有多个区域、需要了解jvm的类加载器吗、jvm调参优化的建议、类加载器为什么会停顿、jvm类加载是怎么的过程、类加载最重要的是了解什么


  1. Native堆(本地堆)是指Java虚拟机以外的本地内存区域,主要用于存储Java虚拟机本身的数据结构,比如虚拟机的代码、静态变量、运行时常量池等。Native堆通常由操作系统分配和管理。 ↩︎

  2. 可回收新生代和老年代,多线程并行,标志整理和复制算法策略结合两个的优点 ↩︎

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值