JVM经典⾯试题(2021最新版)
本⽂包含了JVM中内存结构、垃圾回收算法、垃圾回收器、类加载、双亲委派模型、和JVM调优的常见⾯试题。
包含了Java基础、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统和计算机⽹络等的常考⾯试题。这是本期的 JVM⾯试题⽬录,不会的快快查漏补缺~
1. 什么是JVM内存结构?
jvm将虚拟机分为5⼤区域,程序计数器、虚拟机栈、本地⽅法栈、java堆、⽅法区;
程序计数器:线程私有的,是⼀块很⼩的内存空间,作为当前线程的⾏号指⽰器,⽤于记录当前虚拟机正在执⾏的线程指令地址;
虚拟机栈:线程私有的,每个⽅法执⾏的时候都会创建⼀个栈帧,⽤于存储局部变量表、操作数、动态链接和⽅法返回等信息,当线程请求的栈深度超过了虚拟机允许的最⼤深度时,就会抛出StackOverFlowError;
本地⽅法栈:线程私有的,保存的是native⽅法的信息,当⼀个jvm创建的线程调⽤native⽅法后,jvm不会在虚拟机栈中为该线程创建栈帧,⽽是简单的动态链接并直接调⽤该⽅法;
堆:java堆是所有线程共享的⼀块内存,⼏乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发⽣垃圾回收的操作;
⽅法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在⽅法区了,被元数据区替代了,原⽅法区被分成两部分;1:加载的类信息,2:运⾏时常量池;加载的类信息被保存在元数据区中,运⾏时常量池保存在堆中;
2. 什么是JVM内存模型?
Java 内存模型(下⽂简称 JMM)就是在底层处理器内存模型的基础上,定义⾃⼰的多线程语义。它明确指定了⼀组排序规则,来保证线程间的可见性。
这⼀组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(⽆论它们
是否在同⼀个线程),那么 A 和 B 之间必须满⾜ Happens-Before 关系:
单线程规则:⼀个线程中的每个动作都 happens-before 该线程中后续的每个动作
监视器锁定规则:的解锁动作 happens-before 后续对这个的锁定动作
volatile 变量规则:对 volatile 字段的写⼊动作 happens-before 后续对这个字段的每个读取动作
线程 start 规则:线程 start() ⽅法的执⾏ happens-before ⼀个启动线程内的任意动作
线程 join 规则:⼀个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
怎么理解 happens-before 呢?如果按字⾯意思,⽐如第⼆个规则,线程(不管是不是同⼀个)的解锁动作发⽣在锁定之前?这明显不对。happens-before 也是为了保证可见性,⽐如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进⼊同步块,那么线程2就能看见线程1对共享对象修改的结果。
Java 提供了⼏种语⾔结构,包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序的并发要求,其中:
volatile - 保证可见性和有序性
synchronized - 保证可见性和有序性; 通过管程(Monitor)\保证⼀组动作的*原⼦性*
final - 通过禁⽌在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引⽤逃逸就不好说可见性了)
编译器在遇到这些关键字时,会插⼊相应的内存屏障,保证语义的正确性。
有⼀点需要注意的是,synchronized 不保证同步块内的代码禁⽌重排序,因为它通过锁保证同⼀时刻只有⼀个线程访问同步块(或临界区),也就是说同步块的代码只需满⾜ as-if-serial 语义 - 只要单线程的执⾏结果不改变,可以进⾏重排序。
所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的 Java 代码可以在不同体系结构的处理器上正确运⾏。
3. heap 和stack 有什么区别?
(1)申请⽅式
stack:由系统⾃动分配。例如,声明在函数中⼀个局部变量 int b; 系统⾃动在栈中为 b 开辟空间
heap:需要程序员⾃⼰申请,并指明⼤⼩,在 c 中 malloc 函数,对于Java 需要⼿动 new Object()的形式开辟
(2)申请后系统的响应
stack:只要栈的剩余空间⼤于所申请空间,系统将为程序提供内存,否则将报异常提⽰栈溢出。
heap:⾸先应该知道操作系统有⼀个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻第⼀个空间⼤于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于到的堆结点的⼤⼩不⼀定正好等于申请的⼤⼩,系统会⾃动的将多余的那部分重新放⼊空闲链表中。
(3)申请⼤⼩的限制
stack:栈是向低地址扩展的数据结构,是⼀块连续的内存的区域。这句话的意思是栈顶的地址和栈的最⼤容量是系统预先规定好的,在WINDOWS 下,栈的⼤⼩是 2M(默认值也取决于虚拟内存的⼤⼩),如果申请的空间超过栈的剩余空间时,将提⽰ overflow。因此,能从栈获得的空间较⼩。
heap:堆是向⾼地址扩展的数据结构,是不连续的内存区域。这是由于系统是⽤链表来存储的空闲内存地址的, ⾃然是不连续的,⽽链表的遍历⽅向是由低地址向⾼地址。堆的⼤⼩受限于计算机系统中有效的虚拟内存。由此可见, 堆获得的空间⽐较灵活,也⽐较⼤。
(4)申请效率的⽐较
stack:由系统⾃动分配,速度较快。但程序员是⽆法控制的。
heap:由 new 分配的内存,⼀般速度⽐较慢,⽽且容易产⽣内存碎⽚,不过⽤起来最⽅便。
(5)heap和stack中的存储内容
mysql面试题常问怎么打开pdf格式的文件stack:在函数调⽤时,第⼀个进栈的是主函数中后的下⼀条指令(函数调⽤语句的下⼀条可执⾏语句)的地址, 然后是函数的各个参数,在⼤多数的 C 编译器中,参数是由右往左⼊栈的,然后是函数中的局部变量。注意静态变量是不⼊栈的。
当本次函数调⽤结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下⼀条指令,程序由该点继续运⾏。
heap:⼀般是在堆的头部⽤⼀个字节存放堆的⼤⼩。堆中的具体内容有程序员安排。
4. 什么情况下会发⽣栈内存溢出?
1、栈是线程私有的,栈的⽣命周期和线程⼀样,每个⽅法在执⾏的时候就会创建⼀个栈帧,它包含局部变量表、操作数栈、动态链接、⽅法出⼝等信息,局部变量表⼜包括基本数据类型和对象的引⽤;
2、当线程请求的栈深度超过了虚拟机允许的最⼤深度时,会抛出StackOverFlowError异常,⽅法递归调⽤肯可能会出现该问题;
3、调整参数-xss去调整jvm栈的⼤⼩
5. 谈谈对 OOM 的认识?如何排查 OOM 的问题?
除了程序计数器,其他内存区域都有 OOM 的风险。
什么情况需要使用redis持久化栈⼀般经常会发⽣ StackOverflowError,⽐如 32 位的 windows 系统单进程限制 2G 内存,⽆限创建线程就会发⽣栈的 OOM Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最⼤元空间⼤⼩参数⽆效;
堆内存溢出,报错同上,这种⽐较好理解,GC 之后⽆法在堆中申请内存创建对象就会报错;
⽅法区 OOM,经常会遇到的是动态⽣成⼤量的类、jsp 等;js异步前端面试
直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的⽅法:
10000000的原码增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发⽣时⾃动dump 堆内存信息到指定⽬录;
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题⼤概出在什么区域;
使⽤ MAT ⼯具载⼊到 dump ⽂件,分析⼤对象的占⽤情况,⽐如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引⽤ 。
6. 谈谈 JVM 中的常量池?
JVM常量池主要分为Class⽂件常量池、运⾏时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
Class⽂件常量池。class⽂件是⼀组以字节为单位的⼆进制数据流,在java代码的编译期间,我们编写的java⽂件就被编译为.class⽂件格式的⼆进制数据存放在磁盘中,其中就包括class⽂件常量池。
运⾏时常量池:运⾏时常量池相对于class常量池⼀⼤特征就是具有动态性,java规范并不要求常量只能在运⾏时才产⽣,也就是说运⾏时常量池的内容并不全部来⾃class常量池,在运⾏时可以通过代码⽣成常量并将其放⼊运⾏时常量池中,这种特性被⽤的最多的就是String.intern()。
全局字符串常量池:字符串常量池是JVM所维护的⼀个字符串实例的引⽤表,在HotSpot VM中,它是⼀个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引⽤,底层C++实现就是⼀个Hashtable。这些被维护的引⽤所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进⼊了字符串常量池的字符串”。
基本类型包装类对象常量池:java中基本类型的包装类的⼤部分都实现了常量池技术,这些类是
Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上⾯这5种整型的包装类也只是在对应值⼩于等于127时才可使⽤对象池,也即对象不负责创建和管理⼤于127的这些类的对象。
7. 如何判断⼀个对象是否存活?
判断⼀个对象是否存活,分为两种算法1:引⽤计数法;2:可达性分析算法;
引⽤计数法: 给每⼀个对象设置⼀个引⽤计数器,当有⼀个地⽅引⽤该对象的时候,引⽤计数器就+1,引⽤失效时,引⽤计数器就-1;当引⽤计数器为0的时候,就说明这个对象没有被引⽤,也就是垃圾对象,等待回收; 缺点:⽆法解决循环引⽤的问题,当A引⽤B,B也引⽤A的时候,此时AB对象的引⽤都不为0,此时也就⽆法垃圾回收,所以⼀般主流虚拟机都不采⽤这个⽅法;
可达性分析法 从⼀个被称为GC Roots的对象向下搜索,如果⼀个对象到GC Roots没有任何引⽤链相连接时,说明此对象不可⽤,在java 中可以作为GC Roots的对象有以下⼏种:
虚拟机栈中引⽤的对象
⽅法区类静态属性引⽤的变量
⽅法区常量池引⽤的对象
javascript的源代码加密保护本地⽅法栈JNI引⽤的对象
但⼀个对象满⾜上述条件的时候,不会马上被回收,还需要进⾏两次标记;第⼀次标记:判断当前对象是否有finalize()⽅法并且该⽅法没有被执⾏过,若不存在则标记为垃圾对象,等待回收;若有的话,则进⾏第⼆次标记;第⼆次标记将当前对象放⼊F-Queue队列,并⽣成⼀个finalize线程去执⾏该⽅法,虚拟机不保证该⽅法⼀定会被执⾏,这是因为如果线程执⾏缓慢或进⼊了死锁,会导致回收系统的崩溃;如果执⾏了finalize⽅法之后仍然没有与GC Roots有直接或者间接的引⽤,则该对象会被回收;
8. 强引⽤、软引⽤、弱引⽤、虚引⽤是什么,有什么区别?
强引⽤,就是普通的对象引⽤关系,如 String s = new String("ConstXiong")
软引⽤,⽤于维护⼀些可有可⽆的对象。只有在内存不⾜时,系统则会回收软引⽤对象,如果回收了软引⽤对象之后仍然没有⾜够的内存,才会抛出内存溢出异常。SoftReference 实现
弱引⽤,相⽐软引⽤来说,要更加⽆⽤⼀些,它拥有更短的⽣命周期,当 JVM 进⾏垃圾回收时,⽆论内存是否充⾜,都会回收被弱引⽤关联的对象。WeakReference 实现
虚引⽤是⼀种形同虚设的引⽤,在现实场景中⽤的不是很多,它主要⽤来跟踪对象被垃圾回收的活动。PhantomReference 实现9. 被引⽤的对象就⼀定能存活吗?
不⼀定,看 Reference 类型,弱引⽤在 GC 时会被回收,软引⽤在内存不⾜的时候,即 OOM 前会被回收,但如果没有在
Reference Chain 中的对象就⼀定会被回收。
10. Java中的垃圾回收算法有哪些?
java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法; 标记清除法: 第⼀步:利⽤可达性去遍历内存,把存活对象和垃圾对象进⾏标记; 第⼆步:在遍历⼀遍,将所有标记的对象回收掉; 特点:效率不⾏,标记和清除的效率都不⾼;标记和清除后会产⽣⼤量的不连续的空间分⽚,可能会导致之后程序运⾏的时候需分配⼤对象⽽不到连续分⽚⽽不得不触发⼀次GC;
标记整理法: 第⼀步:利⽤可达性去遍历内存,把存活对象和垃圾对象进⾏标记; 第⼆步:将所有的存活的对象向⼀段移动,将端边界以外的对象都回收掉; 特点:适⽤于存活对象多,垃圾少的情况;需要整理的过程,⽆空间碎⽚产⽣;
复制算法: 将内存按照容量⼤⼩分为⼤⼩相等的两块,每次只使⽤⼀块,当⼀块使⽤完了,就将还存活的对象移到另⼀块上,然后在把使⽤过的内存空间移除; 特点:不会产⽣空间碎⽚;内存使⽤率极低;
分代收集算法: 根据内存对象的存活周期不同,将内存划分成⼏块,java虚拟机⼀般将内存分成新⽣代和⽼⽣代,在新⽣代中,有⼤量对象死去和少量对象存活,所以采⽤复制算法,只需要付出少量存
活对象的复制成本就可以完成收集;⽼年代中因为对象的存活率极⾼,没有额外的空间对他进⾏分配担保,所以采⽤标记清理或者标记整理算法进⾏回收;
对⽐
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论