校招笔记(一)_Java_JVM
我的校招记录:校招笔记(零)_写在前面 ,以下是校招笔记总目录。
备注 | ||
---|---|---|
算法能力(“刷题”) | 这部分就是耗时间多练习,Leetcode-Top100 是很好的选择。 | 补充练习:codeTop |
计算机基础(上)(“八股”) | 校招笔记(一)__Java_Java入门 | C++后端后续更新 |
校招笔记(一)__Java_面对对象 | ||
校招笔记(一)__Java_集合 | ||
校招笔记(一)__Java_多线程 | ||
校招笔记(一)__Java_锁 | ||
校招笔记(一)__Java_JVM | ||
计算机基础(下)(“八股”) | 校招笔记(二)__计算机基础_Linux&Git | |
校招笔记(三)__计算机基础_计算机网络 | ||
校招笔记(四)__计算机基础_操作系统 | ||
校招笔记(五)__计算机基础_MySQL | ||
校招笔记(六)__计算机基础_Redis | ||
校招笔记(七)__计算机基础_数据结构 | ||
校招笔记(八)__计算机基础_场景&智力题 | ||
校招笔记(九)__计算机基础_相关补充 | ||
项目&实习 | 主要是怎么准备项目,后续更新 |
1.6 JVM相关
1.6.1 常问问题
1. (被问过)JVM启动的基本配置参数有哪些?(或者说调优参数)
「堆栈内存相关」
-
-Xms: 设置初始堆的大小
-
-Xmx: 设置最大堆的大小
-
-Xmn :设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
-
-Xss: 每个线程的堆栈大小
-
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-
-XX:NewRatio 年轻代与年老代的比值(除去持久代)
-
-XX:SurvivorRatio Eden区与Survivor区的的比值
-
-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
-
-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
「垃圾收集器相关」
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。
-
-XX:ParallelGCThreads=20:配置并行收集器的线程数
-
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。
-
-XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
-
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
「辅助信息相关」
-
-XX:+PrintGCDetails 打印GC详细信息
-
-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用
-
-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
-
-XX:+PrintTLAB 查看TLAB空间的使用情况
2.说说堆和栈的区别?
-
功能不同:栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象;
注:无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
-
共享性不同:栈内存是线程私有的; 堆内存是所有线程共有的;
-
异常错误不同:如果栈内存或者堆内存不足,都会抛出异常但不同;
栈空间不足:
java.lang.StackOverFlowError
。 堆空间不足:java.lang.OutOfMemoryError
; -
空间大小:栈的空间大小远远小于堆的。
3.【重点】请问JVM运行时内存布局分布? 哪些是线程共享的?
从概念上大致分为 6 个(逻辑)区域,参考下图。注:Method Area 中还有一个常量池区,图中未明确标出。
- 总的来看,JVM 把内存划分为“栈(stack)”与“堆(heap)”两大类
线程私有的:
-
程序计数器:当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令;
-
虚拟机栈: 每个java方法执行时都会创建一个桢栈来存储方法的:
-
变量表、操作数栈、动态链接方法、返回值、返回地址等信息;
-
栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法),“栈帧参考” ;
-
-
本地方法栈: 与虚拟机栈作用相似。但它不是为Java方法服务的,而是调用操作系统原生本地方法时,所需要的内存区域。
- 本地方法被执行的时候,在本地方法栈也会创建⼀个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
线程共享的:
-
堆:GC 垃圾回收的主站场,用于存放类的实例对象及 数组实例等;
-
方法区: 线程共享的,用于存放被虚拟机加载的类的元数据信息:
- 如 常量、静态变量和 即时编译器编译后的代码;
- 运行时常量池:字符串,int -128~127 范围的值等,它是 方法区 中的一部分。
3.1 JVM 堆中又怎么分的?为什么要这么划分?为什么要取消永久代用元空间代替?
-
堆划分:堆又被划分为,新生代,老年代。新生代又被划分为eden和survivor区。
-
为什么划分新生代、老年代?
因为有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。提高效率。
-
代替: 在java7版本前,堆和方法区连在了一起,但这并不能说堆和方法区是一起的,它们在逻辑上依旧是分开的。但在物理上来说,它们又是连续的一块内存。
也就是说,方法区和前面讲到的Eden和老年代是连续的。
永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。本地内存(Native memory),也称为C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。
元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。看上图中的方法区,是不是“膨胀”了。
3.2 程序计数器存哪些东西?能为空吗?
程序计数器是用于存放下一条指令所在单元的地址的地方。
如果正在执行的是Native方法,这个计数器值为空(Ubdifined)。
4. (新)说一下Java创建对象的过程?
-
类加载检查: 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
分配内存: 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定大小的内存从 Java 堆中划分出来。
(重要)分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整⼜由所采用的垃圾收集器是否带有压缩整理功能决定。
-
初始化对象字段零值。 内存分配完成后,进行对象初始化操作。
例如给对象中(区分类加载过程,初始化类变量)所有的基本数据变量赋上初始化值, 当我们没有对它们进行赋值操作时就可以使用对象了。
-
设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
-
执行init() & 构造函数。然后执行对象内部生成的init方法,然后执行构造器方法(程序员设置的)。
-
内存地址赋给引用: 在虚拟机会将刚创建好对象的内存地址赋给引用对象。完成此操作后,便可以在程序中通过引用访问对象的实例数据。
4.1 什么是指针碰撞?空闲列表?TLAB?
三种虚拟机划分堆内存区域的方法,TLAB也可用来给堆内存分配空间。
-
指针碰撞
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。
- 如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞。
-
空闲列表
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞。
- 虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
-
TLAB
TLAB 是虚拟机在内存的eden 区划分出来的一块专用空间,是线程专属的。 在启用TLAB 的情况下,当线程被创建时,虚拟机会为每个线程分配一块TLAB 空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提高分配效率。
虚拟机通过 -XX:UseTLAB 设定它的。
4.2 JVM如何保证对象分配的线程安全问题?
因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- TLAB: 为每⼀个线程预先在Eden区分配⼀块⼉内存TLAB,JVM在给线程中的对象分配内存时,⾸先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用的CAS进行内存分配。
- CAS+失败重试: CAS 是乐观锁的⼀种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
5.描述一下对象分配【原则】?(区分前面new对象【过程】)
JVM 中的堆可划分为两大部分,新生代和老年代,大小比例为1:2:
- 新生代分为 Eden 区和 Survivor 区, Survivor 幸存者区又分为大小相等的两块 from 和 to 区
具体回收过程:
对象的正常流程:Eden 区 -> Survivor 区 -> 老年代。
- 新的对象优先在 Eden 区分配(大对象直接进入老年区,避免在Eden区及两个Survivor区之间发生大量的内存复制) ,当 Eden 区没有足够空间时,会发起一次Minor GC;
- Minor GC采用复制回收算法的改进版本回收Eden中对象:
- 先将Eden存活对象迁移到 to 区,然后清空Eden
- 最后交换to和from区域标签
- 每经过一次Minor GC (在交换区)后对象年龄加1,对象年龄达到15次后将会晋升到老年代;
- 对象提前晋升到老年代,动态年龄判定:如果在 Survivor 区中相同年龄所有对象大小总和大于 Survivor 区大小的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代;
- 老年代空间不够时进行Full GC。
5.1 对象一定分配到堆上吗?
小伙,来给我讲一下是不是所有的对象和数组都会在堆内存分配空间?
并不是,这涉及到对象逃逸。请看下面的代码:
StringBuffer对象,最终会被return,也就是会被该方法之外的给利用。 发生了对象逃逸。
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。 但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
6.对象的访问定位有哪两种方式?
建⽴对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,⽬前主流的访问方式有:①使用句柄和 ②直接指针两种。
-
句柄: 如果使用句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据(存放在方法区)各⾃的具体地址信息;
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
-
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
使用直接指针访问方式最大的好处就是速度快,它节省了⼀次指针定位的时间开销。
7.如何判断对象是否需要回收(死亡)?
- 可达性分析:根据引用的关系构造引用链(有向图),在图中不可达的对象就是要被回收的;
- 引用计数:有地方引用该对象,计数器++,引用失效,计数器–,有循环引用的问题。
8. 如何判断一个常量是废弃变量?如何判断一个类是无用类?
-
判断常量是废弃变量
运行时常量池主要回收的是废弃的常量。
假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量"abc" 就是废弃常量,如果这时发⽣内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
-
判断类是无用类?
虚拟机可以对满足下面3个条件的无用类进行回收,这⾥说的仅仅是“可以”,而并不是和对象⼀样不使用了就会必然被回收。
-
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
-
加载该类的 ClassLoader 已经被回收;
-
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
1.6.1 GC相关
不错的GC介绍文章:一文看懂 JVM 内存布局及 GC 原理
1.请介绍GC基本原理?有什么优点?GC可以马上回收内存吗?如何主动让虚拟机回收?
当程序员创建对象时,GC就开始监控这个对象的地址、大小及使用情况 ,当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间 :
- 基本原理: GC采用有向图的方式记录和管理堆(heap)中的所有对象 ,并确定对象是否“可达”
- GC优点:(1)使得程序员不用考虑内存管理 (2)Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域" (3)有效防止内存泄漏
- 马上回收: 程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
- 主动回收: 手动执行
System.gc()
,但是Java语言规范 并不保证 GC一定会执行。
1.1 本地java程序默认启动的是什么垃圾回收器?
cmd执行命令:
1 | java -XX:+PrintCommandLineFlags -version |
2.什么是GC Root?可以作为GC Root的对象?
-
GC Root : 判断对象是通过可达性分析,所以所有的可达性算法都会有起点 ,这就是 GC Root。
- 特点:当前时刻存活的对象!
通过GC Root 找出所有活的对象,那么剩下所有的没有标记的对象就是需要回收的对象。
-
GC Root对象: (1)虚拟机栈中引用的对象;(2)方法区中的静态变量、常量对象;(3)本地方法引用的对象;(4)被
synchronized
修饰的对象等。
3.哪些内存区域需要GC?
-
(无需)线程独享区域:
PC Regiester、JVM Stack、Native Method Stack
,其生命周期都与线程相同(即:与线程共生死),所以无需 GC; -
(需)线程共享的 Heap 区、Method Area 则是 GC 关注的重点对象。
4.什么时候会触发Full GC?
-
调用 System.gc() ;
-
老年代空间不足;
-
通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
如果发现统计之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
-
方法区空间不足。
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
。
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
5.什么时候触发 Minor GC ? 什么原因会导致minor gc运行频繁?同样的,什么原因又会导致minor gc运行很慢?
-
什么时候触发Minor GC ?
当Eden区不足时就会触发 Minor GC 。
-
minor gc运行频繁
-
产生了太多朝生夕灭的对象导致需要频繁minor gc
-
新生代空间设置太小
-
minor gc运行很慢
- 新生代空间设置过大;
- 对象引用链较长,进行可达性分析时间较长;
- 新生代survivor区设置的比较小,清理后剩余的对象不能装进去需要移动到老年代,造成移动开销;
- 内存分配担保失败,由minor gc转化为full gc;
- 采用的垃圾收集器效率较低,比如新生代使用serial收集器。
6.描述一下GC算法?
GC算法包含:引用计数法,标记清除,标记复制,标记压缩。
- 引用计数:对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用;
- 标记清除: 垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象;然后,在清除阶段,清除所有未被标记的对象,但会产生很多垃圾碎片 ;
- 标记复制: 将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50%的内存;
- 标记压缩(标记整理):标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(类似于 windows 的磁盘碎片整理),避免垃圾碎片。
6.1 新生代和老年代用哪些算法?stw问题会发生在新生代吗?
-
针对新生代,采用标记复制算法
大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
HotSpot实现的复制算法流程如下:
- 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
- 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
- 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
-
针对老年代对象存活率高的特点
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。
老年代的垃圾回收(又称Major GC)通常使用标记-清理或标记-整理算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
7.什么是Stop The World ? 什么是安全点?安全区域?
-
Stop The World
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。JVM在暂停的时候,需要选准一个时机。
由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点的概念。
-
安全点
安全点,即程序(非GC先)执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。
-
如何保证中断时所有线程都在安全点
-
抢先式中断(Preemptive Suspension)
抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。 -
主动式中断(Voluntary Suspension)
主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
-
8. 常见的垃圾回收器有哪些(比如G1)?
-
Serial收集器。 Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是⼀个单线程收集器了。它的单线程 的意义不仅仅意味着它只会使用⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进行垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( “Stop The World”),直到它收集结束。
-
ParNew收集器。 ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全⼀样。
- 新⽣代采用标记-复制算法,⽼年代采用标记-整理算法。
-
Parallel Scavenge收集器。 Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢? Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。
如果对于收集器运作不太了解的话,⼿⼯优化存在的话可以选择把内存管理优化交给虚拟机去完成也是⼀个不错的选择。-
- 新⽣代采用标记-复制算法,⽼年代采用标记-整理算法。
-
(重点)CMS收集器。CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以
并发
执行,以此来达到降低收集停顿时间的目的。- CMS收集器仅作用于老年代的收集,基于 “标记-清除” 。
- 优点: 并发收集、低停顿。
- 缺点(快手): CMS收集器对CPU资源非常敏感;CMS收集器无法处理浮动垃圾(Floating Garbage)。
-
(重点)G1收集器。 G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。
- 并行与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概念。
- 空间整合:与CMS的“标记–清除”算法不同,G1从整体来看是基于标记-整理算法实现的收集器;从局部上来看是基于标记-复制算法实现的。
- 可预测的停顿:这是G1相对于CMS的另⼀个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使用者明确指定在⼀个⻓度为M毫秒的时间片段内。
8.1 介绍一下CMS 和 G1 垃圾回收器原理?
-
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的。
CMS收集器仅作用于老年代的收集,是基于标记-清除算法的,它的运作过程分为4个步骤:
- 初始标记(CMS initial mark) : 需要STW,标记一下GC Roots能直接关联到的对象 ;
- 并发标记(CMS concurrent mark):从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行;
- 重新标记(CMS remark):需要STW ,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 ;
- 并发清除(CMS concurrent sweep): 清除对象。
-
G1收集器
在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1将堆分成许多相同大小的区域单元,每个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成如下图所示:
区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。
和CMS挺像的。
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
- 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
8.2 为什么CMS不用标记-压缩算法?
CMS主要关注低延迟,因而采用并发方式。
在并发清理垃圾时,如果采用压缩算法,这涉及到移动存活的对象。如果不进行停顿会很难处理,违背了CMS初衷,所以采用标记-清除算法。
9.什么是内存泄漏?
广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄漏。
内存泄漏场景:
-
静态集合类、数组添加对象以后遗漏了对于他们的处理,例如HashMap和Vector;
-
各种连接,如数据库连接、网络连接、IO连接;
-
单例模式;
-
变量不合理的作用域。
1.6.2 类加载
1. 介绍一下类文件结构?
Class⽂件字节码结构组织示意图 。
- 魔数: 确定这个⽂件是否为⼀个能被虚拟机接收的 Class ⽂件。
- Class ⽂件版本:Class ⽂件的版本号,保证编译正常执行。
- 常量池 :常量池主要存放两大常量:字面量和符号引用。
- 访问标志:标志用于识别⼀些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
- 当前类索引,⽗类索引 :类索引用于确定这个类的全限定名,⽗类索引用于确定这个类的⽗类的全限定名,由于 Java 语⾔的单继承,所以⽗类索引只有⼀个,除了 java.lang.Object 之外,所有的 java 类都有⽗类,因此除了 java.lang.Object 外,所有 Java 类的⽗类索引
都不为 0。 - 接口索引集合:接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implents (如果这个类本身是接口的话则是 extends ) 后的接口顺序从左到右排列在接口索引集合中。
- 字段表集合:描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
- 方法表集合 :类中的方法。
- 属性表集合: 在 Class ⽂件,字段表,方法表中都可以携带⾃⼰的属性表集合。
2. 什么是类加载器?请你解释一下java程序运行的过程?
参考 : 一个简单java程序的运行全过程
类加载器是一个用来加载类文件的类 。
我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存。主要分为以下3大步、5小步:
-
编译。 将编译的
.java
文件编译为.class
字节码文件,然后交给JVM运行; -
加载 :class字节码文件从各个来源通过类加载器装载入内存中 。
- 来源:包本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
- 类加载器 :一般包括启动类加载器,扩展类加载器,系统类加载器,以及用户的自定义类加载器 (代码加密防止反编译)。
-
链接: 分为,验证、准备、解析(”正-中-准-心“)三阶段
-
验证: 保证加载进来的字节流符合虚拟机规范;
文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
-
准备:类变量(注意,不是实例变量 )分配内存,并且赋予初值(虚拟机根据不同类型设定的初始值);
这些变量所使用的内存都将在方法区中进行分配 ,进行零初始化:
- 即数字类型初始化为 0 ,boolean 初始化为 false,引用类型初始化为 null 等
-
解析:将常量池内的 符号引用 替换为 直接引用 的过程。
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。🌾 实例: 调用方法hello(),方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
-
-
初始化。 初始化就是执行类的
cinit
()的过程。❔ 和准备阶段那个初始化看得我有点迷糊?
- 在编译阶段,编译器收集所有的静态字段的赋值语句及静态代码块,并按 语句出现的顺序 拼接出一个类初始化方法
<clinit>()
。此时,执行引擎会调用这个方法对静态字段进行代码中编写的初始化操作。
- 在编译阶段,编译器收集所有的静态字段的赋值语句及静态代码块,并按 语句出现的顺序 拼接出一个类初始化方法
3.知道类的生命周期吗?
在类加载的过程再加上:
- 使用。new出对象程序中使用
- 卸载。执行垃圾回收
4.请你介绍一下类加载器?
JVM预定义的三种类型类加载器:
- 启动类加载器(BootstrapClassLoader):是一般用本地代码实现,负责将
<Java_Runtime_Home>/lib
下面的类库加载到内存中; - 标准扩展类加载器(ExtensionClassLoader):
< Java_Runtime_Home >/lib/ext
或者由系统变量java.ext.dir
指定位置中的类库加载到内存中; - 系统类加载器(AppClassLoader):又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量或者系统属性
java.class.path
所指定的目录中加载类,是用户自定义加载器的默认父加载器。
5.请你介绍一下双亲委派机制?为什么要这么做?
-
双亲委派机制。某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载;
-
为什么要这么做?防止内存中出现多份同样的字节码 。
比如两个类A和类B都要加载System类:
- 如果不用委托:而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
- 如果使用委托机制:会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。
5.1 如何自定义类加载器 ,如何打破双亲委派机制 ?
-
自定义类加载器 & 打破双亲委派机制
在实现自己的ClassLoader之前,我们先看一下JDK中的ClassLoader是怎么实现的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
231 protected synchronized Class<?> loadClass(String name, boolean resolve)
2 throws ClassNotFoundException
3 {
4 // First, check if the class has already been loaded
5 Class c = findLoadedClass(name);
6 if (c == null) {
7 try {
8 if (parent != null) {
9 c = parent.loadClass(name, false);
10 } else {
11 c = findBootstrapClass0(name);
12 }
13 } catch (ClassNotFoundException e) {
14 // If still not found, then invoke findClass in order
15 // to find the class.
16 c = findClass(name);
17 }
18 }
19 if (resolve) {
20 resolveClass(c);
21 }
22 return c;
23 }1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
(1)继承ClassLoader
(2)重写findClass()方法
2、如果想打破双亲委派模型,那么就重写整个loadClass方法
(1)继承ClassLoader
(2)重写findClass()方法6
(3)调用defineClass()方法
-
tomcat 为什么要违背双亲委托机制?
- 双亲委托机制不能满足tomcat的业务需求;
- Webapp类加载器需要独立加载自身的class以及依赖的jar;
- 例如,webapp1依赖的spring版本为4.x,另一个webapp2依赖的spring版本为5.x. 如果使用双亲委托,那么spring的版本只能存在一个,没法满足这个需求。