Java 复习 面经总结

Java相关

8 种基本类型

String

String是不可变对象, 意思是一旦创建,那么整个对象就不可改变. 即使新手觉得String引用变了,实际上只是(指针)引用指向了另一个(新的)对象,而程序员可以明确地对字符数组进行修改,因此敏感信息(如密码)不容易在其他地方暴露(只要你用完后对char[]置0)。

Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char

== 与 equals

简单来说, ==判断两个引用的是不是同一个内存地址(同一个物理对象)。而 equals() 是可以重写的一个方法,默认直接 return a==b;

String 的 equals 先判断内存地址,再比较值相等。

hashCode 与 equals

hashCode()与equals()的相关规定

如果两个对象相等,则hashcode一定也是相同的

两个对象相等,对两个对象分别调用equals方法都返回true

两个对象有相同的hashcode值,它们也不一定是相等的

因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

访问修饰符

private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。

public : 对所有类可见。使用对象:类、接口、变量、方法

重载、重写

构造器不能被继承,因此不能被重写,但可以被重载。

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

栈溢出

  1. 栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
  2. 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
  3. 调整参数-xss去调整jvm栈的大小

OOM

原因

  • 无法在 Java 堆中分配对象
  • 应用程序保存了无法被GC回收的对象
  • 应用程序过度使用 finalizer

常见类型

Java Heap 溢出

堆 存放实例对象和数组,线程共享。

一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess

java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

虚拟机栈和本地方法栈溢出

虚拟机栈是jvm执行java代码所使用的栈。

本地方法栈是jvm调用操作系统方法所使用的栈。

本地方法栈类似虚拟机栈,但是只为Native方法服务。

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间(不断创建线程),则抛出OutOfMemoryError异常

这里需要注意当栈的大小越大可分配的线程数就越少。

运行时常量池溢出

方法区一部分。存放编译期生成的各种字面量和符号引用。

异常信息:java.lang.OutOfMemoryError:PermGen space

如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

方法区溢出

用于存储已被JVM加载的类信息,类名、访问修饰符、常量池、字段描述、方法描述等。,即时编译器编译后的代码,线程共享。

异常信息:java.lang.OutOfMemoryError:PermGen space

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

排查方法

  1. 查找关键报错信息,如java.lang.OutOfMemoryError: Java heap space
  2. 使用内存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)对Dump出来的堆储存快照进行分析,分析清楚是内存泄漏还是内存溢出。
  3. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,修复应用程序中的内存泄漏。
  4. 如果不存在泄漏,先检查代码是否有死循环,递归等,再考虑用 -Xmx 增加堆大小。

区别于面向过程

就像蛋炒饭(面向对象)和盖浇饭(面向过程)的区别

JVM 内存空间

线程独享:JVM栈,程序计数器,本地方法栈

线程共享:堆,方法区

程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;

虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;

本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;

堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;

方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;

JVM 堆内存空间

新生代(eden,survivor),老年代,永久代(元空间)

  • 新生带(年轻代):新对象和没达到一定年龄的对象都在年轻代
  • 老年代:被长时间使用的对象,老年代的内存空间应该要比年轻代更大
  • 元空间(JDK1.8之前叫永久代):像一些方法中的操作临时对象等,JDK1.8之前是占用JVM内存,JDK1.8之后直接使用物理内存

堆内存调参

-Xms:设置初始分配大小,默认为物理内存的1/64

-Xmx:最大分配内存,默认为物理内存的1/4

-XX:+PrintGCDetails:输出详细的GC处理日志

-XX:+PrintGCTimeStamps:输出GC的时间戳信息

-XX:+PrintGCDateStamps:输出GC的时间戳信息(以日期的形式)

-XX:+PrintHeapAtGC:在GC进行处理的前后打印堆内存信息

-Xloggc:(SavePath):设置日志信息保存文件

在堆内存的调整策略中,基本上只要调整两个参数:-Xms和-Xmx

JVM 垃圾回收

垃圾回收算法

  1. 标记-清除算法
  2. 标记-整理算法
  3. 复制算法(新生代)
标记-清除

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;

