1、知识点汇总
JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高。
重点包括内存模型、类加载机制和垃圾回收(GC)。性能调优侧重应用实践,编译器优化与执行模式侧重理论基础。需掌握内存模型的各部分功能和数据保存;类加载的双亲委派机制及各类加载器的使用;GC的分代回收思想、算法与适用场景;性能调优的JVM参数及工具的应用;执行模式中的解释、编译、混合模式优缺点,以及JIT即时编译、OSR栈替换、C1/C2编译器优化等。新技术包括Java 10的Graal编译器,javac编译过程优化及AST抽象语法树。
2、知识点详解:
1、JVM内存模型:
线程独占:栈,本地方法栈,程序计数器 线程共享:堆,方法区
2、栈:
又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方 法出口等信息.调用方法时执行入栈,方法返回式执行出栈.
3、本地方法栈
与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.
4、程序计数器
保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行 Native方法时,程序计数器为空.
5、堆
JVM内存管理最大的一块,对被线程共享, 目的是存放对象的实例,几乎所欲的对象实例都会放在这里, 当堆没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回 收器进行垃圾的回收管理
6、方法区:
又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7 的永久代和1.8的元空间都是方法区的一种实现
7、JVM 内存可见性
JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱。因此JMM需要提供原子性,可见性,有序性保证.
3、说说类加载与卸载
加载过程
其中验证,准备,解析合称 链接
- 加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象。
- 验证确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全。
- 准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变 量,因为final变量在编译时分配.
- 解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等。
- 初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用 时才会初始化。
- 触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候。
- Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸。
1、加载机制-双亲委派模式
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器. 父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载。
优点:
- 避免类的重复加载
- 避免Java的核心API被篡改
2、分代回收
分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间。
- 年轻代->标记-复制
- 老年代->标记-清除
3、回收算法
a、 G1算法
1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全 部的增量式清理,以保证停顿时间不会过长。
其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一 部分用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区。
同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间。
年轻代回收: 并行复制采用复制算法,并行收集,会StopTheWorld。
老年代回收: 会对年轻代一并回收。
初始标记完成堆root对象的标记,会StopTheWorld.并发标记 GC线程和应用线程并发执行. 最终标记 完成三色标记周期,会StopTheWorld.复制/清楚会优先对可回收空间加大的区域进行回收。
b、ZGC算法
前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿时间。
- 着色指针
- 读屏障
- 并发处理
- 基于region
- 内存压缩(整理)
roots标记:标记root对象,会StopTheWorld。
并发标记:利用读屏障与应用线程一起运行标记,可能 会发生StopTheWorld.清除会清理标记为不可用的对象。
roots重定位是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生。重定位最开始会StopTheWorld,却决于重定位集与对象总活动集的比例。 并发重定位与并发标记类似。
4.简述一下JVM的内存模型
JVM(Java虚拟机)的内存模型定义了Java程序在运行时,JVM如何管理和使用内存。它包括程序运行时所需的各个内存区域,并规定了这些区域如何分配、使用和回收。JVM内存模型的设计非常关键,因为它直接影响程序的性能、稳定性及并发执行的行为。
JVM内存模型的主要组成部分:
1.方法区(Method Area)
功能:方法区用于存储类的相关信息(如类的结构、字段、方法、常量池、静态变量等),是所有线程共享的内存区域。
特点:
- 存储类信息、常量池、静态变量和即时编译(JIT)编译后的代码。
- 由于类的加载、卸载和回收,方法区可能会经历频繁的垃圾回收。
- 在早期的JVM中,方法区和永久代(PermGen)是同一块内存区域,但在Java 8及以后,永久代被**元空间(Metaspace)**替代,元空间不再在堆内存中,而是存储在本地内存中。
2.堆(Heap)
功能:堆是JVM中最大的内存区域,用于存储所有的对象实例。它是垃圾回收器(GC)管理的主要区域,所有的对象实例和数组都在堆中创建。
特点:
- 堆是所有线程共享的内存区域。
- 通过垃圾回收器来管理内存,避免内存泄漏。
- 堆通常分为年轻代(Young Generation)和老年代(Old Generation),其中年轻代存放新创建的对象,而老年代用于存放生命周期较长的对象。
3.栈(Stack)
功能:每个线程都有自己的栈,用于存储局部变量、方法调用的栈帧、返回地址等。栈内存也用于方法的调用和返回。
特点:
- 每个线程有独立的栈。
- 局部变量、方法调用、返回地址等信息都保存在栈中。
- 栈是基于先进后出(LIFO)方式存储的,因此每次方法调用时都会创建一个新的栈帧,方法执行完后,栈帧就会被销毁。
4.程序计数器(Program Counter Register)
功能:程序计数器是一个较小的内存区域,用于记录当前线程所执行的字节码的行号指示器。它能够跟踪正在执行的代码位置,以便程序能够顺利地进行线程切换。
特点:
- 每个线程都有一个独立的程序计数器。
- 线程上下文切换时,程序计数器保存当前线程的执行位置。
5.本地方法栈(Native Method Stack)
功能:本地方法栈是用于支持JVM与本地(非Java)代码交互的栈。它与JVM的栈类似,但用于执行本地方法(如C、C++等语言编写的代码)。
特点:
本地方法栈主要用于执行本地方法,调用本地代码时会创建本地方法的栈帧。
6.直接内存(Direct Memory)
功能:直接内存并非JVM内存的一部分,但它是通过nio(New Input/Output)类库对本地内存的直接访问。这允许JVM绕过堆内存,通过直接与操作系统内存交互来提高性能。
特点:
通过DirectByteBuffer直接访问本地内存,可以减少内存复制,提高I/O性能。
JVM内存模型的内存区域划分
-
堆内存:存储对象实例和数组,是垃圾回收的主要区域。堆被划分为年轻代(Young Generation)和老年代(Old Generation):
-
年轻代:存储大部分新创建的对象,垃圾回收较频繁。年轻代又分为三个区域:
- Eden区:新创建的对象通常会被分配到这里。
- Survivor区:存放经过一次或多次垃圾回收的对象。
-
老年代:存储生命周期较长的对象,垃圾回收相对较少。长期存活的对象会被转移到老年代。
-
方法区:存储类的元数据(如类的结构、方法、字段、常量等),它是所有线程共享的区域。
-
栈内存:每个线程都有自己的栈空间,用于存放局部变量和方法的调用。
-
程序计数器:每个线程都有一个程序计数器,用于记录程序当前的执行位置。
-
本地方法栈:与栈类似,但用于执行本地方法。
JVM内存模型与并发
JVM的内存模型在多线程环境中至关重要,尤其是关于内存可见性和有序性的问题。Java的内存模型(JMM)规定了多线程并发执行时,如何保证各个线程之间对共享变量的访问是一致的。它主要涉及以下几个方面:
- 可见性:一个线程对共享变量的修改,其他线程能够及时看到。
- 原子性:操作的不可分割性,保证某个操作要么完全执行,要么完全不执行。
- 有序性:程序指令的执行顺序,不会随意重排。
通过 synchronized 关键字和 volatile 关键字,Java提供了对并发访问的控制,确保了线程安全。
总结:
JVM内存模型包括了多个重要的内存区域,每个区域负责特定的任务。堆用于存储对象,栈用于存储局部变量和方法信息,方法区存储类信息,程序计数器帮助管理线程执行,直接内存则提供了与操作系统内存的直接交互。通过合理管理这些内存区域,JVM能够提供高效的内存管理和优化,支持多线程和并发执行。
5.说说堆和栈的区别
堆(Heap)和栈(Stack)是计算机内存管理中的两个重要区域,它们在用途、分配方式、生命周期等方面有许多区别。以下是堆和栈的主要区别:
1. 内存分配方式
栈:
- 栈内存由操作系统自动管理,分配和回收的速度非常快。栈内存采用先进后出(LIFO)原则进行分配和回收。
- 每当一个方法被调用时,系统会为该方法分配一个栈帧,存储方法的局部变量、参数和返回地址。当方法调用结束时,栈帧会被销毁,内存空间被释放。
堆: - 堆内存由JVM或操作系统管理,分配和回收相对较慢。堆内存是动态分配的,可以在程序运行时灵活地分配空间。
- 堆内存用于存储对象实例和数组。对象在堆中分配内存,而这些对象的生命周期由垃圾回收器管理。
2. 内存空间的大小
栈:
- 栈的大小通常较小,受到操作系统或JVM的限制。每个线程都有自己的栈空间,因此栈的内存大小通常是有限的。
- 栈的空间比较紧张,适合存储局部变量和函数调用等小规模的数据。
堆:
- 堆的空间通常比较大,且由JVM动态管理,可以根据需要进行扩展。堆的大小可以通过JVM启动参数来配置。
- 堆内存主要用于存储程序中创建的对象和数组,适合存储较大的数据结构。
3. 存储内容
栈:
- 存储局部变量、方法调用信息(如返回地址、函数参数等)以及每个方法的栈帧。
- 每个线程都有一个独立的栈,栈内存只存储当前线程的相关数据。
堆:
- 存储对象实例和数组。这些对象由JVM的垃圾回收器进行管理。
- 堆内存中的对象可以被多个线程共享访问,因此需要注意线程安全。
4. 生命周期
栈:
栈的生命周期由方法调用决定。当一个方法被调用时,栈为该方法分配栈帧,并在方法调用结束时释放栈帧。因此栈中的数据(如局部变量和方法调用信息)具有自动管理的生命周期。
堆:
堆中的对象生命周期由垃圾回收器(GC)管理。当一个对象不再被引用时,垃圾回收器会回收该对象占用的堆内存空间。堆内存的管理更加灵活,但也更复杂。
5. 访问速度
栈:
栈的访问速度非常快,因为栈的内存分配和回收遵循严格的顺序(LIFO),每次分配和回收都是在栈的顶端进行。栈是连续的内存空间,操作系统为其分配的内存通常是高效的。
堆:
堆的访问速度相对较慢,因为堆内存的分配和回收是动态的。堆内存可能会因为频繁的分配和垃圾回收而产生碎片化,影响性能。
6. 线程共享
栈:
每个线程都有自己的栈空间,栈内存是线程私有的,其他线程不能访问当前线程的栈。
堆:
堆内存是所有线程共享的,多个线程可以同时访问堆中的对象。因此,在多线程环境下,堆中的数据需要特别小心,避免数据竞争和同步问题。
7. 垃圾回收
栈:
栈中的内存是自动回收的,每次方法结束时,栈帧会被销毁,栈内存也会被释放。
堆:
堆中的内存则由垃圾回收器(GC)负责回收。垃圾回收器会定期扫描堆内存,找出不再被引用的对象,并释放它们占用的内存。
8. 适用场景
栈:
栈适用于存储局部变量和方法调用信息,它非常适合用于函数调用、递归等操作。
堆:
堆适用于存储对象、数组等动态数据结构。它适合用于存储生命周期较长的对象,比如在Java中通过new关键字创建的对象都会被分配到堆上。
特性 | 堆(Heap) | 栈(Stack) |
---|---|---|
分配方式 | 动态分配,通过JVM管理 | 静态分配,由操作系统自动管理 |
存储内容 | 对象实例、数组 | 局部变量、方法调用信息 |
生命周期 | 由垃圾回收器管理,直到没有引用为止 | 随着方法调用结束自动销毁 |
线程共享 | 所有线程共享 | 每个线程有独立的栈 |
访问速度 | 较慢,因涉及动态分配和垃圾回收 | 快,栈内存是连续的、按顺序分配的 |
内存大小 | 较大,可以根据需要调整 | 较小,受操作系统限制 |
管理方式 | 由垃圾回收器管理 | 由操作系统自动管理 |
总的来说,栈和堆各自有不同的特点和适用场景。栈适合存储局部变量和执行上下文,速度快且自动回收;堆适合存储对象和数组,适用于动态数据分配,但需要垃圾回收器管理内存,速度较慢。