文章目录 面试 线程相关 1、线程有哪些状态? 2、线程池的核心参数 3、sleep VS wait 4、lack VS synchronized 5、volatile能否保证线程安全? 6、悲观锁VS乐观锁 7、HashTable Vs CurrentHashMap 8、ThreadLocal的理解 虚拟机相关 1、JVM 内存结构 1、程序计数器(==线程私有==) 2、虛拟机栈(==线程私有==) 3、 本地方法栈(==线程私有==) 4、堆(==线程共有==) 5、方法区(==线程共有==) 6、运行时常量池 7、直接内存 Q:哪些部分会出现内存溢出? Q:方法区与永久代、元空间之间的关系? 2、JVM内存参数 3、JVM 垃圾回收 4、内存溢出 5、类加载 6、四种引用 7、finalize Q:finalize的理解 为什么finalize方法非常不好,非常影响性能? 两个重要队列
面试
线程相关
1、线程有哪些状态?
Java线程分成6种状态。
操作系统层面有5种状态
①分到CPU时间的:运行 ②可以分到CPU时间的:就绪 ③分不到CPU时间的:阻塞 注意: Java中的RUNNABLE涵盖了就绪、运行、阻塞I/O。
2、线程池的核心参数
corePoolSize 核心线程数目 (最多保留的线程数)maximumPoolSize 最大线程数目(核心线程+救急线程)keepAliveTime 生存时间 (针对救急线程)unit 时间单位(针对救急线程)workQueue (阻塞队列)threadFactory 线程工厂 (可以为线程创建时起个好名字)handler 拒绝策略(四种) AbortPolicy:直接抛出异常。 DiscardPolicy:放弃任务,不抛出异常。 DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常。 CallerRunsPolicy:谁调用谁处理。
案例:银行排队办业务的案例。
线程池在启动的时候会按照核心池的数来创建初始化的线程对象2个。 开始分配任务,如果同时来了多个任务, 2个线程对象都被占用了,第3个以及之后的任务进入等待队列,当前有线程完成任务恢复空闲状态的时候,等待队列中的任务获取线程对象。 如果等待队列也占满了,又有新的任务进来,需要去协调,让线程池再创建新的线程对象,但是线程池不可能无限去创建线程对象,一定会有一个最大上限,就是线程池的最大容量。 如果线程池已经达到了最大上限,并且等待队列也占满了,此时如果有新的任务进来,只能选择拒绝,并且需要根据拒绝策略来选择对应的方案。
3、sleep VS wait
共同点: wait()、wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃CPU的使用权,进阻塞状态。 方法归属不同 sleep(long) 是Thread的静态方法。而 wait(), wait(long) 都是Object的成员方法,每个对象都有。 醒来时机不同 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来。 wait(long) 和 wait() 还可以被notify唤醒,wait() 如果不唤醒就一直等下去。 它们都可以被打断唤醒。 锁特性不同 wait方法的调用必须先获取wait对象的锁 ,而sleep 则无此限制。 wait方法执行后会释放对象锁 ,允许其它线程获得该对象锁(我放弃,但你们还可以用)。 而sleep如果在synchronized代码块中执行,并不会释放对象锁 (我放弃,你们也用不了)。
4、lack VS synchronized
语法层面 synchronized是关键字,源码在jvm中,用C++语言实现。 Lock是接口,源码由jdk提供,用java语言实现。 使用synchronized时,退出同步代码块锁会自动释放 ,而使用Lock时,需要手动调用unlock方法释放锁 。 功能层面 二者均属于悲观锁 、都具备基本的互斥、同步、锁重入功能。 Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量。 性能层面 在没有竞争时,synchronized做 了很多优化,如偏向锁、轻量级锁,性能不赖。 在竞争激烈时,Lock的实现通常会提供更好的性能。
5、volatile能否保证线程安全?
volatile关键字是JVM提供的轻量级同步机制,“可见性” ,主内存对象线程可见。 一个线程执行完任务后,会把变量存回主内存中,并且从主内存中读取当前最新的值,如果线程中是一个空的任务, 则不会重新读取主内存中的值。
线程安全要考虑三个方面:可见性、有序性、原子性 ①可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果。 ②有序性指,一个线程内代码按编写顺序执行。 ③原子性指,一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队。
volatile能够保证共享变量的可见性与有序性,但并不能保证原子性 。 ①原子性举例 ②可见性举例 ③有序性举例
synchronized vs volatile的区别 synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!
volaile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是voltile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块。 volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。 volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源 的同步性。 即synchronized是线程安全的,而volatile是非线程安全的。
6、悲观锁VS乐观锁
悲观锁的代表是synchronized 和Lock 锁
① 其核心思想是 [ 线程只有占有了锁,才能去操作共享变量 ,每次只有一个线程占锁成功,获取锁失败的线程,都得 停下来等待 ] ; ② 线程从运行到阳塞、 再从阻塞到唤醒,涉及线程上下文切换 ,如果颁繁发生,影响性能; ③ 实际上,线程在获取synchronized和ock锁时, 如果锁已被占用,都会做几次重试操作,减少阻塞的机会;
乐观锁的代表是 AtomicInteger (原子整数类),使用 cas 来保证原子性。
① 其核心思想是 [ 无需加锁,每次只有一个线程能成功修改共享变量, 其它失败的线程不需要停止,不断重试直至成 功 ] ; ② 由于线程一直运行, 不需要阻塞,因此 不涉及线程上下文切换 ; ③ 它需要多核cpu支持,且线程数不应超过cpu核数;
何谓上下文切换? 上下文切换: java线程在切换前会保存上一个任务的状态, 以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一 次上下文切换 。
7、HashTable Vs CurrentHashMap
Hashtable 与 ConcurrentHashMap 都是 线程安全 的Map集合; Hashtable 并发度低,整个Hashtable对应一把锁 ,同一时刻,只能有一个线程操作它; 1.8 之前 ConcurrentHashMap 使用了 Segment + 数组 + 链表 的结构,每个Segment对应一把锁,如果多个线程访问,不同的Segment,则不会冲突; 1.8 开始 ConcurrentHashMap 将数组的 每个头节点作为锁 ,如果多个线程访问的头节点不同,则不会冲突; ①演示并发put。 ②演示扩容,说明三个问题forwardingNode ,扩容时的get,扩容时的put。
8、ThreadLocal的理解
ThreadLocal 可以实现 [ 资源对象 ] 的线程隔离 ,让每个线程各用各的 [ 资源对象 ] ,避免争用引发的线程安全问题。 ThreadLocal 同时实现了 线程内的资源共享 。 其原理是,每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象。 ①调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中。 ②调用get方法,就是以ThreadLocal自 己作为key,到当前线程中查找关联的资源值。 ③调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。 为什么ThreadLocalMap中的key ( 即ThreadLocal )要设计为弱引用? ①Thread可能需要长时间运行(如线程池中的线程),如果key不再使用,需要在内存不足(GC)时释放其占用的内存。 ②但GC仅是让key的内存释放,后续还要根据key是否为null来进一步释放值的内存,释放时机有 获取key发现null key; set key时,会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现null key有关。 remove时(推荐), 因为- -般使用ThreadLocal时都把它作为静态变量, 因此GC无法回收。
虚拟机相关
1、JVM 内存结构
1、程序计数器(线程私有 )
作用:
字节码解释器通过改变程序计数器来依次读取指令,从而 实现代码的流程控制 ,如:顺序执行、选择、循环、异常处理; 在多线程的情况下,程序计数器用于记录当前线程执行的位置 ,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意: 程序计数器是 唯一个不会出现OutOfMemoryError的内存区域 , 它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2、虛拟机栈(线程私有 )
描述Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。 每个线程只能有一个活动栈帧, 对应着当前正在执行的那个方法。
虚拟机栈会出现的错误:
StackOverFlowError: 若Java虚拟机栈的 内存大小不允许动态扩展 ,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误。OutOfMemoryError: 若Java虚拟机 堆中没有空闲内存 ,并且垃圾回收器也无法提供更多内存的话。就会抛出OutOfMemoryError错误。
3、 本地方法栈(线程私有 )
虚拟机栈和本地方法栈的区别: 虚拟机栈为虚拟机执行Java 方法(也就是字节码) 服务,而本地方法栈则为虚拟机使用到的Native方法服务。 在HotSpot虚拟机中和Java虚拟机栈合为一。 本地方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种 错误。
4、堆(线程共有 )
存放对象实例 ,存放对象的信息,几乎所有的对象实例以及数组都在这里分配内存。
在JDK 7版本及JDK 7版本之前,堆内存被通常被分为下面三部分:
新生代内存(Young Generation) 老年代(Old Generation) 永生代(Permanent Generation)
JDK 8版本之后方法区(HotSpot 的永久代)被彻底移除了,取而代之是元空间,元空间使用的是直接内存。
对象首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1 ,并且对象的年龄还会加1,(Eden区->Surivor区后对象的初始年龄变为1),当它的年龄增加到一 定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
堆中会出现的错误:
OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间就会发生此错误。java.lang.OutOfMemoryError: Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发 java.lang.OutOfMemoryError: Java heap space
错误。
5、方法区(线程共有 )
存放类的信息,方法区也被称为永久代。
常用参数: JDK 1.8之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。 JDK 1.8的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。下面是一些常用参数: 与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
整个 永久代有一个JVM本身设置固定大小上限,无法进行调整 ,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。当你元空间溢出时会得到如下错误:
你可以使用 -XX MaxMetaspaceSize
标志设置最大元空间大小,默认值为unlimited,这意味着它只受系统内存的限制。 -XX MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则Metaspace将根据运行时的应用程序需求动 态地重新调整大小。
元空间里面存放的是类的元数据 ,这样加载多少类的元数据就不由MaxPermSize控制 了,而由系统的实际可用空间来控制,这样 能加载的类就更多 了。在JDK8,合并HotSpot和JRockit的代码时,JRockit从来没有一个叫永久代的东西,合并之后就没有必要额外的设置 这么一个永久代的地方了。
6、运行时常量池
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代。 JDK1.7 字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。 JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)。
7、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError错误出现。
Q:哪些部分会出现内存溢出?
1、 不会出现内存溢出的区域 —— 程序计数器。
2、出现OutOfMemoryError的情况
① 堆内存耗尽 —— 对象越来越多,又一直在使用,不能被垃圾回收。 ② 方法区内存耗尽 —— 加载的类越来越多,很多框架都会在运行期间动态产生新的类。 ③ 虚拟机栈累积 —— 每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时。
3、出现StackOverflowError的区域
Q:方法区与永久代、元空间之间的关系?
①方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等。 ②永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)。 ③元空间是Hotspot虚拟机对JVM规范的实现(1.8以后), 使用本地内存作为这些信息的存储空间。
2、JVM内存参数
对于JIVM内存配置参数: -Xmx10240m -Xms10240m -Xmn5120m -XX:SurvivorRatio=3
其最小内存值和Survivor区 总大小分别是多少? -Xmx: Java虚拟机JVM的最大内存(单位m代表兆字节) -Xms: Java虚拟机JVM的最小内存 -Xmn: Java虚拟机JVM中新生代的内存大小 -XX:SurvivorRatio
新生代中eden区和from区的内存比例
云空间内存设置 代码缓存 -Xss 控制线程每个线程占用内存大小
3、JVM 垃圾回收
标记清除 标记整理 标记赋值
Q:说说GC和分代回收算法
GC的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度。
GC要点
回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存。 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象。 GC具体的实现称为垃圾回收器。 GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略。 根据GC的规模可以分成 Minor GC、Mixed GC、Full GC。
1、分代回收与GC规模
分代回收 ①伊甸园eden,最初对象都分配到这里,与幸存区合称新生代。 ②幸存区survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成from和to,采用标记复制算法。 ③老年代old,当幸存区对象经过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)。
2、漏标问题-记录标记过程中变化
Incremental Update —— 只要赋值发生,被赋值的对象就会被记录
Snapshot At The Beginning, SATB —— 新加对象会被记录。被删除弓用关系的对象也被记录。
垃圾回收器 1、Parallel Gc ①eden内存不足发生Minor GC,标记复制STW. ②old内存不足发生Full GC,标记整理STW。 ③注重吞吐量 。
2、ConcurrentMarkSweep GC ①old并发标记 ,重新标记时需要STW,并发清除 。 ②Failback Full GC。 ③注重响应时间 。
3、G1 GC ①响应时间与吞吐量兼顾 。 ②划分成多个区域,每个区域都可以充当eden, survivor, old, humongous。 ③新生代回收: eden 内存不足,标记复制STW。 ④并发标记: old 并发标记 ,重新标记时需要STW。 ⑤混合收集: 并发标记完成,开始混合收集 ,参与复制的有eden、survivor、 old,其中 old 会根据暂停时间目标,选择部分回收价值高的区域,复制时STW。 ⑥Failback Full GC。
4、内存溢出
Q:项目中什么情况下会内存溢出,怎么解决的?
误用线程池导致的内存溢出(误用固定大小线程池、误用带缓冲线程池) 查询数据量太大导致的内存溢出。 动态生成类导致的内存溢出。
5、类加载
Q:类加载过程、双亲委派
1、 加载 ① 将类的字节码载入方法区,并创建类 .class 对象。 ② 如果此类的父类没有加载,先加载父类。 ③ 加载是懒惰执行。
2、链接 ① 验证-验证类是否符合Class规范,合法性、安全性检查。 ② 准备 —— 为static变量分配空间,设置默认值。 ③ 解析 —— 将常量池的符号引用解析为直接引用。
3、 初始化 ① 执行静态代码块与非final静态变量的赋值。 ② 初始化是懒惰执行。
何为双亲委派 所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器 能找到这个类,由上级加载,加载后该类也对下级加载器可见。 找不到这个类,则下级类加载器才有资格执行加载。
上级加载后对下级加载器可见,而下级加载后对上级加载器不可见。
双亲委派的目的有两点 ①让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类。 ②让类的加载有优先次序,保证核心类优先加载。
6、四种引用
Q:对象引用类型分为哪几类?
1、强引用 ① 普通变量赋值即为强引用,如 A a = new A();
② 通过GC Root 的引用链,如果强引用不到该对象,该对象才能被回收。
2、软引用( SoftReference) ① 例如: SoftReference a = new SoftReference(new A());
② 如果仅有软引|用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象。 ③ 软引用自身需要配合引用队列来释放典型例子是反射数据。
3、弱引用(WeakReference) ① 例如: WeakReference a = new WeakReference(new A());
② 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象。 ③ 弱引用自身需要配合引用队列来释放。 ④ 典型例子是ThreadLocalMap中的Entry对象。
4、 虚引用( PhantomReference) ① 例如: PhantomReference a = new PhantomReference(new A());
② 必须配合引用队列一起使用,当虚弓|用引用的对象被回收时,会将虚弓|用对象入队,由Reference Handler线程释放其关联的外部资源。 ③ 典型例子是Cleaner释放DirectByteBuffer占用的直接内存。
7、finalize
Q:finalize的理解
一般的回答是:它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作。 较为优秀的回答是:将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM,从Java9 开始就被标注为 @Deprecated,不建议被使用了。
第一,从表面上我们能看出来finalize方法的调用次序并不能保证。 第二,日志中的Finalizer表示输出日志的线程名称,从这我们看出是这个叫做Finalizer的线程调用的finalize方法。 第三,你不能注释掉’System. in.read0’,否则会发现(绝大概率)并不会有任何输出结果了,从这我们看出finalize中 的代码并不能保证被执行。 第四,如果将finalize 中的代码出现异常会发现根本没有异常输出。 第五,还有个疑问,垃圾回收时就会立刻调广finalize方法吗?
为什么finalize方法非常不好,非常影响性能?
1、非常不好
FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放。 异常被吞掉这个就太糟了,你甚至不能判断有没有在释放资源时发生错误。
2、影响性能
重写了finalize方法的对象在第一次被GC时, 并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次GC时才能真正释放内存。 可以想象GC本就因为内存不足引起,finalize调用又很慢 (两个队列的移除操作,都是串行执行的,用来释放连接类的 资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易full GC, full GC后释放速度如果仍跟不上创建新对象的速度,就会00M。
两个重要队列
1、unfinalized队列 当重写了finalize方法的对象,在构造方法调用之时,JVM都会将其包装成一个Finalizer对象,并加入unfinalized 队列中 (静态成员变量、双向链表结构)。
2、ReferenceQueue队列 第二个重要的队列,也是Finalizer类中一个静态成员变量,名为queue (是一个单向链表结构),刚开始它是空的。当狗 对象可以被当作垃圾回收时,就会把这些狗对象对应的Finalizer对象加入这个队列。
3、真正回收时机 即使Dog对象没人引用,垃圾回收时也没法立刻回收它,因为Finalizer还在引用它嘛, 为的是 [ 先别着急回收啊,等我调完finalize方法,再回收 ] 。 查看FinalizerThread线程内的代码,这个线程从ReferenceQueue中逐一取出每个Finalizer对象,把它们从链表断开,这样没谁能引用到它,以及其对应的狗对象,所以下次GC时就可以被回收了。