第二步:在遍历一遍,将所有标记的对象回收掉;

特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;

标记-整理

第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;

第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;

特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

复制算法

将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;

特点:不会产生空间碎片;内存使用率极低;

分代收集

根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;

老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

GC 相关

java堆 = 新生代+老年代;

新生代 = Eden + Suivivor(S0 + S1),默认分配比例是 8:1:1

当Eden区空间满了的时候,就会触发一次Minor GC,以收集新生代的垃圾,存活下来的对象会被分配到Survivor区

大对象(需要大量连续内存空间的对象)会直接被分配到老年代

如果对象在Eden中出生,并且在经历过一次Minor GC之后仍然存活,被分配到存活区的话,年龄+1,此后每经历过一次

Minor GC并且存活下来,年龄就+1,当年龄达到15的时候,会被晋升到老年代

当老年代满了,而无法容纳更多对象的话,会触发一次full gc;full gc存储的是整个内存堆(包括年轻代和老年代)

Major GC是发生在老年代的GC,清理老年区,经常会伴随至少一次minor gc

cms 和 g1

cms

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器仅作用于老年代的收集,是基于标记-清除算法,2次 stop the world

  • 初始标记(CMS initial mark)(stop the world)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)(stop the world)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

g1

G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型

初始标记(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,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

JMM 内存模型

volatile 关键字

指令重排

为了遵守as-if-serial语义, 编译器和处理器不会对存在数据依赖关系的操作做重排序, 因为这种重排序会改变执行结果. 但是,如果操作之间不存在数据依赖关系, 这些操作就可能被编译器和处理器重排序.  

  1. 编译器优化的重排序: 编译器在不改变单线程程序语义的前提下, 可以重新安排语句的执行顺序.
  2. 指令级并行的重排序: 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行. 如果不存在数据依赖性, 处理器可以改变语句对应机器指令的执行顺序.
  3. 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区, 这使得加载和存储操作看上去可能是在乱序执行.
内存屏障

volatile写

对于volatile变量的写,按照JMM的标准,需要插入两条内存屏障:

1
2
3
StoreStore;
volatile_write_code;
StoreLoad;

解释下,在写volatile之前插入了storestore指令,意味着volatile之前的普通写操作需要先于volatile操作执行,也就是不允许之前的普通写操作,与volatile写操作重排序。符合JMM对volatile关键字的规则。

在写volatile之后,需要加入StoreLoad指令,也就是先把volatile变量的最新值刷新到主内存(store),然后再执行后续的操作。同样不允许volatile写操作与后续的读、写操作重排序。

volatile读

对于volatile的读,按照JMM的标准,同样需要查询两条内存屏障,但是不同于上述的写操作,这块都是插入在volatile_code之后的。

1
2
3
volatile_read_code;
LoadLoad;
LoadStore;

这里的LoadLoad用于禁止下面的普通读操作与volatile读重排序,LoadStore则禁止普通写操作与volatile读操作重排序。

Java 跨平台机制

Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。

好处

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

编译型 or 解释型

暂无定论

Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行

类的加载

机制

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;

  1. 装载:查找和导入Class文件
  2. 链接:把类的二进制数据合并到JRE中
    • 校验:检查载入Class文件数据的正确性
    • 准备:给类的静态变量分配存储空间(赋0值)
    • 解析:将符号引用转成直接引用;
  3. 初始化:对类的静态变量,静态代码块执行初始化操作(赋初始值)

类加载器

类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器

启动类加载器:用来加载java核心类库,无法被java程序直接引用;

扩展类加载器:用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;

系统类加载器:它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;

自定义类加载器:由java语言实现,继承自ClassLoader;

双亲委派模型

当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类;

为什么

为了防止内存中出现多个相同的字节码;

因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性;

怎么打破

自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法;

Java 反射

Class getClass() getMethodName()…

对比直接调用速度变慢

1
2
3
4
5
6
7
8
9
10
11
12
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
Class clz = Class.forName("com.chenshuyi.api.Apple");
Method setPriceMethod = clz.getMethod("setPrice", int.class);
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
setPriceMethod.invoke(appleObj, 14);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));

