说一说什么是JVM
JVM,即 Java Virtual Machine,Java 虚拟机。它通过模拟一个计算机来达到一个计算机所具有的的计算功能。JVM 能够跨计算机体系结构来执行 Java 字节码,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。
说说JVM的基本结构是什么样子
jvm的基本结构主要分为3类:
运行时数据区都由什么组成,具体到每个区存放什么
运行时数据区分为线程私有和共享数据区两大类
线程私有:程序计数器、虚拟机栈和本地方法栈
共享数据区:Java堆,方法区(java8 元空间)
程序计数器:记录当前线程指定指令的位置
虚拟机栈:栈帧构成,每调用一个方法就压入一个栈帧,栈帧中包含操作数栈,局部变量表,动态链接和方法出口,其中局部变量表存放的类型是8种基本类型和一个引用类型
本地方法栈:具有和虚拟机栈类似的特点和功能,它服务的对象是Native方法
堆:存放所有的对象实例和数组
方法区:虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
hotspot 虚拟机的方法区存放了什么,1.7之前和1.8之后有什么区别
对于常用的hotspot虚拟机,方法区分为1.7和1.8版本:1.7及之前,方法区也称为永久代。存放类信息、常量、静态变量、即时编译器编译后的代码,1.8之后,使用元空间实现方法区,永久代被废弃,元空间存放在本地内存中。类信息存元空间中,常量池和静态变量放到了Java堆里
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
为什么jdk1.8要把方法区从JVM里(永久代)移到直接内存(元空间)
原因一:
从数据流的角度,非直接内存:本地IO --> 直接内存 --> 非直接内存 --> 直接内存 --> 本地IO
而直接内存是:本地IO --> 直接内存 --> 本地IO
原因二:
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。
堆的分区是什么样子的,各自有什么特点
JVM线程共享区域可分为3个区域:新生代,老年代,和永久代。其中JVM堆分为新生代和老年代
新生代:
Eden空间、From Survivor空间、To Survivor空间
新对象分配内存的地方,发生minor gc会清除eden区和survival区的,把存活的对象移到另
一个Survival区
(理解记忆 Eden:伊甸园 Survivor:存活)
老年代:
对象创建在新生代,经过很多次回收还依然存活的,会进入老年代。
为何新生代要在eden区外设置两个survivor区
survivor区域是为了方便实现复制算法:将原有的内存空间划分成两块,每次只使用其中一块,在垃圾回收的时候,将正在使用的内存中的存活对象复制到另一块内存区域中,然后清除正使用过的内存区域,交换两个区域的角色,完成垃圾回收。
复制算法,为什么要在新生代中使用复制算法:
因为新生代gc比较频繁、对象存活率低,用复制算法在回收时的效率会更高,也不会产生内存碎片。但复制算法的代价就是要将内存折半,为了不浪费过多的内存,就划分了两块相同大小的内存区域survivor from和survivor to。在每次gc后就会把存活对象给复制到另一个survivor上,然后清空Eden和刚使用过的survivor。
对象访问定位有哪些方法
如何判定对象是否存活
引用计数法
实例对象中存在计数器,如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就会被回收。JVM没有使用它的原因是无法解决循环引用问题
可达性分析(JVM所选)
从GC Roots起始向下搜索,不可达对象被回收
GC Roots对象:
什么时候一个对象会被GC
一个对象真正的死亡需要经历两次标记过程,
safepoint 是什么,如何选定安全点
HotSpot 通过GC Roots枚举判定待回收的对象。
找到对象哪些是GC Roots。有两种方法:
一种是遍历方法区和栈区查找(保守式 GC)。
一种是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC)。
保守式GC 的成本太高。因此在HotSpot中,使用OopMap的结构来标记对象引用的位置。OopMap 记录了栈中变量到堆上对象的引用关系,通过OopMap,HotSpot可以快速准确地定位到GC Roots,进行GC。
在执行 GC 操作时需要STW(stop the world,所有的工作线程必须停顿)
安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。
安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。
一般会在如下几个位置选择安全点:
1、循环的末尾
2、方法临返回前
3、调用方法之后
4、抛异常的位置
为什么选定这些位置作为安全点:
避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长
如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?
主要有两种方式:
抢断式中断:在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。
主动式中断:在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。
JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。
聊一聊GC机制
主要是三个问题:
1.在内存上存在着无数对象,之前需要准确将这些对象标记出来,分为存活对象与垃圾对象。标记方法是可达性分析,前面已经说过。
2.发生在运行时数据区中。其中,随线程消亡,线程独享内存(栈,程序计数器和本地方栈)被回收。
3.目前主流 GC 算法主要分为三种:
标记-清除算法
先通过 GC Roots
标记出可达对象,再清理未标记对象。
缺点:内存碎片,效率不高
复制算法
用完一块内存,将对象复制到另外一块上
缺点:空间换时间,牺牲一部分内存
标记-整理算法
通过 GC Roots
标记存活对象
将存活对象往一端移动,按照内存地址一次排序,然后将末端边界之外内存直接清理。
效率低,甚至不如标记清除
图例:
标记-清除
复制算法:
标记-整理
JVM中怎么使用的这些算法
从上面三种 GC
算法可以看到,并没有一种空间与时间效率都是比较完美的算法,所以采用分代方式使用这些算法
JVM根据对象存活周期划分新生代,老年代。新对象一般情况都会优先分配在新生代,新生代对象若存活时间大于一定阈值之后,将会移到至老年代。
新生代每次 GC
之后都可以回收大批量对象,所以比较适合复制算法。这里内存划分并没有按照 1:1 划分,默认将会按照 8:1:1 划分成 Eden
与两块 Survivor
空间。每次将 Eden
与一块Survivor
共同存储对象,GC时存活对象都复制到另一块空闲的Survivor
区,然后这两块Survivor
功能互换,以此类推。当Survivor
空间并不能保存剩余存活对象,就将这些对象通过分配担保进制移动至老年代。
老年代中对象存活率将会特别高,且没有额外空间进行分配担保,所以需要使用标记-清除或标记-整理算法。
什么时候对象会进入老年代
大对象直接进入老年代
一般来说大对象指的是很长的字符串及数组,或者静态对象。
这个虚拟机提供了一个参数-XX:PretenureSizeThreshold=n
,只需要大于这个参数所设置的值,就可以直接进入到老年代。
长期存活的对象将进去老年代
对象熬过以此Minor GC就增长一岁,默认阈值15岁进入老年代
动态年龄判断
Survivor空间相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代
什么是空间分配担保策略
Minor GC之前,先检查老年代最大可用连续空间是否大于新生代所有对象总空间,再检查空间是否大于历次晋升到老年代对象的平均大小,大于则Minor GC,大于则Full GC
介绍下JVM的垃圾收集器
上面的表示是年轻代的垃圾回收器:Serial、ParNew、Parallel Scavenge,下面表示是老年代的垃圾回收器:CMS、Parallel Old、Serial Old,以及不分老年代和年轻代的G1。连线表示可以相互配合使用。
停顿时间:GC中断执行的时间 吞吐量:执行时间(排除GC时间)占总时间的占比 1- 1/(1+n)
CMS和G1是重点,单独分析
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
说一下 CMS 垃圾回收器
CMS(Concurrent Mark Sweep)收集器 目标:最短回收停顿时间,“标记-清除”实现,应用场景广泛,比较主流
工作流程
优缺点:
优:并发收集,停顿时间短
缺:
CMS 缺点解决办法
垃圾碎片的问题:
设置参数:-XX:CMSFullGCsBeforeCompaction=n
上一次CMS并发GC执行过后,还要再执行多少次full GC
才会做压缩。默认0,即每次CMS GC顶不住了而要转入full GC的时候都会做压缩。
concurrent mode failure问题
设置参数-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60:是指设定CMS在对内存占用率达到60%的时候开始GC
由于CMS GC过程中需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。
promotion failed问题
让CMS在进行一定次数的Full GC时候进行一次标记整理算法,CMS提供了以下参数来控制:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
即CMS在进行5次Full GC之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
介绍一下G1 收集器
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8 元空间Metaspace),这种特点是各代的逻辑存储地址是连续的。而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。
有一些Region标明了H,代表Humongous,表示这些Region存储的是巨大对象(humongous object,H-obj),即>=region一半存储的对象。
H-obj特征:
为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。
GC过程
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。
详细过程参考
G1比CMS好在哪儿
描述一下 JVM 加载 Class 文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,功能是把class文件从硬盘读取到内存中。除了反射等显式加载类以外,几乎不需要关心类的加载,这些都是隐式装载的
Java类的加载是动态的,保证程序运行的基础类(像是基类)完全加载到jvm中,其他类则在需要的时候才加载。节省内存开销。
说一下JVM类加载的过程
类加载过程:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
整个过程:通过全限定名来加载生成class对象到内存中,然后进行验证这个class文件,包括文件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码
JVM中有哪些类加载器
JVM 预定义的类加载器
JVM 中内置了三个重要的 ClassLoader,除BootstrapClassLoader 外,其它均由 Java 实现且全部继承自java.lang.ClassLoader
:
%JAVA_HOME%/lib
目录下的jar包和类%JRE_HOME%/lib/ext
目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的jar包。除此之外,还可以用户自定义类加载器
说一说双亲委派模型
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。首先它会把这个类请求委派给父类加载器去完成,一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。(这里的双亲其实就指的是父类,没有mother。父类也不是指继承关系,只是调用逻辑是这样)当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
双亲委派模型不是一种强制性约束,是一种JAVA设计者推荐使用类加载器的方式。
双亲委派模型有什么好处
(1)安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
有没有破坏双亲委派模型的方式
某些情况下,需要由子类加载器去加载class文件,这时就需要破坏双亲委派模型。避免双亲委托机制,可以自定义一个类加载器,然后重写 loadClass()
即可。
经典如Tomcat,t造了一堆自己的classloader,**每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。**目的: