面试知识点-JVM
2021-10-11 18:35:54 344 举报
AI智能生成
java虚拟机(持续更新)
作者其他创作
大纲/内容
对象
对象的创建
根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出ClassNotFoundException;
检查符号引用对应的类是否加载过
如果没有,则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
从堆中划分一块对应大小的内存空间给该对象
指针碰撞
java堆内存空间规整的情况下使用
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
空闲列表
java堆空间不规整的情况下使用
对象中的成员变量赋上初始值
设置对象头信息
调用对象的构造函数进行初始化
对象的内存布局
对象头
Mark Word
对象的hashCode
CG年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC标志
Class Metadata Address
指向对象实例的指针
对象的实例数据
对齐填充
对象的访问方式
指针
reference中存储的直接就是对象地址
句柄
java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息
两种方式的比较
使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot虚拟机使用的是直接指针访问的方式。
内存相关
内存分配
对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
最典型的大对象是那种很长的字符串以及数组。
避免在 Eden 区和 Survivor 区之间的大量内存复制。
长期存活对象进入老年区
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1,对象在Survivor区中每熬过一次 Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)_时,就会被晋升到老年代中。
对象年龄动态判定
如果在 Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
内存回收
Minor GC
特点
发生在新生代上,发生的较频繁,执行速度较快
触发条件
Eden区空间不足
空间分配担保
Full GC
特点
发生在老年代上,较少发生,执行速度较慢
触发条件
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC
内存溢出
程序在申请内存时,没有足够的内存空间
内存溢出的构造方式
堆溢出
OutOfMemoryError:不断创建对象
栈溢出
StackOverflowError: 增大本地变量表,例如不合理的递归
OutOfMemoryError:不断建立线程
方法区和运行时常量池溢出
OutOfMemoryError:通过String.intern()方法不断向常量池中添加常量,例如String.valueOf(i++).intern()
本机内存直接溢出
内存泄漏
程序在申请内存后,无法释放已申请的内存空间
原因
长生命周期的对象持有短生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为null
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏
解决方法
将内部类定义为static
用static的变量引用匿名内部类的实例
或将匿名内部类的实例化操作放到外部类的静态方法中
Hash值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
其他知识
动态绑定
指的是在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
编译阶段,根据引用本身的类型(Father)在方法表中查找匹配的方法,如果存在则编译通过
运行阶段,根据实例变量的类型(Son)在方法表中查找匹配的方法,如果实例变量重写了方法,则调用重写的方法,否则调用父类方法
以 Father ft=new Son();ft.say();为例
表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,则对应表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面。
参数传递
值传递
引用传递
Java 在参数传递的时候,实际上是传递的当前引用的一个拷贝
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
JVM调优
常见参数
Xms
Xmx
Xmn
Xss
-XX:SurvivorRatio
-XX:NewRatio
-XX:+PrintGCDetails
-XX:ParallelGCThreads
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseG1GC
-XX:MaxGCPauseMillis
调优思路
确定是否有频繁Full GC现象
1.2 如果Full GC并不频繁,各个区域内存占用也很正常,那么考虑线程阻塞,死锁,死循环等情况
1.1 如果Full GC频繁,那么考虑内存泄漏的情况
内存泄露角度
1.使用jps -l命令获取虚拟机的LVMID
2.使用jstat -gc lvmid命令获取虚拟机的执行状态,判断full GC次数
3.使用jmap -histo:live 分析当前堆中存活对象数量
4.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件
jmap -dump:format=b,file=/usr/local/base/02.hprof 12942
5.使用MAT等工具分析dump文件,一般使用的参数是Histogram或者Dominator Tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点
1.2 如果Full GC并不频繁,各个区域内存占用也很正常,那么考虑线程阻塞,死锁,死循环等情况
线程角度
1.使用jps -l命令获取虚拟机的LVMID
2.使用 jstack 分析各个线程的堆栈内存使用情况,如果说系统慢,那么要特别关注Blocked,Waiting on condition,如果说系统的cpu耗的高,那么肯定是线程执行有死循环,那么此时要关注下Runable状态。
3.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件
4.使用MAT等工具分析dump文件,一般使用的参数是Histogram或者Dominator Tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点。
1.3 如果都不是,考虑堆外存溢出,或者是外部命令等情况
Runtime.getRuntime.exec()
内存结构
线程共享区域
堆
新生代
Eden区
Survivor(from)区
设置Survivor是为了减少送到老年代的对象
Survivor(to)区
设置两个Survivor区是为了解决碎片化的问题
eden:survivor:survivor8:1:1
老年代
老年代:新生代=2:1
方法区
运行时常量池
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
存储信息
符号引用
符号引用包含的常量
类符号引用
方法符号引用
字段符号引用
概念解释
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
字面量
文本字符串
String a = "abc",这个abc就是字面量
八种基本类型
int a = 1; 这个1就是字面量
声明为final的常量
静态变量
final类型常量
类信息
类的完整有效名
返回值类型
修饰符(public,private...)
变量名
方法名
方法代码
这个类型直接父类的完整有效名(除非这个类型是interface或是 java.lang.Object,两种情况下都没有父类)
类的直接接口的一个有序列表
线程私有区域
虚拟机栈
栈帧
动态链接
符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
操作数栈
保存着Java 虚拟机执行过程中的数据
局部变量表
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
存放的信息
基本数据类型
对象引用
returnAddress类型
方法返回地址
方法被调用的位置
方法退出的过程实际上就等同于把当前栈帧出栈
方法退出可能包含的操作
恢复上层方法的局部变量表和操作数栈
把返回值(如果有的话)压入调用者栈帧的操作数栈中
调整PC计数器的值以指向方法调用指令后面的一条指令
异常
线程请求的栈深度大于虚拟机所允许的深度
StackOverflowError
JVM动态扩展时无法申请到足够的内存时
OutOfMemoryError
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
本地方法栈
和虚拟机栈类似,区别是本地方法栈为使用到的Native方法服务
程序计数器
如果线程正在执行的是一个Java方法,则指明当前线程执行的代字节码行数
此内存区域是唯一一个不会出现OutOfMemoryError情况的区域。
如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)
上述三个区域的生命周期和线程相同
直接内存
使用Native函数库直接分配堆外内存
并不是JVM运行时数据区域的一部分,但是会被频繁使用
避免了在Java 堆和Native 堆中来回复制数据,能够提高效率
垃圾收集
对象是否存活
引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
缺陷:循环引用会导致内存泄漏
可达性分析算法
该算法是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
GC Roots
当前虚拟机栈中局部变量表中的引用的对象
当前本地方法栈中局部变量表中的引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
判断一个对象是否可回收的过程(两步)
1.找到GC Roots不可达的对象,如果没有重写finalize()或者调用过finalize(),则将该对象加入到F-Queue中
2.再次进行标记,如果此时对象还未与GC Roots建立引用关系,则被回收
回收对象引用类型
强引用
垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止
软引用
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
弱引用
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。
ThreadLocal的key是弱引用
虚引用
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动
垃圾收集算法
标记-清除
过程
1.将需要回收的对象标记起来
2.清除对象
缺陷
1.标记和清除的效率都不高
2.会产生大量的不连续的内存碎片
复制算法
复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。
新生代使用的是复制算法
优点
简单高效,不会出现内存碎片问题
缺陷
内存利用率低
存活对象较多时效率明显会降低
标记-整理
原理和标记清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。
老年代使用的是标记-整理算法
缺陷
需要移动大量对象,效率不高
分代回收算法
根据各个年代的特点选取不同的垃圾收集算法
新生代使用复制算法
老年代使用标记-整理或者标记-清除算法
垃圾收集器
Serial收集器
串行单线程收集器
优点:简单高效
是Client 模式下的默认新生代收集器
ParNew收集器
Serial收集器的多线程版本
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
Parallel Scavenge收集器
多线程收集器
“吞吐量优先”收集器,更加关注系统的吞吐量
适合在后台运算而不需要太多交互的任务。
Serial Old收集器
Serial 收集器的老年代版本
可用于Client模式下
用于Server模式下时
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
ParNew Old收集器
Parallel Scavenge 收集器的老年代版本
注重程序的吞吐量
CMS收集器
流程
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW)。
并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。
并发清除:不需要停顿。
缺陷
吞吐量低
低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高
无法处理浮动垃圾,可能出现 Concurrent Mode Failure
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
会产生空间碎片
标记 - 清除算法会导致产生不连续的空间碎片
G1收集器
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
流程
初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。
筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
空间整合,不会产生内存碎片
可预测的停顿
参数设置
-XX:+UseG1GC
-XX:MaxGCPauseMillis
类加载机制
类生命周期
加载
验证
准备
解析
初始化
使用
卸载
类加载器
启动类加载器
C++ 实现,是虚拟机自身的一部分;
负责将存放在 <JRE_HOME>\lib 目录中的类库加载到虚拟机内存中
其他加载器
由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
分类
启动类加载器
扩展类加载器
它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中
应用程序类加载器
它负责加载用户类路径(ClassPath)上所指定的类库
自定义类加载器
用户根据需求自己定义的。也需要继承自ClassLoader.
双亲委派模型
内容
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
实现
首先检查类是否被加载;
若未加载,则调用父类加载器的loadClass方法;
若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当前类加载器调用findClass加载类;
若父类加载器可以加载,则直接返回Class对象;
好处
保证java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题。
破坏
基础类需要调用用户的代码
解决方式
线程上下文类加载器
也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原
实现方法
重写ClassLoader类的loadClass()
示例:
JDBC
原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类
JNDI服务需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码
重写loadClass()方法
双亲委派模型的具体实现就在loadClass()方法中
用户对程序的动态性的追求
例如OSGi(面向Java的动态模型系统)的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
代码热替换、模块热部署
典型的打破双亲委派模型的框架和中间件有tomcat与osgi
模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
类加载过程
加载
将编译后的.Class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到
验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
解析
将class文件的常量池的符号引用替换为直接引用的过程(是静态链接)。
可能发生在始化阶段之前,也可能发生在初始化阶段之后,后者是为了支持 Java 的动态绑定。
初始化
为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序(执行<cinit>()方法)。
类加载方式
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
类加载时机
遇到new,getStatic,putStatic,invokeStatic这四条指令
new一个对象时
调用一个类的静态方法
直接操作一个类的static 属性
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
class文件结构
魔数
唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
版本号
常量池
字面量
符号引用
访问标志
用于识别一些类或接口层次的访问信息
是否final
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
类索引,父类索引,接口索引集合
这三项数据主要用于确定这个类的继承关系。
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类父类的全限定名
接口索引
描述这个类实现了哪些接口
字段表集合
表结构
访问标志
名称索引
描述符索引
属性表集合
字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
简单来说,字段表集合存储字段的修饰符+名称
变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
方法表集合
访问标志
名称索引
描述符索引
属性表集合
Java代码经过编译器编译为字节码之后,存储在方法属性表集合中一个名叫“Code”的属性中
属性表集合
在 Class 文件、字段表、方法表都可以携带子集的属性表集合,以用于描述某些场景专有的信息。
0 条评论
下一页