面试题学习
仅作学习记录,非原创,已在文末标注原文出处
🍊 : JVM(java virtual machine) 解释性语言的解释器
1.为什么要学习,或者说是使用JVM
-
-
JS(Web)、Scala(类java8)、Java(后端)、Koltin(安卓)多种语言可以经过编译器变成规范的字节码文件,都可以在JVM上运行,小众语言借助别的流行语言很强的虚拟机,也可以有很好的运行性能
-
不同平台都可以编写,经过Linux版的JVM可以在linux上运行,经过Windows版的JVM可以在Windows上运行,实现跨平台性
-
由于语言无关性,奠定了JVM有很强的生态圈,即很多人愿意开源,参与维护更新
-
-
在C++中,使用
new
来动态分配内存以创建对象时,必须在不再需要对象时手动使用delete
来释放内存。如果不手动释放内存,就会造成内存泄漏,导致程序运行时占用的内存持续增加,最终可能导致程序崩溃或系统性能下降。为什么java不会呢,因为java有垃圾回收机制,比如:
- GC判断是否可以回收–可达性分析:Java的垃圾回收器通过可达性分析算法来确定对象是否可被访问。如果对象不可被任何活跃的线程引用到,则该对象将被标记为垃圾对象。
- 何时会进行–垃圾回收器的运行:垃圾回收器会周期性地或在内存资源紧缺时运行,它会清理未被引用的对象,并释放它们所占据的内存空间。
- 垃圾回收算法:
- 标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。
- 复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代
- 标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
在Java中,开发者一般无需显式地去释放内存。垃圾回收器会在适当的时机自动执行内存回收,这样可以避免内存泄漏和其他与手动内存管理相关的问题
2.学习JVM的什么
- 数据区
- 内存结构
- 回收机制
- 垃圾收集器
- JVM参数
- 类加载器
1.数据区
- 堆(Heap):
-
存放内容:堆是一块用于动态分配内存的区域,用来存储程序运行时动态分配的对象、数据结构。
-
大小:堆的大小通常比栈大,且在程序启动时就会分配一定的空间。
-
GC堆:java堆是垃圾收集器管理的主要区域
-
内存回收角度:java堆可以分为新生代和老生代。
- 新生代(Young Generation):
- 对象生命周期:新生代主要用于存放新创建的对象。大部分对象在创建后很快就会变得不可达,即成为垃圾对象。
- 垃圾回收频率:由于新生代中的对象生命周期较短,垃圾回收器会频繁地对其进行垃圾回收。这样可以快速地识别并回收大部分不再使用的对象,以避免堆空间的过度占用。
- 垃圾回收算法:新生代通常使用复制算法(Copying Algorithm)进行垃圾回收。该算法将新生代分为两个区域:Eden区和两个Survivor区(通常为From和To)。对象首先被分配到Eden区,经过一次垃圾回收后,仍然存活的对象会被复制到Survivor区。多次回收后仍然存活的对象会被移到老生代。
- 老生代(Old Generation):
- 对象生命周期:老生代主要用于存放长时间存活的对象,包括从新生代晋升过来的对象和程序中长时间存在的对象。
- 垃圾回收频率:由于老生代中的对象生命周期较长,垃圾回收器会相对较少地对其进行垃圾回收。这样可以减少垃圾回收的开销,提高程序的性能。
- 垃圾回收算法:老生代通常使用标记-清除(Mark and Sweep)算法或标记-整理(Mark and Compact)算法进行垃圾回收。这些算法会标记所有可达的对象,并清除或整理掉不可达的对象,以释放内存空间。
- 新生代(Young Generation):
-
数据结构:在堆上分配的内存空间是按需动态分配的,对象的大小可以在运行时确定。
-
访问:线程共享
-
从内存结构看:
-
实际上,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.垃圾回收
-
定义垃圾: 没有引用指向的内存对象,因为程序已经用不上了,jvm会在程序运行过程不断进行自动的垃圾回收(GC—Garbage Collection)
-
优点:无需内存管理
-
缺点:即使程序员可以判断哪个部分内存已经无用,调用了System.gc手动执行回收垃圾,它是否执行回收,什么时候执行也是一样未知的,因为不可达的对象并不会马上直接回收,而垃圾收集器的回收过程的自动的,不能强制执行某个对象,即不能delete()
-
分代:
- 新生代:
- 细分为Eden和两个(From、To)Survivor区域,默认空间分配8:1:1,其中总是有一块Survivor是空闲的,将空闲块Survivor所占10%的内存空间用于存放复制算法回收后存活的对象,Eden和另一个Survivor的90%的空间用来创建对象
- 每次GC都有较多对象回收,便采用了复制算法回收,只有少量的复制成本就可以完成回收
- Minor 新生代垃圾回收动作,频繁,快,触发条件:eden区不够用
- 老年代:
- 一个区域,新生代和老年代堆空间比例:1:3
- Major 老年代垃圾回收动作,通常伴随Minor执行,较慢
- Major触发条件:
- 每次晋升到老年代的对象平均大小>老年代剩余空间
- MinorGC后存活的对象超过了老年代剩余空间
- 永久代空间不足
- 执行System.gc()
- CMS GC异常
- 堆内存分配很大的对象
- 永久代:jvm的方法区,类信息,静态变量,常量
- 新生代:
-
判断对象是否存活
-
引用计数法:
- 每个对象创建的时候绑定一个计数器,引用指向该对象计数器++,指向它的引用被删除计数器–,计数器为0,对象死亡
- 优点:判断效率高,实现简单
- 缺点:对象之间相互循环引用,主流已经不再使用
-
可达性分析:
- 从GC Roots开始搜索,路径表示引用链,如果一个对象链到GC Roots的搜索中不相连,表示该对象不可用,没有引用的对象
-
GC Roots对象:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象:
方法中定义的局部变量:例如,方法中声明的对象引用变量。
方法(函数)参数:方法的参数也会被存储在局部变量表中,如果参数是对象引用类型,则会引用相应的 对象。
中间计算结果:在方法执行过程中产生的临时变量或计算结果,如果是对象引用类型,则会引用 相应的对象。
2、方法区中类“静态属于引用的对象”;
3、方法区中”常量引用的对象“;
4、本地方法栈中”JNI指针“(即一般说的Native方法)引用的对象。
-
7.垃圾回收策略:
1、标记清除算法:
- 优点: 解决循环引用的问题、必要时才回收
- 缺点:回收时,应用需要挂起、标记和清除效率不高,尤其是扫描的对象比较多时、造成内存碎片、如上图
- 应用场景:一般和标记整理算法一样应用于老年代,因为对象生命周期长
2、标记整理算法:
- 优点:解决了标记清除的内存碎片 问题
- 缺点:算是清除算法的升级,但是仍然具备上述一些的缺点,而且移动了对象,需要更新引用
- 应用场景:如上
3、复制算法:
- 优点:对象不多的情况下,性能高,解决内存碎片和标记整理中的引用更新问题
- 缺点:分配的内存只有部分使用,另一部分空闲
- 应用场景:新生代中,存活的对象并不多,使用复制算法拷贝时效率比较高
8.垃圾收集器:
特点:
- 多线程
- 并行
- 并发:垃圾收集和程序同时进行
- 分代,收集范围:新生代和老年代
- 独立管理GC堆(新生和老年),不需要和其他收集器配合
- 采用不同方式处理不同时期的对象
- 可以面向服务端应用,针对大内存、多cpu的机器
- 采用标记整理+复制算法策略
使用方式:
-XX:+UserG1GC 指定G1收集器
-XX:InitiatingHeapOccupancyPercent 当java堆达到占用率参数值,标记整理算法开始并发标记阶段
-XX:MaxGCPauseMillis 垃圾回收时,应用被挂起的时间
-XX:G1HeapRegionSize 堆每个区域大小,不同的区域采用独特的垃圾回收策略,新生代,老年代,可看上面标注整理算法图中一系列框,即为不同区域,
3.JVM参数配置
常用的设置
-
-Xms: 初始堆大小,JVM启动时,给定堆空间大小
-
-Xmx: 最大堆大小,初始化堆空间不足,最大可以扩展到多少
-
-Xm: 堆中年轻代大小
-
-Xss: 设置每个线程的堆栈大小,JDK5后每个线程java栈大小为1M
-
-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调参,内存占小些
网上调优的建议:
-
初始化堆(-Xmx)和最大堆(-Xmx)大小相同,默认jvm堆是电脑内存的1/4,可以减少程序运行时垃圾回收次数
-
并发线程数 (-XX:ParallelGCThreads, -XX:ConcGCThreads): 根据服务器的 CPU 核心数来设置并发垃圾回收线程数,以充分利用硬件资源
-
类加载优化: 可以通过设置
-XX:+UseConcMarkSweepGC
来减少类加载器的停顿时间 -
启用即时编译器 (-XX:+UseCompressedOops, -XX:+TieredCompilation): 启用即时编译器(JIT)可以提高代码执行效率
-
初始化堆和最大堆内存越大,吞吐量越高
-
多线程,使用并行GC收集器,大部分情况默认用G12就挺好
-
设置新生代和老年代的比例最好为1:2或者1:3
4.类加载的机制及过程
1、过程
据数据区–上面所描述在方法区存储类信息,类通过类加载器后,经过加载、链接、初始化的过程,存储在方法区,通过堆中的Class对象访问
- 加载(Loading): 加载是指将类的二进制数据读入 JVM 的内存中,一般来说,这些数据来源于 Class 文件、网络等,这些数据转化为方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区加载入的相关类数据的访问入口。
- 验证(Verification): 验证是指对类的二进制数据进行合法性检查,包括文件格式、语义、字节码和符号引用等方面的验证。
- 准备(Preparation): 准备是指为类的静态变量分配内存并设置初始值,例如将数字类型的静态变量初始化为 0,将对象类型的静态变量初始化为 null。
- 解析(Resolution): 解析是指将符号引用转换为直接引用的过程,例如将类名转换为对应的 Class 对象、方法名转换为对应的直接地址等。
- 初始化(Initialization): 初始化是指执行类构造器
<clinit>
方法的过程,该方法由编译器自动生成,会对静态变量进行赋值和执行静态块中的代码等。 - 使用(Usage): 使用是指在应用程序中使用该类的过程,包括创建实例、调用方法等。
其中,前三个步骤(加载、验证和准备)统称为链接(Linking),而第四个步骤(解析)可以与前三个步骤进行合并。在类加载过程中,如果遇到错误,将会抛出 ClassNotFoundException、NoClassDefFoundError、LinkageError 等异常。感觉和计算机系统里面学的动态链接过程相似
2、😿机制
类加载机制主要包括以下几个方面:
- 🦀 双亲委派模型(Parent Delegation Model): 在类加载过程中,JVM 使用双亲委派模型,即当一个类需要被加载时,先委托给父类加载器去加载,只有在父类加载器无法加载该类时,才由子类加载器尝试加载。这样可以确保类加载的顺序和唯一性,避免类重复加载和类冲突。
- 类加载器分类: 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虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,)
- 加载、链接和初始化阶段: 类加载主要包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和初始化(Initialization)五个阶段。在加载阶段,将类的字节码加载到内存中;在链接阶段,对类进行验证、准备和解析;在初始化阶段,执行类的初始化操作,包括静态变量初始化和静态代码块执行。
- 延迟加载(Lazy Loading): 类在第一次被使用时才会被加载,这种延迟加载的机制可以提高程序的性能和资源利用率。
- 动态加载(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类加载是怎么的过程、类加载最重要的是了解什么