Java中级面试总结

Source

JVM相关问题

JVM:java虚拟机,存在于JRE。
虚拟机把java代码编译成计算机能识别的机器码

JVM内存结构

  1. 方法区: 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。(与类有关的信息)。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。方法区在虚拟机启动时创建。
    当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
    jdk1.8之前,方法区是用的堆内存,1.8之后,方法区用的操作系统内存。
  2. 程序计数器: 记住下一条jvm指令的执行地址;(多线程的情况下在cpu对各线程的切换时需要程序计数器来对线程接下来需要执行的指令执行地址)
    是线程私有的,不会存在内存溢出(内存结构中唯一一个不会内存溢出的结构)
    二进制字节码->解释器->机器码->CPU
  3. 虚拟机栈:
    1.(结构如桶)先进后出原则。
    2.方法调用会从最后的方法执行。
    3.虚拟机栈就是我们线程运行时需要的内存空间。
    4.一个线程运行时需要一个栈。
    5.每个栈可以看成是由多个栈帧组成。
    6.每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(位于栈顶)。
    7.一个栈帧就对应着Java中一个方法的调用,即栈帧就是每个方法运行时需要的内存。每个方法运行时需要的内存一般有参数,局部变量,返回地址,这些都需要占用内存,所以每个方法执行时,都要预先把这些内存分配好。
    当我们调用第一个方法栈帧时,它就会给第一个方法分配栈帧空间,并且压入栈内,当这个方法执行完了,就会把这个方法栈帧出栈,释放这个方法所占用的内存。
  4. 本地方法栈: Java虚拟机调用本地方法时,需要给本地方法提供的一些内存空间;
    本地方法不是由Java编写的代码,由于Java有时不能直接和操作系统打交道,所以需要用C/C++语言来与操作系统打交道,那么Java就可以通过调用本地方法来获得这些功能。本地方法非常的多,如Object类的clone(),hashCode方法,wait方法,notify方法等;
  5. 堆: 是JVM中最大的一块内存区域,该区域的目的只是用于存储对象实例及数组;

垃圾回收(GC)

何时需要回收

引用计数法

引用增加时+1 引用失效-1 为0可以回收
弊端:
1.循环引用可能导致计数永不为0 导致没法回收
2.HotSpot和其他主流虚拟机并没有采用这种算法

可达性分析算法

“GC Roots”对象做为起点,从这些节点向下搜索,搜索所经过的路径称为“引用链”,如果一个对象没有一条引用链可以到达“GC Roots”,这认为这个对象是不可达,即对象不可能再被使用到,可以回收的;可达性算法可以解决上面提到的循环引用问题,一般可以做为“GC Roots”对象的有以下:

  1. 虚拟机栈(帧栈中的本地变量)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI引用的对象;

回收哪些对象

引用类型

强引用(Strong Reference)
永远不会被GC,除非显示的设置null,才会GC
软引用(Soft Reference)
如果内存紧张,则会被回收
弱引用(Weak Reference)
短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null
虚引用(Phantom Reference)
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null

如何回收

标记-清除算法

实现简单;
弊端: 效率不高,有内存碎片问题(标记对象可能分散在内存非连续地方,清楚后释放的内存不是连续的 分配较大内存的对象不得不触发另一次垃圾回收动作)

复制算法

效率很高,也不会有内存碎片问题
就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。反复去交换两个内存的角色,完成垃圾收集
标记-整理算法
在标记清除法基础上做了优化,把存活的对象压缩到内存一端,然后直接清理掉端边界以外的内存(老年代使用的就是标记压缩法)

分代收集算法

  1. 根据对象存活周期的不同将内存划分为几块。
  2. 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
  3. 在新生代中,每次垃圾收集时都发现有大批对象死去(回收频率很高),只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。其中,新生代又细分为三个区:Eden,From Survivor,ToSurviver,比例是8:1:1。
  4. 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

