京东深资架构师告诉你Java面试常见知识点(建议收藏)

本文由资深Java架构师撰写,详细讲解了Java面试中常见的JVM相关知识点,包括JVM内存结构、类加载过程、垃圾收集机制等。此外,还涵盖了Java基础、容器框架、线程与并发等内容,是Java开发者面试的必备参考资料。
摘要由CSDN通过智能技术生成

后端架构师

后端架构师

专注研究 Java 核心技术、架构,不限于分享算法、架构、高并发、多线程、JVM、Spring Boot、Maven、分布式、Spring Cloud +Docker+k8s、Dubbo、Zookeeper、Kafka、RocketMQ等。

点击“大礼包”有惊喜礼包!

日英文

You must be strong now. You must never give up. And when you are afraid of the dark, don't forget the light is always there. 

你要坚强,决不能放弃。当你担忧生活黑暗无边之时,请记得希望总会在某处闪光。

每日掏心话

每个人的一生都注定要跋涉坎坎,品尝那些苦涩与无奈。

   正文   

一、JVM

1. JDK、JRE、JVM三者间的关系

JDK(Java Development Kit)是Java开发工具包,是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)、一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。

JRE是运行基于Java语言编写的程序所不可缺少的运行环境。JRE中包含了JVM,runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器),只是针对于使用Java程序的用户。

JVM(java virtual machine)就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。

2. JVM四部分组成   相关资料分享

整个JVM框架由类加载器加载文件,执行器在内存中处理数据,交互时通过本地接口。

[1] ExecutionEngine:执行引擎,又叫解释器

负责解释命令,提交操作系统执行。

[2] NaiveInterface本地接口

作用:融合不同的语言为java所用。

[3] Runtimedataarea运行数据区

是整个JVM的重点,所有程序都被加载到这里运行。

[4] ClassLoader:类加载器

流程:Java文件>JDK编译>.class文件>classloader>加载到内存中

n ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,ClassLoader将描述该类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型。

n classloader具备层次关系:

a) 引导类加载器(bootstrap class loader)

他用类加载java 的核心库(String 、Integer、List。。。)在jre/lib/rt.jar路径下的内容,是用C代码来实现的,并不继承自java.lang.ClassLoader。

加载扩展类和应用程序类加载器。并指定他们的父类加载器。

b) 扩展类加载器(extensions class loader)

