Java后端社招面试个人总结

近期面了一些后端的内容,准备先对其中一些内容进行总结下,便于个人加深理解。行文可能仅支持个人能看懂理解就行。如有理解不到位的地方请谅解。终结面试后再来一一回顾整理。当然,友情提示下:面试前最好是提前做好项目亮点和难点、个人优点和缺点、为什么找工作的介绍准备,在面试中不断总结提炼,形成较好的表述。本文会持续更新到换工作结束,再分模块整理。(由于文字较多,可能存在没被发现的错别字,欢迎及时指出,感谢)

JVM

JVM内存区域是什么样的?各有什么内容?🌟🌟🌟🌟

JVM内存区域包含以下几部分:

程序计数器:线程私有。当前线程所执行字节码的行号指示器,便于线程切换后回到正确的执行位置,无OOM情况;

Java虚拟机栈:线程私有。方法执行时同步创建栈帧,存储局部变量表、操作数栈、动态连接和方法出口等信息。方法执行过程就是入栈到出栈的一个过程。(补充:局部变量表中有基本数据类型、对象引用、返回地址类型,数据类型以局部变量槽来存储,long/double两个槽位,其余的只有一个,编译时确定,运行时不会发生改变)线程请求栈深度大于虚拟机所允许深度,将抛出StackOverflowError,如虚拟机栈容量可动态扩展,则当栈扩展时无法申请足够内存时抛出OOM。

本地方法栈:线程私有。为虚拟机使用到的本地方法服务。也有可能StackOverflowError或OOM。

Java堆线程共享。内存管理最大的一块。存放对象实例,垃圾收集器管理的内存区域。物理上可不连续,但在逻辑上是连续的。可根据-Xmx和-Xms设定固定大小或可扩展的。无空间完成对象实例分配且无法再扩展时,将抛出OOM。

方法区线程共享。存储被虚拟机加载的类型信息、常量、静态变量、即时编译器后的代码缓存等数据。如果方法区无法满足新内存分配需求时,也会抛出OOM,一般垃圾回收器不对其进行处理。

new对象的原理/生命周期 🌟

对象创建过程:new对象/反射创建

a、检查类是否加载,没有则先加载类。(懒加载,会在堆区有class对象,方法区会有类的相关元数据信息)

b、分配内存。jvm根据大小分配内存空间;空闲列表(空间不规整,容易形成碎片空间)和指针碰撞方式(空间比较规整,默认使用)。并发问题,用CAS+重试机制或本地线程分配缓冲(每个线程预先分配一块堆内存)。

c、初始化。实例赋零值或null等操作。

d、设置对象头。hashcode、分代年龄、锁状态等信息。

e、执行初始化方法。对实例设置程序指定的初始值,并执行构造方法。

对象的回收

当对象不再被使用时,就需要进行垃圾回收,为其他对象腾出空间。

一般分配在eden区,不够用时会触发minor gc,存活对象被移动到survivor区,eden:survivor:survivor=8:1:1,如果survivor满了或年龄到了15后则被移动到老年代。

大对象和长期存活的对象都将进入老年代。如果老年代满了则会触发full gc,回收整个堆。

垃圾回收算法有哪些?新生代用的是哪种?哪种容易引起full GC?TLAB是什么? 🌟🌟

标记清除从gc root链上标记所有被引用的对象;遍历整个堆,把未标记的对象清除。(需暂停整个应用,并会产生内存碎片)。缺点:执行效率不稳定,会因为对象数量增长,效率变低;标记清除后会有大量的不连续的内存碎片,空间碎片太多就会导致无法分配较大对象,无法找到足够大的连续内存,而发生gc

标记整理:算法分为”标记-整理-清除“阶段,首先需要先标记出存活的对象,然后把他们整理到一边,最后把存活边界外的内存空间都清除一遍。这个算法的好处就是不会产生内存碎片,但是由于整理阶段移动了对象,所以需要更新对象的引用。

复制:复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。缺点:可用内存缩短一半,浪费空间。

