目录
4、什么是OutOfMemoryError异常(内存溢出错误,OOM)?
17、深入理解java虚拟机介绍了两个可视化监控工具,此处记录下:
0、jvm 运行时区域 如何划分?
线程私有:
1)程序计数器:记录线程指令行号,方便线程切换、分支跳等定位
2)虚拟栈区:存放线程 java 方法局部变量表,操作数栈,动态链接、方法出口等信息
3)本地方法栈:服务于Native方法(Hotspot虚拟机不区分2和3)
线程共享:
1)java堆区:存放 java对象实例 信息,堆区 可分为 “新生代” 和 “老年代”
2)方法区:存放 类信息、常量、静态变量、及时编译器编译后的代码等数据
1、java对象创建的过程?
1)jvm先检查类是否已被加载、解析和初始化,若未初始化,需要执行类加载
2)为新生对象分配 堆 内存
对象内存分配方法:
“指针碰撞”:假设内存规整,一边是空闲内存,一边是使用中内存,中间是指针,分配时将指针向空闲内存移动一个对象大小的距离。
“空闲列表”:假设内存不规整,jvm维护一个可用空闲内存列表,分配时从列表中选择一块划分给对象
分配内存可能存在的问题:
并发情况下,存在A对象移动指针后,B用原来指针来分配内存的情况
解决方法:
1、jvm使用CAS配上失败重试的方法保证更新操作原子性
2、为每个线程预先分配一块内存,称为“本地线程分配缓冲(TLAB)”,只有TLAB用完后,才需要同步锁定
3)jvm给对象分配内存后,初始化值为0,下一步仍需执行init,进行真正的初始化赋值
2、jvm中,对象在内存分为哪几部分?
1)对象头:又称“Mark Word”存储运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳。长度32bit或64bit
2)实例数据:对象真正存储的有效信息,也就是各种类型的字段内容,包含继承父类和子类定义的。
3)对齐填充:非必然存在,起占位符作用。对象大小必须是8字节(64bit)的整数倍。
3、jvm中,如何访问对象?
通过 栈上的 reference数据 来操作 堆上的对象
1)句柄访问:堆内存中会划分一块内存作为句柄池,句柄中放对象实例数据和类型数据的具体地址信息,reference中存储的就是对象的 句柄地址。
2)直接指针访问:reference中存储的是 对象实例数据地址,对象实例数据中 则存放有对象类型数据地址。
两种方式各有优劣:
句柄访问优势在于 reference中存储稳定的句柄地址,对象移动只会修改句柄的实例数据指针
直接指针访问优势在于 速度快。
4、什么是OutOfMemoryError异常(内存溢出错误,OOM)?
引申三个问题:
1)如何判断OOM发生区域?
2)什么样的代码会导致这些区域出现OOM?
3)出现OOM如何处理?
1)java堆溢出
堆内存 用于存放对象实例,只要不断创建对象,并保证 GC Roots到对象之间有可达路径来避免垃圾回收机制清除,当达到堆最大容量限制后,会产生OOM
针对这种情况,常见日志打印如下:(heap space : 堆空间)
“java.lang.OutOfMemoryError: Java heap space”
如何处理?
1)加-XX:HeapDumpOnOutOfMemoryError可让jvm 在 OOM时 Dump出 堆存储快照
2)使用内存映像分析工具 对 快照 分析,重点确认 内存中对象是否必要
3)确认是 内存泄漏 还是 内存溢出
如果是内存泄漏 可进一步通过工具查看对象到GC Roots的引用链,从而确认是怎样的路径关联导致GC无法回收,掌握了泄露对象类型信息和GC Roots引用链信息,就可比较准确定位泄露代码位置。
如果不存在内存泄漏,内存中对象确实必须存在,则应检查虚拟机堆参数(-Xms和-Xmx)是否可以调大;并检查代码中某些对象是否生命周期过长,并尝试减少内存消耗。
2)虚拟机栈和本地方法栈溢出
HotSpot jvm 不区分虚拟机栈和本地方法栈,因此栈容量由 -Xss参数设定。
常见日志打印:
1)如果线程请求的栈深度 大于 虚拟栈 所允许的最大深度,将抛出 “StackOverflowError” 异常
2)如果 虚拟栈容量可以动态扩展,且扩展时无法申请到足够内存空间,将抛出 “OutOfMemoryError” 异常
从测试情况看,第一种异常比较好模拟,方法如下:
1)减小-Xss参数配置
2)定义大量本地变量,以增大此方法栈中本地变量表的长度
注意:栈容量和线程数是挂钩的,一个进程内,每个线程分配到的栈容量越大,可以建立的线程数越少。
出现 “StackOverflowError” 异常时,有错误堆栈信息可以阅读,相对比较容易找到问题所在。
3)方法区和运行时常量池溢出
运行时常量区是方法区的一部分
常用模拟方法:
产生大量常量和类去填满方法区
常见日志打印:
运行时常量池溢出:“java.lang.OutOfMemoryError: PermGen space”
4)本机直接内存溢出
直接内存(DirectMemory)并不是 JVM运行时数据区域所定义的任何一部分,是指这些区域之外的内存。在JDK1.4中新加入了NIO(NEW Input/Output)类,并引入了基于通道(channel)和缓冲区(Buffer)的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用,并进行操作,这样能显著提高性能,避免了Java堆和Native堆来回复制数据。
什么场景使用直接内存?
需要频繁访问大内存,而不是申请和释放空间场景
DirectMemory 容量 可通过 -XX指定,默认和-Xms一样
常见日志打印:“java.lang.OutOfMemoryError”
由于申请直接内存不有虚拟机管理,由此导致的OOM,不会有明显异常打印。如果出现OOM,且dump文件很小,同事程序又使用NIO,可以考虑这方面原因
5、什么是垃圾回收,以及其需要做什么事?
GC(Garbage Collection),功能就是 回收内存
GC需要解决如下三个问题
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
GC回收主要针对线程共享区域,也就是java堆区和方法区,线程私有区域则随线程生灭。
6、如何判断内存需要回收?
1)引用计数算法
给每个对象添加一个引用计数器,一个地方引用,计数器值+1,引用失效,则-1,当引用计数值为0,对象就是可回收的。
算法优势:
实现简单
算法问题:
无法解决两个对象互相引用,计数器值均不为0,导致无法GC的问题
因此,目前主流jvm未使用该算法
2)可达性分析算法
以GC Roots对象为起始点,分析对象到 GC Roots 有无 引用链相连接,若没有,则为可回收对象
GC Roots对象包括下面几种:
1)虚拟机栈中引用的对象
2)方法区中静态变量属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Native方法)引用的对象
7、java中,引用是什么?
java中引用分为如下4类:
1)强引用
代码中普遍存在的引用,如 Object ob = new Object()
2)软引用
用于描述 有用但非必需 的对象,当发生内存溢出异常前,会把这些对象列进回收范围
java中用SoftReference类来实现软引用
3)弱引用
用于描述 非必需 对象,下次垃圾回收,这种对象都会回收
4)虚引用
顾名思义,就是虚幻的引用,无法影响对象生存时间,也无法通过它获取对象实例
其作用 就是 在垃圾回收前,收到一个系统通知
8、java中,对象死亡过程是怎样的?
1)第一次标记过程:
可达性分析算法 标记 某个对象 为不可达对象,此时 对象 处于 GC前的缓刑阶段
2)第二次标记过程:
1)当对象 没有覆盖 finalize() 方法 或者 finalize() 方法已被JVM调用过,则 直接GC
2)当对象 有必要执行 finalize() 方法,则将其放入 F-Queue 队列,等待 Jvm 建立Finalizer线程去 触发该对象的 finalize()方法
1)当 对象在 finalize() 方法 中重新建立的 引用,则逃脱 GC
2)当 对象在 finalize() 方法 中未建立 引用,则被GC
注意:finalize() 方法开销较大,建议不要使用
8、java中,方法区(永久代)是判断可回收的呢?
方法区 主要回收 废弃常量 和 无用类
但方法区回收 性价比低,一般不处理
如何判定废弃常量:
该常量没有被任何引用
如何判定无用类,三个条件需要同时满足:
1)该类所有实例已被回收
2)加载该类的 ClassLoader 已被回收
3)该类对应的 java.lang.Class 对象 没有在任何地方被引用,无法则任何地方通过反射 访问该类方法
9、如何回收,垃圾收集算法有哪些?
1)标记-清除算法
如何标记?
见对象死亡过程中的二次标记
如何清除?
标记完成,统一回收
这种算法,最基础,但标记效率和回收效率 均不高,易产生内存碎片
2)复制算法
将内存分为大小相等两块,每次使用一块,当内存用完的时候,将存活对象复制到另一块上,然后整体清理使用的内存空间
这种方式有点浪费内存空间,一般将内存分为3块,一块eden空间 和 两块 survivor空间,每次使用 一块eden空间 和 一块survivor空间,每次将存活对象复制到剩下一块survivor空间中
一块survivor空间 约占 10%
--- 很明显,这种算法,不适合对象存活率较高的场景,也就是老年代
3)标记-整理算法
“标记-整理” 相比 “标记-清除”,就是将 存活的对象 向一端移动,然后整体清理端边界以外的内存。
4)分代收集算法
这种算法其实是上述几种方法的总结。
将java堆,根据 对象存活周期不同,将内存划分为几块,一般如下划分:
{
新生代:对象存活率较低的区域 --- 采用 复制算法 清理
老年代:对象存活率较高的区域 --- 采用 标记-清理/整理算法 清理
}
10、HotSpot 虚拟机 如何实现 上述算法?
了解以下两点即可:
1、GC进行时,会停顿所有执行线程,确保引用不会发生变化
2、使用 OopMap 数据结构,保存对象引用,以便快速定位
3、安全点 --- 定义了 何时 停顿
4、安全域 --- 定义了 何时 停顿
11、垃圾收集器,垃圾收集算法的具体实现?
新生代 垃圾收集器:
1)serial 收集器:单线程,复制算法,运行时,其他线程中断,适合单核CPU
2)parnew 收集器:多线程,复制算法,运行时,其他线程中断,适合多核CPU
3)parallel scavenge 收集器:多线程,吞吐量高,吞吐量=(线程运行时间-GC时间)/ 线程运行时间
老年代 垃圾收集器:
1)serial old:单线程,标记-整理算法,运行时,其他线程中断
2)parallel old:多线程,标记-整理算法,运行时,其他线程中断
3)CMS:多线程,停顿时间极短,收集过程分为4个阶段:初始标记、并发标记、重新标记、并发清除,其中 并发标记和并发清除 耗时长 但 可与工作线程 并发执行
--- 无法清理浮动垃圾,易产生内存碎片
两个年代都可以用的:
G1 收集器:支持 并行并发、分代收集、空间整合、可预测停顿
12、什么是 Full GC、Minor GC?
新生代GC(Minor GC):指发生在 新生代的 GC,该操作频繁,速度快。
老年代GC(Major GC/Full GC):指发生在 老年代的 GC,速度较慢
13、如何理解GC日志?
案例“33.125 [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]”
含义:jVM启动以来经过的秒数 [GC/Full GC [含New/Young表示新生代: GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量), 该内存区域GC所占用时间] GC前Java堆已使用容量->GC后Java堆已使用容量, 该区域GC所占用时间]
14、jvm内存分配如何进行?
1)若开启了本地线程分配缓冲,会先在TLAB上分配
2)大多数情况下,对象在 Eden区中分配
3)大对象(长字符串和数组),长期存活对象 直接在 老年代进行内存分配
15、如何判断 对象年龄?
1)jvm 给 每个对象 分配了一个年龄计数器,对象在 Eden区 诞生,,经过一次 Minor GC后,年龄+1,若可以被survivor区域容纳,则被移入,在survivor区域中,每经过一次Minor GC 年龄+1,当年龄增加到一定程度(默认15岁,可配置),会被晋升为老年代
2)若survivor区域中,某个年龄段的对象 大小总和 超过了 该区域空间的一半,那么 大于这个年龄段的对象 直接进入老年代
16、jvm 常用排查工具?
这些工具 在 jdk 的 bin 目录下
jps:显示指定系统内所有HotSpot虚拟机进程(类似Linux命令 ps),命令格式:jps [-q|-m|-l|-v]
jstat:用于收集HotSpot虚拟机各方面的运行数据,命令格式:jstat [-class|-gc|...] [线程id] [间隔时间 毫秒] [查询次数]
jinfo:显示虚拟机配置信息,命令格式:jinfo [-flag|-sysprops] [进程id],jinfo -flag name=value 可修改运行时虚拟机参数
jmap:生成虚拟机的内存存储快照(heapdump或dump文件),命令格式:jmap [-dump...] [进程id]
jhat:用于分析heapdump文件,它会建立一个http/html服务器,让用户可以在浏览器上查看分析结果,一般使用更专业的内存分析工具,如eclipse memory Analyzer等
jstack:显示虚拟机线程快照,命令格式:jstack [-F|-l|-m] [线程id]
上述命令,可加 -h 详细了解使用方法
补充生成dump文件的三种方法:
1)jvm启动时添加 -XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常后自动生成dump文件
2)jvm启动时添加 -XX:+HeapDumpOnCtrlBreak参数,可以使用[Ctrl]+[Break]键,让虚拟机生成dump文件
3)在Linux下,使用kill -3 命令发送进程退出信号给虚拟机,也能拿到dump文件
引申问题:什么是线程快照?
指当前jvm内每条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待 等。线程出现停顿时,通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事,或者等待什么资源。
17、深入理解java虚拟机介绍了两个可视化监控工具,此处记录下:
1)JConsole:java监视和管理控制工具
2)VisualVM:多合一故障处理工具
18、简述类生命周期?
加载-->验证-->准备-->解析-->初始化-->使用-->卸载
19、双亲委派模型工作过程?
如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类记载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
其好处在于 java类随着它的类加载器一起具备了一种带有优先级的层次关系,解决了各个类加载器的基础类统一问题。
ps. 只有在类加载器是同一个的情况下,才能谈类是否相同。
20、破坏双亲委派模型的情况有?
1)JNDI(jdbc)服务中,启动类加载器去加载时,需要调用 JNDI接口提供者的代码,此时启动类加载器无法识别代码。为此引入了线程上下文类加载器,它可以使 父类加载器 可以请求 子类加载器去完成类加载的动作。
2)代码热替换,热部署:OSGi
21、java内存模型?
java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出变量的底层细节。
高速缓存:读写速度接近处理器
处理器 <---> 高速缓存 <---缓存一致性协议---> 主内存
产生缓存一致性的原因:每个处理器均有自己的高速缓存,但共享一个主内存,因此需要缓存一致性协议来保障 数据一致性
java线程 <---> 工作内存 <---save和load操作---> 主内存
工作内存是线程独有,线程对变量的所有操作都必须在工作内存中进行。
线程间 变量值的传递 需要在主内存完成
主内存、工作内存
和
堆、栈、方法区
的划分不是同一层次
变量如何从主内存拷贝到工作内存?又如何从工作内存同步到主内存?
lock(锁定):作用于主内存变量,把一个变量 标识为 线程独占
unlock(解锁):作用主内存变量,将被线程独占的变量释放
read(读取):作用于主内存变量,把一个变量值 从主内存 传输到 线程的工作内存中,以便随后的load动作使用
load(载入):作用于 工作内存变量,将 read操作读取的变量值,放入工作内存的变量副本中
use(使用):作用于 工作内存变量,把 工作内存中的变量值 传递给执行引擎,每当虚拟机 遇到一个 需要使用变量值得 字节码指令时,会执行该操作
assign(赋值):作用于 工作内存变量,把 从执行引擎 接收到的值 赋值给 工作内存的变量,每当虚拟机 遇到一个 给变量赋值 的字节码指令时 执行该操作
store(存储):作用于 工作内存变量,把工作内存中的一个变量值 传送到 主内存中
write(写入):作用于 主内存变量,将store操作传送的变量值 写入 主内存变量中
上述操作必须满足如下规则:
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
22、java线程状态?
新建、运行、无限期等待、限期等待、阻塞、结束
23、线程安全的实现方法?
1)互斥(阻塞)同步(通过使用互斥锁来实现同步)
互斥是因/方法,同步是果/目的
1)synchronized关键字:注意点,其对同一个线程来说是可重入的,不会出现自己把自己锁死的情况,但阻塞其他线程
2)ReentrantLock(可重入锁)
*上述两种方法:一是原生语法层面的互斥锁,另一是API层面的互斥锁(需要lock()和unlock()方法配合try/finally语句来完成)
*上述两种方法可实现以下3项高级特性:
1)等待可中断
这个特性的含义是:等待获取锁的线程可以选择放弃等待,改为处理其他事情
2)可实现公平锁
公平的含义是:等待获取锁时,必须按照申请锁的时间顺序来依次获得锁
synchronized和ReentrantLock 默认都是 非公平锁,但可以通过带布尔值的构造方法要求使用公平锁
3)锁可以绑定多个条件
指 ReentrantLock 对象 可以同时绑定多个 Condition 对象
*互斥锁 属于 “悲观” 并发策略,先加锁后操作
2)非阻塞同步(通过使用非阻塞方式来实现同步)
基于 “乐观” 并发策略,先操作,再进行冲突检测,若操作失败,则采取补救措施(常见补偿措施就是 重试)。
这种方法依赖于 “操作”和“冲突检测” 要具备原子性。具备这种能完成多次操作的原子操作指令有如下几种:
1)测试并设置(Test-and-Set)
2)获取并增加(Fetch-and-Increment)
3)交换(Swap)
4)比较并交换(Compare-and-Swap,CAS)
5)加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)
常用第三种指令CAS,是实现非阻塞同步。
CAS指令 需要3个操作数,分别是 变量的内存地址V、旧的预期值A 和 新值B。
操作过程是:当且仅当V符合预期值A时,处理器会用新值B更新V的值,否则就不更新,但无论是否更新成功,均返回V的旧值A。这一过程是原子操作,由sun.misc.Unsafe类中方法提供。
CAS存在的问题:无法避免 旧值A 是先变成 B 后又被更新成 A的问题,这个值其实被更新过。不过大部分情况下,ABA问题不影响程序并发执行。
3)无同步方案
要保证线程安全,并不是一定要进行同步,两者并没有因果关系。同步只是保证共享数据争用时的正确手段,如果一个方法本来就不涉及共享数据,那么自然无须任何同步措施,天然线程安全。举例如下:
1)可重入代码
不依赖存储在堆上的数据和公用资源,用到的状态量均由参数传入,不调用非可重入方法等。
2)线程本地存储
共享数据 在 同一个线程中。
24、锁的四种状态?
1)无锁
2)偏向锁
偏向第一个获取锁的线程,在对象头中记录 线程ID,如果下次来还是它,直接获取锁,降低了获取锁的成本
3)轻量级锁
应用了自旋锁和自适应自旋锁技术,含义是 其他线程获取锁时,会自适应等待一段时间来获取锁,不会阻塞
4)重量级锁
会阻塞来获取锁的其他线程
25、共享锁和排它锁
共享锁,可以被多个线程共有,如读锁,获取共享锁线程只有读权限。
排它锁,只可以被一个线程所有,如写锁,获取排它锁的线程可读可写。
26、如何避免死锁?
1)避免一个线程同时获取多个锁
2)避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3)尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制
4)对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
27、什么是上下文切换?
cpu保存一个任务状态,再切换另一个任务执行,在切回这个任务,表示一次上下文切换
线程创建和上下文的切换会带来一定开销
28、如何减少上下文切换?
无锁并发编程
CAS算法
使用最少线程
协程
29、QPS TPS OPS 含义?
QPS(Query Per Second):每秒请求数,就是说服务器在一秒的时间内处理了多少个请求。
TPS:Transactions Per Second(每秒传输的事物处理个数),即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出,加上一次用户数据库访问。
redis OPS 每秒操作的指令数