一般分为新生代和老年代,根据各个年代的特点采取合适的收集算法;在新生代中,正常情况下,每次垃圾回收都会有大量的对象需要回收,就可以选择使用复制算法,而老年代中的对象存活率比较高,就可以使用标记-清理或者标记-整理算法收集;
弊端: 牺牲一半的可用内存用作复制;

相关问题

简述 java 垃圾回收机制?

在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

垃圾回收是否涉及栈内存?

不涉及,垃圾回收只是回收堆内存中的无用对象,栈内存不需要对它执行垃圾回收,随着方法的调用结束,栈内存就释放了。

栈内存分配越大越好吗?

首先栈内存可以指定:-Xss size(如果不指定栈内存大小,不同系统会有一个不同的默认值)
其次由于电脑内存一定,假如有100Mb,如果给栈内存指定为2Mb,则最多只能存在50个线程,所以并不是越大越好,栈内存较大一般是可以进行较多次的方法递归调用,而不会增强线程效率,反而会使线程数量减少,一般使用默认大小。

什么情况下会导致栈内存溢出?

  1. 栈帧过多导致栈内存溢出(一般递归调用次数太多,进栈太多导致溢出) 这里最容易出现的场景是函数的递归调用。
  2. 栈帧过大导致栈内存溢出(不太容易出现)

类加载过程?

加载、验证、准备、解析、初始化

JVM分哪几个区?

方法区、程序计数器、虚拟机栈、本地方法栈、堆

如和判断一个对象是否存活?

引用计数法、可达性分析算法

java中垃圾收集的方法有哪些?

标记清除算法、标记整理算法、复制算法、分代收集算法。

HashMap相关问题

map接口的实现,允许k/v为空,但是最多只允许一条key为null的数据(多条会被覆盖),线程不安全。重写equals和hashcode方法。
Get/Put:通过hash(Key)找到该key在哈希表的位置坐标
通过equals()找到该key在链表里面对应的value
Jdk1.8+ 链表长度大于8时树化
默认增长因子为0.75,当达到初始大小的0.75时开始扩容为原来的两倍。

HashMap数据结构

数组+链表(jdk1.8增添红黑树)
数组存放的是一个链表,Entry类持有一个指向下一个元素的引用,这就构成了链表。 当链表长度大于8时树化。

  1. 每个链表里面的元素的hash值相同
  2. 通过key的equals方法找到链表上的value

HashMap线程安全

扩容时,数组所有元素需要rehash,将链表翻转,放到对应的bucket上的链表中,这个过程在并发环境下会发生错误,导致数组链表中的链表形成循环链表,并发环境下多线程put后可能导致get死循环。
多线程put的时候可能导致元素丢失。两个线程发生碰撞,就可能出现覆盖丢失的情况。那么就要使用线程安全的哈希表容器。如下:使用Hashtable 类,Hashtable是线程安全的;使用java并发包(java.util.concurrent)下的ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全。或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。

MQ相关问题

核心:解耦、异步、削峰

优点

  1. 解耦: (Pub/Sub 发布订阅消息)主系统发布消息其他系统接收保证主系统和其他系统解耦且减轻主系统的压力。
  2. 异步: 在不影响用户体验和系统业务流程的情况下加快响应速度。同样的异步业务使用mq反而更快,调用接口网络开销更大且可能使系统崩掉。
  3. 削峰: 高并发的情况下,大量的请求需要处理,对数据库的压力极大,使用消息队列可以缓存请求减少短时间的高并发问题。

缺点

  1. 系统可用性降低: 系统引入的外部依赖越多,越容易挂掉。
  2. 系统复杂性提高: 考虑更多的问题,丢消息、重复消费、顺序性等。

相关问题

如何保证消息不重复消费
生产者添加唯一ID,消费者根据ID判断重复消息。

如何保证消息不丢失

  1. 生产者丢失:开启事务,捕获异常等;
  2. MQ丢失数据:开启持久化;
  3. 消费者丢失:手动ack,主动提交成功完成消费。