原因

  1. 反射调用会多出来参数校验的过程
  2. 无法被JIT优化,JIT可以帮助java的字节码到原生的机器码层面上,这样的话减少了java字节码的再解析操作,而反射方法是无法被jit优化的
  3. 调用过程中的封装与解封操作,invoke 方法的参数是 Object[] 类型,在调用的时候需要进行一次封装。产生了额外的开销

Java 动态代理

java动态代理详细用法

通过使用代理,通常有两个优点

优点一:可以隐藏委托类的实现;

优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

HashMap

查看详细解读

1.7 1.8区别

拉链法解决hash碰撞时

超过8个链表转红黑树

低于6个转为链表

cpu 占用 100%

在resize的过程中,需要将部分冲突的key迁移至新的桶中

在此过程中,多线程操作可导致链表形成环

ConcurrentHashMap

1.7 1.8 区别

1.7采用分段锁

1.8采用synchronized + cas

主要是因为sychronized在jvm上进行了优化,把起步重量级锁,改为从偏向锁开始起步,再到轻量级锁,直到最后才膨胀为重量级锁,极大优化了执行效率,减少了锁的开销

ArrayList

扩容机制

初始大小为10,每次扩容增加自身大小的一半

newCap=oldCap+(oldCap>>1)

多线程

Synchronized

  • 同步普通方法,锁的是当前对象。
  • 同步静态方法,锁的是当前 Class 对象。
  • 同步块,锁的是 () 中的对象。

synchronized 很多都称之为重量锁,JDK1.6 中对 synchronized 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁轻量锁

CAS(乐观)

通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现等,所以CAS不会保证线程同步。乐观的认为在数据更新期间没有其他线程影响

  • 优点
    非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。
  • 缺点
    • ABA问题 线程C、D,线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本来保证CAS的正确性。
    • 自旋时间过长,消耗CPU资源, 如果资源竞争激烈,多线程自旋长时间消耗资源。

线程池(4种类型,拒绝策略)

  • Executors.newCachedThreadPool():无限线程池。
  • Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
  • Executors.newSingleThreadExecutor():创建单个线程的线程池。
  • newScheduledThreadPool() 创建一个定长线程池,支持定时及周期性任务执行。

优点

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
1
2
3
4
5
6
ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
  • corePoolSize 为线程池的基本大小。
  • maximumPoolSize 为线程池最大线程大小。
  • keepAliveTimeunit 则是线程空闲后的存活时间。
  • workQueue 用于存放任务的阻塞队列。
  • handler 当队列和最大线程池都满了之后的饱和策略。

线程池的5种状态

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);
  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
  4. TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
  5. TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

ThreadLocal

每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

StringBuilder 和 StringBuffer

StringBuffer线程安全,StringBuilder线程不安全

StringBuilder的效率比StringBuffer高一些

字符串连接操作中StringBuffer的效率要比String高:

1
2
String str = new String("hello ");
str += "world";

以上代码的处理步骤实际上是通过建立一个StringBuffer

然后调用append(),最后再将StringBuffer toSting();

这样的话String的连接操作就比StringBuffer多出了一些附加操作,当然效率上要打折扣.

SpringBoot

相关设计模式

代理

动态代理

AOP

JDK代理和Cglib代理

观察者

ApplicationEvent

ApplicationListener

ApplicationEventPublisher

适配器

DispatcherServlet

Handler

Controller

单例

Bean

Component

工厂

BeanFactory

DataSourceFactory

模板

JdbcTemplate

HibernateTemplate

装饰器

InputStream

Wrapper

上下文

ApplicationContext

区别Spring

Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。

  1. 创建独立的Spring应用。
  2. 嵌入式TomcatJettyUndertow容器(无需部署war文件)。
  3. 提供的starters 简化构建配置
  4. 尽可能自动配置spring应用。
  5. 提供生产指标,例如指标、健壮检查和外部化配置
  6. 完全没有代码生成和XML配置要求
作者

housirvip

发布于

2020-09-06

更新于

2023-01-01

许可协议

评论