前言:本节主要复习下之前所讲过的一些知识点,整理出一些面试题,供各位大佬享用。
-
Java类加载过程
类加载器加载class可以大致分为:
加载 >> 验证 >> 准备 >> 解析 >> 初始化
1)加载:从磁盘中读取字节码文件放入运行时数据区。该过程会产生供开发人员使用的Class对象(如获取类的所有属性,所有方法等)。并且只有在程序运行到要使用该类时才会加载(通过new关键字等)。
2)校验:校验字节码文件格式的正确性(魔数:CAFE BABE)
3)准备:给类的静态变量(类变量)分配内存,给赋予默认等值(null 或 0);
4)解析:将符号引用替换为直接引用(静态链接)。
5)初始化:将类的静态变量初始化为程序给定的值,执行静态代码块。 -
几大类加载器和双亲委派机制
加载器种类:
1)引导类加载器(Bootstrap):负责加载JRE目录下的核心类库,如rt.jar
2)扩展类加载器(Extention):负责加载JRE目录下ext目录中的jar包
3)应用程序类加载器(Application):负责加载ClassPath目录下的类文件(项目中自已写的)
4)自定义类加载器(Custom):负责加载用户指定目录下的jar包双亲委派机制:
在java中,当类加载器需要对class文件进行加载时,会递归调用父加载器,当父加载器加载失败时,再向下递归传递进行加载,最后加载成功或失败。
如当程序中new Computer()时,应用程序类加载器委托给扩展类加载器,扩展类加载器委托给引导类加载器,引导类加载器findClass失败后向下传递给扩展类加载器,同理扩展类加载器findClass向下传递给应用程序类加载器,此时就可以加载到Computer类。优点:
安全加载:沙箱操作,保护核心类不被篡改。
避免类重复加载:当父加载器已经加载了某类时,子加载器就没必要再加载一次。 -
JVM内存结构
JVM包含:类加载子系统,运行时数据区,字节码引擎
运行时数据区:栈,堆,方法区,本地方法,程序计数器
栈:程序计数器,栈帧(局部变量,操作数据栈,动态链接,方法出口),本地方法
堆:年轻代(Eden,sTo,SFrom),老年代 -
JVM常见调优参数:
1) ‐Xms:初始堆大小
2)‐Xmx:堆最大内存
3)‐Xmn:年轻代内存大小
4)‐Xss:栈线程大小
5)‐XX:MetaspaceSize:指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M- ‐XX:MaxMetaspaceSize:元空间最大值,默认-1
-
Java对象创建过程
步骤:类加载检查 >> 分配内存 >> 初始化 >> 设置对象头 >> 执行init方法
1)类加载检查:会判断类是否已经加载,如果没有加载会执行第一道题步骤。2)分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为 对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
分配方法: 1)指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点 的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。 2)空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟 机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录 内存分配并发处理: 1)CAS(compare and swap):虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。 2)本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
3)初始化:给对象的实例字段分配初始值(与类加载赋初始值区分)
4)对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对 象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
5)执行init方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋代码中设定的值,和执行构造方法 -
对象内存分配方式
1)栈上分配:逃逸分析与标量替换,保证对象随着出栈而销毁2)年轻代Eden区:朝生夕死对象,会有Minor GC比较频繁,回收速度一般也比较快,Minor Gc会将存活对象在sTo和sFrom区移动清理
3)老年代分配:
a)大对象直接进入老年代
b)对象年龄超过阈值进入老年代
c)对象动态年龄判断
d)老年代空间分配担保机制 -
内存回收算法
1)引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。效率高,但是存在对象相互引用的问题,造成内存泄漏2)可达性分析算法:将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象 GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
-
垃圾收集算法
1)分代收集算法:根据对象存活周期的不同将内存分为几 块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。2)复制算法:它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的 内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一半进行回收。
3)标记-清除算法:算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标 记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。会产生空间碎片
4)标记-整理算法:根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回 收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
-
垃圾收集器种类与常见垃圾收集器回收过程
1)种类:
2)收集过程:
Serial收集器:串行垃圾收集器。当他进行垃圾收集时,会直接暂停所有用户线程,单线程收集直到收集结束。总体来说serial收集器结构比较简单,垃圾回收的速率比较慢,频繁的STW体验也不友好。Parallel收集器:多线程版本,同样也会STW。
ParNew收集器:多线程版本,会STW,可以结合CMS组合收集。
CMS垃圾收集器:实现了应用线程与GC线程同时运行(几乎),步骤如下:
初始标记:暂停所有的用户线程,并标记GC Root的直接可达对象,速度很快。 并发标记:从GC Root的直接关联对象向下遍历所有对象,所以过程耗时比较长,但是此处不需要停止用户线程,可以同时与gc线程一同运行。 重新标记:因为在并发标记过程中,很可能对象的状态已经发生变动(对象在并发过程中是可达的,但是在并发结束后马上变成垃圾对象了),所以此步骤是一个查漏补缺的操作,但是相比较于并发标记STW,此步骤耗时就少多了。 并发清理:GC开始对垃圾对象进行回收,并且用户线程也可以同时运行。 并发重置:重置本次GC过程中的标记数据。
G1垃圾收集器:Garbage-First收集器主要应用在配备大内存,多核处理器的机器上面。他以高概率满足垃圾收集暂停时间目标,同时在几乎不需要配置的情况下实现高吞吐量。G1 旨在使当前应用程序和环境在停顿和吞吐量之间提供最佳平衡。
-
字符串常量池位置变化,底层实现,intern方法赋值逻辑
1)位置变化:Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池 Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里 Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
2)底层实现:字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用
3)intern方法说明:
String s3 = new String("a") + new String("b");
s3.intern();
String s4 = "ab";
System.out.println(s3 == s4);
// 在 JDK 1.6 下输出是 false
// 在 JDK 1.7 及以上的版本输出是 true
JDK7之前是这样,直接复制一个字符串放到常量池中,而在JDK7之后,常量池就放进堆里面一起存放了,所以不需要再复制对象而是直接添加对象的引用到常量池就可以了。
总结:JVM的面试题都是比较抽象的概念性题目,如果硬记的话估计过会就会忘掉,所以更多要靠自己的理解。阿雷也把这些题目重新再梳理一遍放在这里,不记得了就拿出来看看,算是加深印象。我是阿雷,夜深人静,正是打代码的好时候。