如何选型MQ
Kafka:适用大数据领域的实时计算、日志采集等场景。
RabbitMQ:基于 erlang 开发,并发能力很强,性能极好,延时很低微秒级。
RocketMQ:分布式架构,扩展性好,在同等机器下,可以支撑大量的 topic。

使用MQ会有什么问题

  1. 降低了系统可用性
  2. 增加了系统的复杂性

怎样保证MQ的高可用
rabbitMQ、Kafka、RocketMQ都支持集群模式;
MQ的部署方式可分为单Master、多Master、多Master多Slave异步复制、多Master多Slave同步双写等模式。视情况而定选择不同模式,针对于数据高可用要求高的公司建议同步双写,虽然降低效率,单保证了数据的不丢失,高可用性系数变高。

多线程相关问题

线程属于进程的一个实体,也是cpu调度和分派的一个基本单位。多线程可以使cpu能够得到充分的利用,提高程序运行效率。

相关问题

Java线程的五种状态

  1. 新建状态(New)
  2. 就绪状态(start)
  3. 运行状态(run)
  4. 阻塞状态(Blocked)等待阻塞:执行wait() 同步阻塞:资源被占用等待获取sync 其他阻塞:sleep()或join()
  5. 死亡状态(Dead)退出run()方法。

如何创建一个多线程

  1. 继承Thread类 重写run()
  2. 实现runnable接口 重写run()
  3. 创建线程池,实现runnable接口或者callnable接口。

线程池的优点

  1. 重用存在的线程,减少对象创建销毁的开销。
  2. 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  3. 提供定时执行、定期执行、单线程、并发数控制等功能。

synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

  1. ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
  2. ReentrantLock可以获取各种锁的信息
  3. ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

什么是乐观锁和悲观锁

  1. 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
  2. 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

什么是死锁
死锁就是两个或两个以上的线程相互等待对方释放资源,从而进入被无限的阻塞的状态。

什么是方法锁、对象锁、类锁

方法锁
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。
对象锁
方法锁也是一种对象锁。当一个线程访问一个带synchronized方法时,由于对象锁的存在,所有加synchronized的方法都不能被访问(前提是在多个线程调用的是同一个对象实例中的方法)。
类锁
一个class其中的静态方法和静态变量在内存中只会加载和初始化一份,所以,一旦一个静态的方法被申明为synchronized,此类的所有的实例化对象在调用该方法时,共用同一把锁,称之为类锁。

数据库相关问题

相关问题

事务四大特性
ACID,原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)、持久性(Durability)。

原子性
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性
在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。

隔离性
隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据。

持久性
在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。

索引工作原理
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
优点

  1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  2. 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  3. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  4. 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  5. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点

  1. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
  3. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

数据库优化思路

SQL语句优化

  1. 避免!= or <>,避免引起全盘扫描。
  2. 避免对null的判断(设置默认值)。
  3. Exists代替in。
  4. Where替代having,因为having在查询结果后对结果集筛选过滤。
  5. 避免子查询,使用连接查询

索引优化
见索引的工作原理。

结构优化
数据引擎、分库分表。

spring如何使用异步任务

@EnableAsync

循环依赖问题
在SpringBoot项目中添加@EnableAsync是使@Async 注解生效的。之前没加这个注解的时候异步方法都是没有生效的。而我们Async 是通过AOP生成代理类来实现异步执行的。
当我们Spring的IOC容器检查到@Async 注解之后,会通过AOP这个方法所在的类生成一个代理类。注入的时候发现该Bean已经被其他对象注入了,所以这就出现了问题了。
解决办法
把需要异步执行的任务统一放在一个类里面,在注入该类时添加@Lazy注解,使用spring懒加载,在需要执行异步方法时生成代理对象bean注入spring。使之注入的是代理类的Bean,而不是原始的Bean。

    @Autowired
    @Lazy
    private payNotifyService payNotifyService;