尽管看起来问题很大,但分代理论说大多数对象生命周期短,这种情况下标记复制就很适合了(复制的存活对象少)。至于内存消耗太大的问题,java虚拟机通过将新生代分为一个Eden区与2个Survivo区,其中一个Survivo区用来复制,这样一来极大得提高了内存空间利用率。

所以新生代用的是标记复制算法

容易引起Full GC的是标记清除算法,因为空间碎片太多导致无法分配大对象。

线程本地局部缓存TLAB(Thread Local Allocation Buffer),JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB, 其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配, 在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。

TLAB仅作用于新生代的Eden Space,所以通常多个小的对象比大的对象分配起来更加高效。

虽然总体来说堆是线程共享的,但是在堆的年轻代中的Eden区可以分配给专属于线程的局部缓存区TLAB,也可以用来存放对象。相当于线程私有的对象。所以这块内存分配是线程独占的,读取、使用和回收是线程共享的。

垃圾对象的判定方式?哪些可以做gc root对象 🌟

引用计数器:被引用一次+1,为0时表示无引用,可以被回收了。但相互引用但外部无引用的情况下不会被回收,容易造成内存泄漏。

可达性分析:从gc root开始扫描堆中的对象,被扫描到的都是存活对象。没有扫到的则需要被回收。gc root对象:虚拟机栈中的引用对象、方法区中的静态属性引用对象、方法区中的常量引用对象和本地方法栈中JNI引用对象、被锁持有的对象、虚拟机内部引用对象等。

引用有哪些?各有什么特点?🌟

强引用(strong reference)

