JVM
2024-10-29 14:55:02 0 举报
AI智能生成
面向 JVM 面试
作者其他创作
大纲/内容
JVM 内存结构
线程共享
堆
堆内存空间最大,几乎所有的对象实例以数组都在此分配空间,也是垃圾回收的主要区域
方法区
方法区是 JVM 运行时数据区域的一块逻辑区域,当虚拟机要使用一个类的时候,它需要读取并解析 Class 文件获取相关信息,将信息存入到方法区中。方法区会存储已经被虚拟机加载的类信息、字段信息、方法信息等。
永久代,JDK 1.8以前
元空间,JDK 1.8 及以后
为什么用元空间替代永久代
永久代的内存空间受 JVM 内存的限制,而元空间是使用本地内存存储,它受本地内存的限制,相比于永久代,是元空间出现内存溢出的几率就会降低
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
线程私有
栈(Java 虚拟机栈)
除了一些 Native 方法调用通过调用本地方法栈来实现,其他的所有 Java 方法都是通过栈来实现的
本地方法栈
程序计数器
占有很小的一块内存空间,每个线程都有自己的程序计数器,标记当前程序执行到什么位置,为线程切换后能恢复到正确的位置做保障
垃圾回收
垃圾回收机制
引用计数
会记录一个对象被引用的数量,如果被以引用数为 0 ,就会成为垃圾对象
缺点:如果有对象 A,和对象 B,两个对象相互引用,就会造成及时成为垃圾也不会被回收的局面
可达性分析
为了解决引用计数出现的问题,提出了可达性分析,在 JVM 中,会将一些特殊引用标记为 GCRoot,如果通过 GCRoot 可以访达的对象,不会被当做垃圾,如果不能访达,则会当做垃圾
什么类型的引用可以看做 GCRoot
栈中的局部变量
堆中的静态变量和常量
本地方法栈 JNI 的引用对象
垃圾回收算法
标记清除法
获取所有的 GCRoot 遍历所有的对象,如果被 GCRoot 就加个标记,剩下的被当做垃圾直接清除
优点
实现简单,执行效率高
缺点
容易产生内存锁片,如果需要申请连续的大空间可能会频繁的 GC
标记压缩法
获取所有的 GCRoot,遍历所有的对象,将可用的对象压缩到另一端,再将垃圾清除
优点
不会产生内存锁片,也不需要复制算法中的内存分块
缺点
需要将对象进行移动,效率低
复制算法
将内存分为两块,每次用其中的一块,首先遍历所有对象,将可用的对象复制到另一个内存块中,清除原来内存块中的所有对象
优点
不会产生内存碎片
缺点
需要一块空的内存空间
分代回收策略
将堆中内存划分为新生代,老年代。新生代又划分为 Eden,Survivor to,Survivor from(8:1:1)
当一个对象刚被创建的时候会分配到 Eden 区,当 Eden 区即将存满进行一次垃圾回收,采用复制算法,将整个新生区可用的对象复制到空的 Survivor 区,再将另外两个区域清除,当 Survivor 区域的对象经过 15 次 GC 后,如果还存活,将该对象放入老年代
当老年代区域即将被存满,进行一个 Full GC。老年代存放的对象多、垃圾对象少,采用标记压缩算法时移动少、也不会产生内存碎片。所以老年代一般采用标记压缩算法
垃圾回收器
ParNew + CMS 垃圾回收器
ParNew(新生代)
使用复制算法进行垃圾清除,将新生代按照 8:1:1 进行划分。
原因:新生代中存在大量的垃圾对象需要被回收,只能存活少量对象,所以只需要复制少量对象的代价就能完成对新生代对象的收集,而且不会产生内存碎片
CMS(老年代)
使用标记清除算法或者标记压缩算法
原因:老年代存活对象数量比较多,如果采用复制算法性能会很差,需要进行很多次的复制操作
JVM 参数默认是,标记清除(5次之后才会去整理内存空间),有可能会产生内存碎片;大对象有可能直接进入老年代,导致 FullGC,一般会设置为 0,(标记清除+整理),这样就不会产生内存碎片
为了减少 STW 时间,CMS 垃圾回收器采用三色标记法,实现垃圾回收线程和用户线程的并发线程
初始标记
标记出被 GCRoot 直接引用的对象,这个阶段需要 STW
并发标记
这个阶段垃圾回收线程和用户线程并发执行,对初始标记阶段标记的对象进行这个引用链的扫描
重新标记
对并发标记阶段出现的问题进行校正,该阶段需要 STW
并发清理
将标记为垃圾的对象进行清除,这个阶段用户线程和垃圾回收线程并发执行
优点
并发收集、低停顿
缺点
对 CPU 资源非常敏感,在 CPU 资源紧缺的时候,CMS 性能大打折扣,当 CPU 数量较少的时候,垃圾回收线程占用的比例相对较大,系统的吞吐量降低
G1 垃圾回收器
为了代替 CMS ,实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。与 CMS 垃圾回收器相比,G1 在停顿时间上添加了预测机制,用户可以指定期望的停顿时间;将内存分为等大的小内存区域(Region)
两种 GC 模式
Young GC
新创建的对象首先会被放置在 Eden 区,G1 垃圾回收器通过监控年轻代的使用情况,当判断年轻代区域已满(超过 60% 容量)时,触发 Young GC
在执行 Young GC 时,G1 首先会精确标记处 Eden 和 Survivor 区的存活对象
根据预设的最大暂停时间和其他配置参数,G1 选择某些区域,将存活对象复制到一个新的 Survivor 区,并清空这些区域
在执行 Young GC 的过程中,G1 垃圾回收器会记录每次回收时每个 Eden 区和 Survivor 区的详细耗时数据。为下次回收提供参考,帮助 G1 垃圾回收器更精确的计算出在给定的最大暂停时间内可以回收 Region 数量
Mixed GC
当总堆占有率达到预设值,触发 Mixed GC,这种回收会处理所有的年轻代和部分老年代,对于老年代的清理策略是选择存活度最低的区域进行回收,同样使用复制算法
三色标记法导致的漏标
有至少一个黑色对象引用了白色对象
灰色对象在在自己引用被扫描之前删除了对白色对象的引用
解决办法
CMS:增量更新
在这个黑色对象增加了对白色对象引用之后,将这个引用记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描
G1:原始快照
在灰色对象取消对这个白色对象引用之前,将这个引用记录下来,在最后标记的时候,再以这个白色对象为根,对它的引用进行重新扫描
缺点:如果这个白色对象没有被黑色对象引用的话,就导致原本应该回收的垃圾没有被回收,产生了浮动垃圾,相比于增量更新,这种方法只是让垃圾在内存中多活了一会,下次回收再把它回收,但是速度比增量更新快
整体流程
Java 文件
编译成为 class 文件
类加载器把 class 文件加载到 JVM 中执行
类加载过程
加载
加载是一个读取 Class 文件,将其转化为某种静态数据结构存储在方法区中,在堆中生成一个 java.lang.Class 类型的对象,由类加载器完成
连接
验证
确保 Class 文件的字节流中包含的信息符合 《Javav 虚拟机规范》的全部约束
文件格式验证
它发生的时期其实是在加载之前,只有通过了文件格式验证才会进行加载操作
元数据验证和字节码验证
在 class 文件被顺利加载之后,紧接着会进行元数据验证个字节码验证,这是进行连接的第一步,是进一步对 class 文件进行验证的一步
符号引用验证
确保解析阶段能正常进行,符号引用验证发生在解析阶段,解析阶段既可以发生在初始化之前,又可以发生在初始化之后
准备
为类中静态变量分配内存空间以及设置初始值(赋 0 值)
解析
将符号引用转换为直接引用
初始化
类加载的最后一步,这时候 JVM 开始真正的执行类中定义的 Java 程序代码,执行成员变量的赋值动作、静态变量的赋值动作以及静态代码块的逻辑
类加载器
BootStrapClassLoader(启动类加载器)
对顶层的加载类,负责加载 JDK 内部的核心类库
ExtensionClassLoader(扩展类加载器)
负责加载 lib/ext 下的 jar 包
SystemClassLoader(系统类加载器)
面向用户的类加载器,负责加载当前 ClassPath 下的所有 jar 包和类
双亲委派机制
如果一个类收到了类加载请求,检查这个类有没有被加载过,如果没有,它并不会自己先去加载,而是把这个请求交给父类加载器去执行
父类加载器再检查有没有加载过这个类,如果没有,进一步向上委托,请求最终会到达启动类加载器
如果父类加载器可以完成类的加载任务,就返回成功,如果不能,就交给子加载器去尝试加载这个类
优点
避免类的重复加载
保护程序安全,防止核心 API 被随意篡改
缺点
在某些场景下双亲委派机制过于局限,需要打破双亲委派机制来达到目的
打破双亲委派机制
Tomcat
Tomcat 采用默认的双亲委派加载机制,是无法加载同一类库不同版本的类的,因为默认的双亲委派加载机制在加载类时,是通过类的全限定类型做唯一校验的
Tomcat 怎么做的?
有 WebappClassLoader,各个 Webapp 私有的类加载器,其加载的类只对当前的 Webapp 可见,这样我们就能解决对于存在多个 Webapp,但是他们无法使用同一个类的不同版本,实现了隔离性
当 Jsp 文件更新之后,也就是 class 文件更新了,此时类的全限定名并没有改变,修改 Jsp 文件后,类加载器会从方法区中直接取到已存在的,这导致修改后 Jsp 文件不会重新加载
Tomcat 怎么做?
设计了 JasperLoader ,这个加载器的加载范围仅仅只是这个 JSP 文件所编译出来的 .class 文件,一对一的设计是为了随时丢弃它,当 Tomcat 检测到 JSP 文件被修改时,会替换掉当前的 JasperLoader 的实例,并通过再一次建立一个新的 JasperLoader 实例来实现 JSP 文件的热加载功能
JDBC
双亲委派机制是,每个 ClassLoader 都只能加载自己目录下的资源,如果一个类由类加载器 A 加载,那么这个类的依赖类也应该由相同的类加载器加载
但是在 JDBC 中,Driver.class 在 rt.jar 中应该被 BootstrapClassLoader 加载,但是其 SPI 机制的实现类却是由 System ClassLoader 加载,双亲委派机制无法解决这个问题
怎么办?
使用线程上下文加载器,线程上下文加载器默认会使用 AppClassLoader
Tomcat 的设计是违背双亲委派机制的,每个 WebappClassLoader 加载自己目录下的 .class 文件,不会传递给父加载器,这就打破了双亲委派机制,这样做正是为了实现隔离性
0 条评论
下一页