共计 13531 个字符,预计需要花费 34 分钟才能阅读完成。
本文蕴含了 JVM 中内存构造、垃圾回收算法、垃圾回收器、类加载、双亲委派模型、和 JVM 调优的常见面试题。
本文收录于《面试小抄》系列,Github 地址:https://github.com/cosen1024/…
国内 Gitee:https://gitee.com/cosen1024/J…
蕴含了 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 中的存储内容
stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,而后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,而后是函数中的局部变量。留神动态变量是不入栈的。
当本次函数调用完结后,局部变量先出栈,而后是参数,最初栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点持续运行。
heap:个别是在堆的头部用一个字节寄存堆的大小。堆中的具体内容有程序员安顿。
4. 什么状况下会产生栈内存溢出?
1、栈是线程公有的,栈的生命周期和线程一样,每个办法在执行的时候就会创立一个栈帧,它蕴含局部变量表、操作数栈、动静链接、办法进口等信息,局部变量表又包含根本数据类型和对象的援用;
2、当线程申请的栈深度超过了虚拟机容许的最大深度时,会抛出 StackOverFlowError 异样,办法递归调用肯可能会呈现该问题;
3、调整参数 -xss 去调整 jvm 栈的大小
5. 谈谈对 OOM 的意识?如何排查 OOM 的问题?
除了程序计数器,其余内存区域都有 OOM 的危险。
- 栈个别常常会产生 StackOverflowError,比方 32 位的 windows 零碎单过程限度 2G 内存,有限创立线程就会产生栈的 OOM
- Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数有效;
- 堆内存溢出,报错同上,这种比拟好了解,GC 之后无奈在堆中申请内存创建对象就会报错;
- 办法区 OOM,常常会遇到的是动静生成大量的类、jsp 等;
- 间接内存 OOM,波及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的办法:
- 减少两个参数 -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 的对象有以下几种:
- 虚拟机栈中援用的对象
- 办法区类动态属性援用的变量
- 办法区常量池援用的对象
- 本地办法栈 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 虚拟机个别将内存分成新生代和老生代,在新生代中,有大量对象死去和大量对象存活,所以采纳复制算法,只须要付出大量存活对象的复制老本就能够实现收集;老年代中因为对象的存活率极高,没有额定的空间对他进行调配担保,所以采纳标记清理或者标记整顿算法进行回收;
比照
11. 有哪几种垃圾回收器,各自的优缺点是什么?
垃圾回收器次要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
- Serial: 单线程的收集器,收集垃圾时,必须 stop the world,应用复制算法。它的最大特点是在进行垃圾回收时,须要对所有正在执行的线程暂停(stop the world),对于有些利用是难以承受的,然而如果利用的实时性要求不是那么高,只有进展的工夫管制在 N 毫秒之内,大多数利用还是能够承受的,是 client 级别的默认 GC 形式。
- ParNew:Serial 收集器的多线程版本,也须要 stop the world,复制算
- Parallel Scavenge: 新生代收集器,复制算法的收集器,并发的多线程收集器,指标是达到一个可控的吞吐量,和 ParNew 的最大区别是 GC 主动调节策略;虚构机会依据零碎的运行状态收集性能监控信息,动静设置这些参数,以提供最优进展工夫和最高的吞吐量;
- Serial Old:Serial 收集器的老年代版本,单线程收集器,应用标记整顿算法。
- Parallel Old:是 Parallel Scavenge 收集器的老年代版本,应用多线程,标记 - 整顿算法。
- CMS: 是一种以取得最短回收进展工夫为指标的收集器,标记革除算法,运作过程:初始标记,并发标记,从新标记,并发革除,收集完结会产生大量空间碎片;
- G1: 标记整顿算法实现,运作流程次要包含以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,能够准确地管制进展;G1 将整个堆分为大小相等的多个 Region(区域),G1 跟踪每个区域的垃圾大小,在后盾保护一个优先级列表,每次依据容许的收集工夫,优先回收价值最大的区域,已达到在无限工夫内获取尽可能高的回收效率;
垃圾回收器间的配合应用图:
各个垃圾回收器比照:
12. 具体说一下 CMS 的回收过程?CMS 的问题是什么?
CMS(Concurrent Mark Sweep,并发标记革除) 收集器是以获取最短回收进展工夫为指标的收集器(谋求低进展),它在垃圾收集时使得用户线程和 GC 线程并发执行,因而在垃圾收集过程中用户也不会感到显著的卡顿。
从名字就能够晓得,CMS 是基于“标记 - 革除”算法实现的。CMS 回收过程分为以下四步:
- 初始标记(CMS initial mark):次要是标记 GC Root 开始的上级(注:仅下一级)对象,这个过程会 STW,然而跟 GC Root 间接关联的上级对象不会很多,因而这个过程其实很快。
- 并发标记 (CMS concurrent mark):依据上一步的后果,持续向下标识所有关联的对象,直到这条链上的最止境。这个过程是多线程的,尽管耗时实践上会比拟长,然而其它工作线程并不会阻塞,没有 STW。
- 从新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
- 并发革除(CMS concurrent sweep):革除阶段是清理删除掉标记阶段判断的曾经死亡的对象,因为不须要挪动存活对象,所以这个阶段也是能够与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致 CPU 资源缓和:
在并发阶段,它尽管不会导致用户线程进展,但却会因为占用了一部分线程而导致应用程序变慢,升高程序总吞吐量。CMS 默认启动的回收线程数是:(CPU 核数 + 3)/ 4,当 CPU 核数有余四个时,CMS 对用户程序的影响就可能变得很大。
2. 无奈清理浮动垃圾:
在 CMS 的并发标记和并发清理阶段,用户线程还在持续运行,就还会随同有新的垃圾对象一直产生,但这一部分垃圾对象是呈现在标记过程完结当前,CMS 无奈在当次收集中解决掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
因为在垃圾回收阶段用户线程还在并发运行,那就还须要预留足够的内存空间提供给用户线程应用,因而 CMS 不能像其余回收器那样等到老年代简直齐全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行应用。默认状况下,当老年代应用了 92% 的空间后就会触发 CMS 垃圾回收,这个值能够通过 -XX: CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个危险:要是 CMS 运行期间预留的内存无奈满足程序调配新对象的须要,就会呈现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,长期启用 Serial Old 来从新进行老年代的垃圾回收,这样一来进展工夫就很长了。
4. 内存碎片问题:
CMS 是一款基于“标记 - 革除”算法实现的回收器,这意味着回收完结时会有内存碎片产生。内存碎片过多时,将会给大对象调配带来麻烦,往往会呈现老年代还有很多残余空间,但就是无奈找到足够大的间断空间来调配以后对象,而不得不提前触发一次 Full GC 的状况。
为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整顿过程,因为这个内存整理必须挪动存活对象,是无奈并发的,这样进展工夫就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求 CMS 在执行过若干次不整顿空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整顿(默认值为 0,示意每次进入 Full GC 时都进行碎片整顿)。
13. 具体说一下 G1 的回收过程?
G1(Garbage First)回收器采纳面向部分收集的设计思路和基于 Region 的内存布局模式,是一款次要面向服务端利用的垃圾回收器。G1 设计初衷就是替换 CMS,成为一种全功能收集器。G1 在 JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被申明为不举荐应用的垃圾回收器。G1 从整体来看是基于 标记 - 整顿 算法实现的回收器,但从部分(两个 Region 之间)上看又是基于 标记 - 复制 算法实现的。
G1 回收过程,G1 回收器的运作过程大抵可分为四个步骤:
- 初始标记(会 STW):仅仅只是标记一下 GC Roots 能间接关联到的对象,并且批改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中调配新对象。这个阶段须要进展线程,但耗时很短,而且是借用进行 Minor GC 的时候同步实现的,所以 G1 收集器在这个阶段理论并没有额定的进展。
- 并发标记:从 GC Roots 开始对堆中对象进行可达性剖析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描实现当前,还要重新处理在并发时有援用变动的对象。
- 最终标记(会 STW):对用户线程做短暂的暂停,解决并发阶段完结后仍有援用变动的对象。
- 清理阶段(会 STW):更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间。这里的操作波及存活对象的挪动,必须暂停用户线程,由多条回收器线程并行实现的。
14. JVM 中一次残缺的 GC 是什么样子的?
先形容一下 Java 堆内存划分。
在 Java 中,堆被划分成两个不同的区域:新生代 (Young)、老年代 (Old),新生代默认占总空间的 1/3,老年代默认占 2/3。
新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
新生代的垃圾回收(又称 Minor GC)后只有大量对象存活,所以选用复制算法,只须要大量的复制老本就能够实现回收。
老年代的垃圾回收(又称 Major GC)通常应用“标记 - 清理”或“标记 - 整顿”算法。
再形容它们之间转化流程:
对象优先在 Eden 调配。当 eden 区没有足够空间进行调配时,虚拟机将发动一次 Minor GC。
- 在 Eden 区执行了第一次 GC 之后,存活的对象会被挪动到其中一个 Survivor 分区;
- Eden 区再次 GC,这时会采纳复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
- 挪动一次,对象年龄加 1,对象年龄大于肯定阀值会间接挪动到老年代。GC 年龄的阀值能够通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
- 动静对象年龄断定:Survivor 区雷同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个指标使用率)时,大于或等于该年龄的对象间接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;
- Survivor 区内存不足会产生担保调配,超过指定大小的对象能够间接进入老年代。
- 大对象间接进入老年代,大对象就是须要大量间断内存空间的对象(比方:字符串、数组),为了防止为大对象分配内存时因为调配担保机制带来的复制而升高效率。
- 老年代满了而 无奈包容更多的对象 ,Minor GC 之后通常就会进行 Full GC,Full GC 清理整个内存堆 – 包含年老代和老年代。
15. Minor GC 和 Full GC 有什么不同呢?
Minor GC:只收集新生代的 GC。
Full GC: 收集整个堆,包含 新生代,老年代,永恒代 (在 JDK 1.8 及当前,永恒代被移除,换为 metaspace 元空间) 等所有局部的模式。
Minor GC 触发条件:当 Eden 区满时,触发 Minor GC。
Full GC 触发条件:
- 通过 Minor GC 后进入老年代的均匀大小大于老年代的可用内存。如果发现统计数据说之前 Minor GC 的均匀降职大小比目前 old gen 残余的空间大,则不会触发 Minor GC 而是转为触发 full GC。
- 老年代空间不够调配新的内存(或永恒代空间有余,但只是 JDK1.7 有的,这也是用元空间来取代永恒代的起因,能够缩小 Full GC 的频率,缩小 GC 累赘,晋升其效率)。
- 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- 调用 System.gc 时,零碎倡议执行 Full GC,然而不必然执行。
16. 介绍下空间调配担保准则?
如果 YougGC 时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么解决呢?其实 JVM 有一个老年代空间调配担保机制来保障对象可能进入老年代。
在执行每次 YoungGC 之前,JVM 会先查看老年代最大可用间断空间是否大于新生代所有对象的总大小。因为在极其状况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用间断空间是大于新生代所有对象的总大小的,那就能够释怀进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候 JVM 就会先查看 -XX:HandlePromotionFailure 参数是否容许担保失败,如果容许,就会判断老年代最大可用间断空间是否大于历次降职到老年代对象的均匀大小,如果大于,将尝试进行一次 YoungGC,尽快这次 YoungGC 是有危险的。如果小于,或者 -XX:HandlePromotionFailure 参数不容许担保失败,这时就会进行一次 Full GC。
在容许担保失败并尝试进行 YoungGC 后,可能会呈现三种状况:
- ① YoungGC 后,存活对象小于 survivor 大小,此时存活对象进入 survivor 区中
- ② YoungGC 后,存活对象大于 survivor 大小,然而小于老年大可用空间大小,此时间接进入老年代。
- ③ YoungGC 后,存活对象大于 survivor 大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会产生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC 后,老年代还是没有足够的空间,此时就会产生 OOM 内存溢出了。
通过下图来理解空间调配担保准则:
17. 什么是类加载?类加载的过程?
虚拟机把形容类的数据加载到内存外面,并对数据进行校验、解析和初始化,最终变成能够被虚拟机间接应用的 class 对象;
类的整个生命周期包含:加载(Loading)、验证(Verification)、筹备 (Preparation)、解析(Resolution)、初始化(Initialization)、应用(Using) 和卸载(Unloading)7 个阶段。其中筹备、验证、解析 3 个局部统称为连贯(Linking)。如图所示:
加载、验证、筹备、初始化和卸载这 5 个阶段的程序是确定的,类的加载过程必须依照这种程序循序渐进地开始,而解析阶段则不肯定:它在某些状况下能够在初始化阶段之后再开始,这是为了反对 Java 语言的运行时绑定(也称为动静绑定或早期绑定)
类加载过程如下:
- 加载,加载分为三步:
1、通过类的全限定性类名获取该类的二进制流;
2、将该二进制流的动态存储构造转为办法区的运行时数据结构;
3、在堆中为该类生成一个 class 对象; - 验证:验证该 class 文件中的字节流信息复合虚拟机的要求,不会威逼到 jvm 的平安;
- 筹备:为 class 对象的动态变量分配内存,初始化其初始值;
- 解析:该阶段次要实现符号援用转化成间接援用;
- 初始化:到了初始化阶段,才开始执行类中定义的 java 代码;初始化阶段是调用类结构器的过程;
18. 什么是类加载器,常见的类加载器有哪些?
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:
- 启动类加载器(BootStrapClassLoader):用来加载 java 外围类库,无奈被 java 程序间接援用;
- 扩大类加载器(Extension ClassLoader):用来加载 java 的扩大库,java 的虚拟机实现会提供一个扩大库目录,该类加载器在扩大库目录外面查找并加载 java 类;
- 零碎类加载器(AppClassLoader):它依据 java 的类门路来加载类,一般来说,java 利用的类都是通过它来加载的;
- 自定义类加载器:由 java 语言实现,继承自 ClassLoader;
19. 什么是双亲委派模型?为什么须要双亲委派模型?
当一个类加载器收到一个类加载的申请,他首先不会尝试本人去加载,而是将这个申请委派给父类加载器去加载,只有父类加载器在本人的搜寻范畴类查找不到给类时,子加载器才会尝试本人去加载该类;
为了避免内存中呈现多个雷同的字节码;因为如果没有双亲委派的话,用户就能够本人定义一个 java.lang.String 类,那么就无奈保障类的唯一性。
补充:那怎么突破双亲委派模型?
自定义类加载器,继承 ClassLoader 类,重写 loadClass 办法和 findClass 办法。
20. 列举一些你晓得的突破双亲委派机制的例子,为什么要突破?
- JNDI 通过引入线程上下文类加载器,能够在 Thread.setContextClassLoader 办法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就能够实现父类加载器申请子类加载器实现类加载的行为。突破的起因,是为了 JNDI 服务的类加载器是启动器类加载,为了实现高级类加载器申请子类加载器(即上文中的线程上下文加载器)加载类。
Tomcat,利用的类加载器优先自行加载利用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。
tomcat 之所以造了一堆本人的 classloader,大抵是出于上面三类目标:
- 对于各个
webapp
中的class
和lib
,须要互相隔离,不能呈现一个利用中加载的类库会影响另一个利用的状况,而对于许多利用,须要有共享的 lib 以便不浪费资源。 - 与
jvm
一样的安全性问题。应用独自的classloader
去装载tomcat
本身的类库,免得其余歹意或无心的毁坏; - 热部署。
tomcat 类加载器如下图:
- 对于各个
- OSGi,实现模块化热部署,为每个模块都自定义了类加载器,须要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。突破的起因是为了实现模块热替换。
- JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载申请,在委派给父加载器加载前,要先判断该类是否可能归属到某一个零碎模块中,如果能够找到这样的归属关系,就要优先委派给负责那个模块的加载器实现加载。突破的起因,是为了增加模块化的个性。
21. 说一下 JVM 调优的命令?
- jps:JVM Process Status Tool, 显示指定零碎内所有的 HotSpot 虚拟机过程。
- jstat:jstat(JVM statistics Monitoring)是用于监督虚拟机运行时状态信息的命令,它能够显示出虚拟机过程中的类装载、内存、垃圾收集、JIT 编译等运行数据。
- jmap:jmap(JVM Memory Map)命令用于生成 heap dump 文件,如果不应用这个命令,还阔以应用 -XX:+HeapDumpOnOutOfMemoryError 参数来让虚拟机呈现 OOM 的时候·主动生成 dump 文件。
jmap 不仅能生成 dump 文件,还阔以查问 finalize 执行队列、Java 堆和永恒代的详细信息,如以后使用率、以后应用的是哪种收集器等。 - jhat:jhat(JVM Heap Analysis Tool)命令是与 jmap 搭配应用,用来剖析 jmap 生成的 dump,jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的剖析后果后,能够在浏览器中查看。在此要留神,个别不会间接在服务器上进行剖析,因为 jhat 是一个耗时并且消耗硬件资源的过程,个别把服务器生成的 dump 文件复制到本地或其余机器上进行剖析。
- jstack:jstack 用于生成 java 虚拟机以后时刻的线程快照。jstack 来查看各个线程的调用堆栈,就能够晓得没有响应的线程到底在后盾做什么事件,或者期待什么资源。如果 java 程序解体生成 core 文件,jstack 工具能够用来取得 core 文件的 java stack 和 native stack 的信息,从而能够轻松地晓得 java 程序是如何解体和在程序何处产生问题。
伟人的肩膀
https://jishuin.proginn.com/p…
https://www.javanav.com/val/9…
https://juejin.cn/post/684490…
https://www.cnblogs.com/chian…
https://juejin.cn/post/684490…
https://segmentfault.com/a/11…
这里也举荐一个我收集的计算机书籍仓库,仓库目前有上百本经典 cs 电子书,看经典的书籍会更悟得深~
点此链接即可中转书单,计算机必看经典书籍(含 pdf 下载)
Github 也有相应仓库,https://github.com/cosen1024/…
欢送 star。