强引用就是我们最常见的普通对象引用(如new 一个对象),只要还有强引用指向一个对象,就表明此对象还“活着”。在强引用面前,即使JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),让程序异常终止,也不会靠回收强引用对象来解决内存不足的问题。(不符合垃圾收集对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就意味着此对象可以被垃圾收集了。但要注意的是,并不是赋值为null后就立马被垃圾回收,具体的回收时机还是要看垃圾收集策略的。

软引用(soft reference)

软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。(垃圾收集可能会执行,但会作为最后的选择

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

在使用软引用的时候必须检查引用是否为null。因为垃圾收集器可能在任意时刻回收软引用,如果不做是否null的判断,可能会出现NullPointerException的异常。

弱引用(weak reference)

弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短弱引用指向的对象是一种十分临近finalize状态的情况,当弱引用被清除的时候,就符合finalize的条件了。弱引用与软引用最大的区别就是弱引用比软引用的生命周期更短暂。垃圾回收器会扫描它所管辖的内存区域的过程中,只要发现弱引用的对象,不管内存空间是否有空闲,都会立刻回收它。(符合垃圾收集)具体的回收时机还是要看垃圾回收策略的,因此那些弱引用的对象并不是说只要达到弱引用状态就会立马被回收。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列。这一点和软引用一样。

虚引用(phantom reference)  

虚引用并不会决定对象的生命周期。即如果一个对象仅持有虚引用,就相当于没有任何引用一样在任何时候都可能被垃圾回收器回收。(符合垃圾收集)不能通过它访问对象,虚引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制(如做所谓的Post-Mortem清理机制),也有人利用虚引用监控对象的创建和销毁。

虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态。所以对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以确保处于弱引用状态的对象没有改变为强引用。

但是有个问题,如果我们错误的保持了强引用(比如,赋值给了static变量),那么对象可能就没有机会变回类似弱引用的可达性状态了,就会产生内存泄露。所以,检查弱引用指向对象是否被垃圾收集,也是诊断是否有特定内存泄露的一个思路,我们的框架使用到弱引用又怀疑有内存泄露,就可以从这个角度检查。

什么是内存溢出,什么是内存泄漏 🌟

内存溢出是指程序在申请内存时,没有足够的空间供其使用。而内存泄漏是指在程序在申请空间后,无法释放已申请的空间。一次泄漏不会造成什么影响,但内存泄漏堆积会耗尽所有内存。

OOM如何排查?如何调优gc? 🌟🌟🌟(没经验的话可以说自己会按照什么方式去排查)

1、根据top命令查看各个进程的使用情况,通过ctrl+m找到消耗最高的几个进程;根据pid查看性能消耗较高的是什么服务在运行;

2、通过jstat虚拟机统计信息命令行工具查看进程的类加载、内存、垃圾收集、即时编译等运行数据。

如 -gc 进程号 间隔时间  输出条数来监视Java堆情况;-gcutil 进程号查看空间占比;

3、jmap内存映射工具来生成堆转储快照,即dump文件,jmap -heap 进程号。也可通过-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/log/risk-manager-magpie.hprof配置在OOM时自动生成。

调优:堆大小的设置;新生代老年代的比例设置;回收器设置;(仅做了解,没实践过)

用的是什么jdk版本?什么垃圾回收机制?G1和CMS的回收流程与算法是什么样的?有啥优缺点?🌟🌟🌟🌟(一定要提前了解线上用的是哪种,面试官会根据这个来引入)

jdk1.8,用的是G1收集器。

CMS收集器

获取最短停顿时间为目标的收集器。基于标记-清除算法实现,老年代收集器。步骤为:

1、初始标记(CPU停顿,很短):标记gcroot直连对象,速度很快;

2、并发标记(收集垃圾和用户程序一起执行):进行GC root对象图遍历的过程;

3、重新标记(CPU停顿,比初始标记稍长,但比并发标记短):修正并发标记中因用户线程继续运作而导致标记变更的记录;

4、并发清除-清除算法(不需移动存活对象,与用户线程同时):清除已死亡对象。

优点:并发收集,低停顿

缺点:对CPU资源敏感,总吞吐量下降;无法处理浮动垃圾;空间碎片对对象分配不利

G1收集器

面向服务端应用的垃圾收集器,基于region的堆内存布局,化整为零,大小相等的region根据需要作为新生代或老年代,不同的region采用不同的策略处理,分代收集器。步骤:

1、初始标记(CPU停顿):标记直连对象;停顿很短,利用minorGC完成,实际上并没有额外停顿;

2、并发标记(与用户线程并发执行):可达性分析,找出要回收的对象;耗时长;

3、最终标记(CPU停顿):处理原始快照SATB中并发阶段结束后的遗留记录;

4、筛选回收:(可根据用户期望的GC停顿时间回收):对各个region的回收价值和成本排序,基于用户所期望停顿的时间来回收对应的内存。

优点:并行与并发,多CPU下可通过并发继续执行从而缩短停顿时间;分代收集:不需其他收集器配合就能独立管理整个GC堆;空间整合整体基于标记-整理,局部复制来实现可预测的停顿

双亲委派模式是什么?为什么要有双亲委派?如何破坏双亲委派?

什么是双亲委派

所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

Java中提供的这四种类型的加载器,是有各自的职责的:

  • Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
  • Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
  • User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件

那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。

为什么要有双亲委派

1、通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

2、通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。

双亲委派的实现

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。

1、先检查类是否已经被加载过

2、若没有加载则调用父加载器的loadClass()方法进行加载

3、若父加载器为空则默认使用启动类加载器作为父加载器。

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

如何破坏双亲委派

因为双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。

ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClassdefineClass等,那么这几个方法有什么区别呢?

  • loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
  • findClass() 根据名称或位置加载.class字节码
  • definclass() 把字节码转化为Class

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中

因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载

如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。

Spring/SpringBoot

Spring如何实现自定义注解 🌟

Java注解是附加在代码中的一些元信息,用于一些工具在编译时、运行时进行解析和使用,起到说明、配置的功能。其本质是继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。反射获取注解时,返回的是java运行时生成的动态代理对象$Proxy1。

1、创建一个自定义注解和创建一个接口类似。但自定义注解需要使用@interface;

2、添加元注解信息;

3、注解方法不能带有参数;

4、注解方法返回值为基本类型、String、Enums、Annotation或其数组;

5、注解可以有默认值;

@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface CarName {
    String value() default "";
}

@Target:注解用于什么地方:ElementType.Constructor\Field\local_variable\method\package\parameter\type(类或接口或枚举声明);

@Document:注解是否会包含在javadoc中;

@Retention:什么时候用该注解。SOURCE(编译阶段就丢弃)/CLASS(类加载时丢弃)/RUNTIME(始终不会丢弃);

@Inherited:定义该注解与子类的关系。子类是否能使用。

Spring beanFactory和Factory Bean的区别? 🌟

BeanFactory:所有SpringBean的容器根接口,定义了Spring容器的规范,如getBean\isSingleton等方法;实现类诸如XmlBeanFactory、AbstructBeanFactory;

FactoryBean:Spring容器创建Bean的一种形式,可让用户通过实现该接口来自定义该Bean接口的实例化过程;让调用者无需关心具体实现细节。方法有getObject/getObjectType/isSingleton;常用类有ProxyFactoryBean(AOP代理Bean).

SpringBean的循环依赖如何处理的?有几级缓存? 🌟🌟🌟

循环依赖,即A依赖B,B又依赖A;或者ABC三者的依赖关系。

解决循环依赖:主要是针对单例Bean对象而言的。原型的会抛出异常提示。

1、创建原始Bean对象

instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);

假设BeanA先被创建。创建后的原始对象是BeanA1。上述代码中的bean即BeanA1。

2、暴露早期引用 

addSingletonFactory(beanName, new ObjectFactory<Object>() {
 
    @Override
    public Object getObject() throws BeansException {
 
    return getEarlyBeanReference(beanName, mbd, bean);
 
    }
});

通过暴露早期引用,BeanA指向的原始对象BeanA1创建好后,就会把原始对象的引用通过ObjectFactory暴露出去,在getObject的时候,其getEarlyBeanReference第三个参数就是原始对象暴露的bean。

3、解析依赖

populateBean(beanName, mbd, instanceWrapper);

解析依赖阶段,会先对BeanA对象进行属性填充,当检测到BeanA依赖于BeanB时,就会先去实例化B。而BeanB也会在此处解析自己的依赖。就可以直接调用BeanFactory.getBean("beanA")方法获取beanA;

4、获取早期引用

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
 
Object singletonObject = this.singletonObjects.get(beanName);
 
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
 
synchronized (this.singletonObjects) {
 
// 从缓存中获取早期引用
 
singletonObject = this.earlySingletonObjects.get(beanName);
 
if (singletonObject == null && allowEarlyReference) {
 
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
 
if (singletonFactory != null) {
 
// 从 SingletonFactory 中获取早期引用
 
singletonObject = singletonFactory.getObject();
 
 
this.earlySingletonObjects.put(beanName, singletonObject);
 
this.singletonFactories.remove(beanName);
}}}}
 
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

 在上一步中,getBean("beanA")会先调用getSingleton("beanA"),尝试从缓存中获取;由于beanA 还未实例化好,则返回的是null,接着getEarlySingletonObject也返回空,因为早期引用还没有放入缓存中。因此调用singletonFactory.getObject,由于已经有了早期引用,则实际上指向了BeanA1。beanB获取了这个原始对象的引用,就可以顺利完成实例化,这样beanA也就能顺利完成实例化了。由于beanB.beanA和beanA指向的是同一个对象beanA1,所以beanB中的beanA也处于可用状态了。

Spring有三级缓存。处于最上层的缓存是singletonObjects,它其中存储的对象是完全创建好,可以正常使用的bean,二级缓存叫做earlySingletonObjects,它其中存储的bean是仅执行了第一步通过构造方法实例化,并没有填充属性和初始化,第三级缓存singletonFactories存储的是对应bean的一个工场。

/** 一级缓存,保存singletonBean实例: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** 二级缓存,保存早期未完全创建的Singleton实例: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 

/** 三级缓存,保存singletonBean生产工厂: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);


Spring尝试获取单例bean时,首先会在三级缓存中查找。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 查询一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //若一级缓存内不存在,查询二级缓存
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                //若二级缓存内不存在,
  • 11
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值