Java 面试经验总结
总结不易,希望大家给点支持,坚持每日一更,如果需要更加全面的资料可以联系wx:qh950520
1. JVM 部分
1. JVM内存模型
JVM内存模型主要是指我们的JVM运行时数据区:
主要分为线程私有和线程共有两个部分:
线程私有:虚拟机栈,本地方法栈,程序计数器
线程共有:堆 和 方法区(1.8元空间)
vm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
2. 垃圾回收
垃圾回收的区域:堆内存
垃圾判断方法:
引用计数法:每个对象有一个引用计数属性,新增一个引用计数加1,引用释放计数-1,计数为0可回收。缺点是:无法解决对象间的重复引用问题。
可达性分析法:从GC ROOT开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC ROOT没有任何引用链时,证明这个对象不可用。即为不可达对象。
GC ROOT包括:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区常量引用的对象,本地方法栈中JNI引用的对象
(重点)垃圾回收算法:
标记-清除
复制算法
标记-整理
分代收集算法
年轻代:复制算法(新生代对象比较多,存活期短)
老年大:标记整理算法
年轻代分为三块区域:EDEN,FROM,TO
对象优先在新生代 Eden 区中分配,如果 Eden 区没有足够的空间时,则会发起minor gc,将这两块区域存活的对象放入另外一个survivor区并给gc年龄+1,对已使用空间进行垃圾回收。经过15次垃圾回收还存活的,会被放到老年代。
Full GC 的触发条件有多个,FULL GC 的时候会 STOP THE WORD
老年代空间不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
7种垃圾回收器:
新生代的垃圾回收器有:
Serial(串行运行,复制算法,响应速度快,使用与单cpu环境)
ParNew(并行运行,复制算法,响应速度快,多CPU环境与CMS配合使用)
Parallel Scavenge (并行运行,复制算法,吞吐量大,使用与后台运行而不需要太多交互的场景)
老年代的垃圾回收有:
Serial Old(串行运行,标记整理,响应速度快,适用于单CPU环境)
Parallel Old(并行运行,标记整理,吞吐量大,使用与后台运行而不需要太多交互的场景)
CMS(并发运行,标记-清除算法,响应速度快,适用于互联网B/S业务场景)
最新垃圾回收器:
G1(并发,并行运行,根绝区域进行回收,对新生代采用复制算法,对老年代采用标记整理,面向服务端)
JAVA 四中引用类型
5.1 强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
5.2 软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
5.3 弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
5.4 虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
JVM垃圾回收器中:G1和CMS有什么区别?
CMS采用的是标记-清除来回收老年代垃圾,会产生短暂的停顿,而G1利用机器的多核并发处理STW,可不停顿执行java进程执行gc。
3.类加载机制
类加载分为三个部分:加载-->连接(验证,准备,解析)-->初始化
加载:将class文件的二进制数据加载到jvm内存中并存放在方法区,然后利用字节码文件创建class对象,存放在堆区
验证:验证class的字节流信息是否符合虚拟机的要求。
准备:为静态成员变量赋默认值,比如static int a = 8 , a = 0;
解析:虚拟机将常量池内的符号引用替换成直接引用的过程
初始化:查看是否具有父类,有的话先初始化父类---先初始化的静态成员,静态代码
类的初始化与实例化的区别?
静态代码块,静态成员变量只初始化一次。在类的初始化过程中执行!!!
非静态代码块和非静态成员变量和构造器的执行都是在实例化过程中被执行,每次实例化都会执行!!!
*类的初始化和类的实例化,这两个过程是两个互相独立的,两个过程所要执行的任务也是分开的!!!*
*类的初始化和类的实例化执行顺序也是不一定的,比如:类实例化完成时,类的初始化不一定已经完成了。在类初始化过程中,可以实例化本类!!!*
4.java基础知识
java继承和组合?怎么选择
一、相比于组合,继承有以下优点:
1、在继承中,子类自动继承父类的非私有成员(default类型视是否同包而定),在需要时,可选择直接使用或重写。
2、在继承中,创建子类对象时,无需创建父类对象,因为系统会自动完成;而在组合中,创建组合类的对象时,通常需要创建其所使用的所有类的对象。
二、组合的优点:
1、在组合中,组合类与调用类之间低耦合;而在继承中子类与父类高耦合。
2、可动态组合。
5. jvm 调优
JVisualVM堆信息查看可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)查看堆内类、对象信息查看:数量、类型等有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
--年老代年轻代大小划分是否合理
--内存泄漏
--垃圾回收算法设置是否合理
线程监控线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下Dump线程详细信息:查看线程内部运行情况
死锁检查
CPU热点:检查系统哪些方法占用的大量CPU时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
java.lang.OutOfMemoryError: Java heap space 这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
java.lang.OutOfMemoryError: PermGen space
主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
1. -XX:MaxPermSize=16m
2. 换用JDK。比如JRocket。
java.lang.StackOverflowError
这个就不多说了,一般就是递归没返回,或者循环调用造成
Fatal: Stack size too small
增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
java.lang.OutOfMemoryError:unabletocreatenewnativethread
1.重新设计系统减少线程数量。
2.线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
一、什么情况下会发生栈内存溢出?
1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;2、当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;3、调整参数-xss去调整jvm栈的大小
2. JAVA多线程
java多线程
1. 线程和线程池
创建线程的几种方式:
继承Thread
实现Runnable接口
实现Callable<T>接口,通过FutureTask包装器来创建Thread线程
class A{
call(){
retrun "ok"
}
main{
FutrueTask task =new FutrueTask(new A())
new Thread(task).start();
task.get()
}
}
线程池创建线程
使用ExecutorService、Callable、Future实现有返回结果的多线程
线程池:
理解什么是线程池
1)重复利用已创建的线程降低线程的创建和销毁造成的消耗
2)响应速度快,任务到达时,任务不需要等待线程创建就能立即执行
3)提高线程的可管理性。线程池可以对线程进行统一的分配和监控
线程池的工作原理
当线程池中的存活核心线程数小于corePoolSize时,就会创建一个核心线程处理提交的任务
当核心线程数满了,即线程数已等于corePoolSize,新提交的任务会被放到阻塞队列
当线程池存过的线程数等于corePoolSize,并且队列也满了,判断线程数是否达到maxnumPoolSize,即最大线程数是否已满,如果没满就创建一个核心线程去执行提交的任务
当线程数量达到maxnumPoolSize时,还有新的任务过来,直接采用拒绝策略处理。
插入一个面试题:如果阻塞队列使用无界队列,会发生什么?
使用无界队列来做阻塞队列时,就不需要考虑决绝策略了,因为等待队列永远不会满,但是如果此时线程池每个线程取到一个任务时耗时特别长,这个时候无界队列任务会越来越多,这一过程可能导致机器内存不停的飙升,极端情况会发生OOM
核心线程池怎么回收
threadPoolExecutor.allowCoreThreadTimeOut(true);
拒绝策略
AbortPolicy策略:丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息(线程池默认的拒绝策略)
CallerRunsPolicy策略:只要线程池没有关闭的话,则使用调用线程直接运行任务。
DiscardOleddestPolicy策略:只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
DiscardPolicy策略:直接丢弃,其他啥都没有
线程池的核心参数
corePoolSize 线程池核心线程数量的最大值
maximumPoolSize:线程池最大线程数大小
keepAliveTime:线程池中非核心线程空闲的存活时间大小
unit:线程空闲存活时间单位
workQueue:存放任务的阻塞队列
threadFactory:创建新线程的工厂,所有线程都是通过该工厂创建的,有默认实现
handler:线程池的拒绝策略
线程池有几种工作队列
ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
LinkedBlockingQueue一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
SynchronousQueue一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool(5)使用了这个队列。
PriorityBlockingQueue一个具有优先级的无限阻塞队列。
线程池的核心方法
void execute(Runable) ,没有返回值,无法判断任务的执行状态。
submit() 也是用来向线程池中提交任务的, 它和恶execute()方法不一样的是, 它能够返回执行的结果 (其实内部还是调用了execute(),用的Future 来获取执行结果),
shutdown()
shutdownNow()关闭线程
面试:为什么不用Executors去创建线程池?
1)newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。2)newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
面试:说下常见的线程池和使用场景?
newSingleThreadExecutor
一个单线程化的线程池,它只会用唯一的工作线程来执行任务
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
使用场景:我们用户发送一个查询用户积分的请求,这个时候后台会调用很多服务,用户积分使用服务,用户积分兑换的服务,而这些都是相互独立的,这个时候就不能用我们主线程去顺序完成这些操作,可以使用newCachedThreadPool去执行我们积分兑换的服务,这样就充分利用了我们的线程资源
面试:单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。
阻塞队列持久化,正在处理事物控制。断电之后正在处理的回滚,日志恢复该次操作。服务器重启后阻塞队列中的数据再加载
java多线程使用场景?
一般来说,我们需要在后台处理的任务,通常会使用定时器来开启后台线程处理,比如有些数据表的状态我需要定时去修改、我们搜索引擎里面的数据需要定时去采集、定时生成统计信息、定时清理上传的垃圾文件等。
例子很多比如,用户要查下本月积分,这个就涉及用户积分查询,用户积分兑换情况查询,这几个都是互不相干的,用一个主线程单独去查太浪费资源了,可以使用线程池newCachedThreadPool(5) 去多线程执行。
每晚12点定时统计每天订单受理量,生产统计信息等。
java并发场景?
场景:夏日流量狂欢, 10元3G流量套餐,7日内使用,限量10000份
因为公司的项目都是中心化的,同时有套zk可以使用,当时上线这个活动就是使用zk分布式锁来来做的。
为什么不考虑使用redis分布式锁呢?
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能
zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小