用来加载java的扩展库(jre/ext/*.jar路径下的内容)java虚拟机的实现会自动提供一个扩展目录。该类加载器在此目录里面查找并加载java类。

c) 应用程序类加载器(application class loader)

他根据java应用的类路径(classpath路径),我们编写的应用类默认情况下都是通过AppClassLoader进行加载的。当我们使用 new关键字或者Class.forName来加载类时,所要加载的类都是由调用new或者Class.forName的类的类加载器(也是 AppClassLoader)进行加载的。

ApplicationClassLoader由于是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称为SystemClassLoader.

d) 自定义类加载器

为了能够绕过Java类的既定加载过程,(例如为了达到类库的互相隔离,例如为了达到热部署重加载功能。),开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,并在其中对类的加载过程进行完全的控制和管理

n Java类加载双亲委派机制

Ø 在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。

Ø 如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.

Ø 这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系.这是十分必要的,比如Object类,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.

3. java类加载过程

[1] 加载

1. 通过一个类的全限定名获取该类的二进制流。

2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。

3. 在内存中生成该类的Class对象,作为该类的数据访问入口。

[2] 验证

验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

[3] 准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

[4] 解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

[5] 初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

4. JVM内存结构

[1] 方法区(又叫静态区):

所有线程共享方法区。用于存放类的元数据(即:编译后的代码、类的信息、常量池、字段信息、方法信息、静态变量),并不是类的Class对象!Class对象是加载的最终产品。方法区有时候也称为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载

[2] 堆:

所有线程共享堆区。用于存储所有new出来的对象,此对象最终由垃圾收集器收集,垃圾收集器针对的就是堆区

[3] 虚拟机栈(Java栈、占内存):

线程私有,生命周期和线程相同。每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

[4] 本地方法栈:

和java栈的作用差不多,只不过是为JVM使用到的native方法服务的,存储了每个native方法调用的状态

[5] 程序计数器:

线程私有,用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方。

5. Java中Native关键字

native是在java和其他语言(如c++)进行协作时使用的,也就是native后的函数的实现不是用java写的。

Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),java要实现对底层的控制,就需要一些其他语言的帮助,为此Java使用native方法来扩展Java程序的功能。

其实现步骤:

1、在Java中声明native()方法,然后编译;

2、用javah产生一个.h文件;

3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);

4、将第三步的.cpp文件编译成动态链接库文件;

5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

6. JVM中的年轻代 老年代 持久代 gc

虚拟机中的共划分为三个代:年轻代、老年代和持久代(永久代)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和老年代的划分是对垃圾收集影响比较大的。

你出生在 Eden 区,在 Eden 区有许多和你差不多的小兄弟、小姐妹,可以把 Eden 区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden 区不能无休止地放你们在里面,当幼儿园满了,你就要被送到学校去上学,这里假设从小学到高中都称为 Survivor 区。开始的时候你在 Survivor 区里面划分出来的的“From”区,读到高年级了,就进了 Survivor 区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。直到你 18 岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了 20 年 (每次 GC 加一岁),最后寿终正寝,被 GC 回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫爱德华 (慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代。

l 年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

l 年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

l 持久代:用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

² 新生代进行一次垃圾清理,被称为youngGC或者minorGC,频率高,执行快!

² 老年代进行一次垃圾清理,被称为FULLGC或者majorGC,比年轻代的慢10倍

² JVM优化的一个原则就是:降低youngGC的频率、减少FULLGC的次数。

n JVM内存参数

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M

-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了

-Xms128m JVM初始分配的堆内存

-Xmx512m JVM最大允许分配的堆内存,按需分配

-XX:PermSize=64M JVM初始分配的非堆内存

-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配

u JVM内存配置参数

-Xmx10240m-Xms10240m-Xmn5120m-XXSurvivorRatio=3

-Xmx10240m:代表最大堆

-Xms10240m:代表初始/最小堆

-Xmn5120m:代表新生代

-XXSurvivorRatio=3:代表Eden:Survivor=3根据Generation-Collection算法(目前大部分JVM采用的算法),一般根据对象的生存周期将堆内存分为若干不同的区域,一般情况将新生代分为Eden,两块Survivor;计算Survivor大小,Eden:Survivor=3,总大小为5120,3x+x+x=5120x=1024,则Surviver总大小为2048m。

Ø 堆(Heap)和非堆(Non-heap)内存

按照官方的说法:“Java虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在Java虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。

可以看出JVM主要管理两种类型的内存

7. 何时会抛出OutOfMemoryException

并不是内存被耗空的时候才抛出

JVM98%的时间都花费在内存回收

每次回收的内存小于2%

8. 内存泄漏与内存溢出

l 内存泄露是指你的应用使用资源之后没有及时释放,导致应用内存中持有了无用的资源,这是一种状态描述;

l 存溢出是指你的应用的内存已经不能满足正常使用了,堆栈已经达到系统设置的最大值,进而导致崩溃,这事一种结果描述;

l 通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。

9. 对象存活判定

[1] 引用计数法

给对象添加一个引用计数器,每当有一个地方引用了该对象,计数器就加1;当引用失效,计数器就减1;任何时刻的计数器为0的对象就是不可能在被使用的对象。虽然是一个实现简单有效的算法,但是很难解决对象之间循环相互引用的问题。

[2] 根搜索算法(GC Roots Tracing)

现在主流的JVM算法来标记“死去”的对象。

算法的基本思路是通过一些称为“GC-Roots”的对象,从这些节点往下延伸,搜索所走过的路叫引用链,当一个对象没有被引用链搜索到,则证明该对象不可用。如下图Object-5\6\7是不可用的:

可用作GC-Roots的对象有:

1.方法区的静态类型引用的对象

2.方法区的常量引用的对象

3.方法栈中引用

10. 垃圾收集的方法有哪些?

[1] 标记-清除:

先标记那些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,需要遍历整个堆,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。3.在进行GC时还要停掉当前运行的应用程序。

[2] 复制算法:

为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。

后来将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。

[3] 标记-整理

该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

11. GC垃圾回收机制

如果一个对象不在被直接或间接地引用,那么这个对象就成为了「垃圾」(Garbage),它占用的内存需要及时地释放,否则就会引起「内存泄露」。有些语言需要程序员来手动释放内存(回收垃圾),有些语言有垃圾回收机制(GC)。

l 垃圾回收是在后台运行的,是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收,但是我们可以告诉他,尽快回收资源(System.gc和Runtime.getRuntime().gc())

l 垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize方法

12. 垃圾收集器

上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

[1] Serial(串行GC)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

[2] ParNew(并行GC)收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

[3] Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

[4] Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

[5] Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

[6] CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的

[7] G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。

13. JVM优化

Ø 如何将新对象预留在年轻代

Full GC的成本远远高于Minor GC,因此某些情况下需要尽可能将对象分配在年轻代,可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。

Ø 如何让大对象进入年老代

因为大对象出现在年轻代很可能扰乱年轻代GC,可以使用参数-XX:PetenureSizeThreshold设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。

Ø 如何设置对象进入年老代的年龄

堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在Eden区,经过一次GC后依然存活,则被移动到Survivor区中,对象年龄加1。以后,如果对象每经过一次GC依然存活,则年龄再加1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold来设置,默认值是15。虽然-XX:MaxTenuringThreshold的值可能是15或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。

Ø 稳定的Java堆VS动荡的Java堆

一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms和-Xmx的大小一致,即最大堆和最小堆(初始堆)一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少GC次数,但同时也增加了每次GC的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使GC应对一个较小的堆,可以加快单次GC的速度。基于这样的考虑,JVM还提供了两个参数用于压缩和扩展堆空间。

Ø 增大吞吐量提升系统性能

吞吐量优先的方案将会尽可能减少系统执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。在拥有高性能的计算机上,进行吞吐量优先优化

二、Java基础

14. public class和class的区别

class的定义有两种方式:
public class类名------------class类名
采用public class来声明class,那么文件名必须和类名完全一致(包括大小写),如果文件名和类名不一致,将会出现错误。在一个java源文件中只能有一个class被public修饰。

15. Java标识符的命名规则

a)标识符是由,数字,字母,下划线和美元符号构成,其他符号不可以

b)必须以字母、下划线或美元符号开头,不能以数字开头

16. 访问控制权限

17. 数据类型

基本数据类型叫做原生类!

类中声明的变量有默认初始值;

方法中声明的变量没有默认初始值,必须在定义时初始化,否则在访问该变量时会出错。

自动转换:(byte,short,char)àintàlong(L)àfloat(F)àdouble

强制转换会导致溢出,精度降低

18. Integer与int的区别

1. 包装类、基本数据类型

2. Null,0

3. Integer中包含更多的与整数操作相关的方法。

19. Math.round();

Math.ceil():向上取整

Math.floor();向下取整

Math.round();四舍五入:+0.5后向下取整

20. Unicode与utf-8的区别

计算机内只能保存101010等二进制数据,那么页面上显示的字符是如何显示出来的呢?

[1] 字符集(Charset)
charset=char+set,char是字符,set是集合,charset就是字符的集合。字符集就是这个编码方式涵盖了哪些字符,每个字符都有一个数字序号。

[2] 编码方式(Encoding)
编码方式就是一个字符要怎样编码成二进制字节序,或者反过来怎么解析。
也即给你一个数字序号,要编码成几个字节,字节顺序如何,或者其他特殊规则。

[3] 字形字体(Font)
根据数字序号调用字体存储的字形,就可以在页面上显示出来了。
所以一个字符要显示出来&

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值