八股文骚套路之JVM
运行时数据区中包含哪些区域?哪些线程共享?哪些线程独享?【⭐⭐⭐⭐⭐】
JDK1.7之前的运行时数据区包括 堆、方法区、虚拟机栈、本地方法栈、程序计数器
其中堆和方法区是线程共享的;虚拟机栈、本地方法栈和程序计数器是线程私有的。
Jdk1.8之后 方法区的实现变成了元空间,和运行时常量池一起被放到了本地内存
程序计数器
可以看作当前线程所执行的字节码文件的行号指示器,实现代码的流程控制,还需要能让线程切换之后能恢复到正确的执行位置,自然是线程私有的
虚拟机栈
除了一些本地方法是在本地方法栈中实现,其他所有java方法的调用都通过虚拟机栈实现的。
栈由一个个栈帧组成,每一个方法调用的时候都会有一个栈帧被压入虚拟机栈中,每个栈帧中都保存着局部变量表、操作数栈、动态链接和方法的返回地址。
局部变量表
主要存储编译期间的基本数据类型和对象引用(是指向对象起始地址的引用指针)
操作数栈
放方法执行过程中产生的中间计算结果。
动态链接
主要用于一个方法需要调用其余方法的场景,当一个方法要去调用其他方法,就需要把常量池里指向方法的符号引用转化为内存地址中的直接引用。
本地方法栈
和虚拟机栈类似,只是里面存的是Native方法
堆
用来存放对象实例,几乎所有的对象实例和数组都在这里分配内存
说一下方法区和永久代的关系
方法区是JVM运行时的数据区域的一块逻辑区域,是各个线程共享的内存区域,它是一个逻辑概念,定义规则。
而永久代则是方法区的一个具体实现方式,Jdk1.8之后方法区的实现变成了元空间,被放到了本地内存里
Java 创建一个对象的过程。【⭐⭐⭐⭐】
类加载检查:首先,再Java中使用new关键字创建对象的时候,首先加载对象的类。如果类未被加载,那么JVM会使用类加载器(这里有一个双亲委派机制)加载并初始化该类
分配内存空间:类加载完成之后,JVM会在堆中为对象分配内存空间。分配内存的方式有指针碰撞和空闲列表两种,选择哪种分配方式取决于Java堆内存是否规整。
连接:JVM会把对象里的普通成员变量初始化为0值。这一步操作主要是保证对象里面的实例字段不用初始化就可以直接使用。
设置对象头:为对象添加首部属性信息,比如对象所属的类元信息、对象的GC分代年龄、hashCode、锁标记等。
执行
方法:调用对象的构造函数,对对象的属性进行赋值 和 其他初始化操作 最后,JVM会返回对象的引用
对象的访问定位的两种方式【⭐⭐⭐⭐⭐】
一般有两种方式:
- 句柄(堆中划分出一个句柄池,栈中的引用指向句柄地址,然后句柄中包含了对象的实例数据和类型数据的地址)
- 直接指针(栈中的引用直接指向实例数据的地址,如果访问对象本身的话,就不用多一次访问开销,而对象的类型数据的指针存放在方法区中,如果定位的话,需要多一次直接定位开销)
使用句柄最大的好处就是引用中存储的是句柄地址,对象移动时只需改变句柄的地址就可以,而无需改变对象本身。
使用直接指针来访问速度更快,它节省了一次指针定位的时间开销,因为对象访问在 Java 中非常频繁。
你了解分代理论吗?讲一下 Minor GC、还有 Full GC ⭐⭐⭐⭐⭐ / 讲一下java的垃圾回收机制?
垃圾回收机制是Java语言自动内存管理的一个特性,不需要像C++那样手动释放对象占用的内存空间。
在Jdk1.7之前 堆内存通常分为 新生代 老年代 和永久代,Jdk8之后永久代被元空间取代,元空间使用的是直接内存
大多数情况下,对象优先在新生代中的eden区中分配(大对象直接进入老年代),当eden区空间不足的时候,JVM会进行一次minorGC,经过一次minorGC的对象会被分配到survivor区,如果survivor区放不下,那么会通过分配担保机制提前移动到老年代去,如果仍能存活且能被survivor容纳的话,会被移动到survivor空间。且年龄+1
当年龄增加到阈值的时候,就会晋升到老年代中。
minorGC是partialGC 的一种,只对新生代进行垃圾收集
Java 用什么方法确定哪些对象该被清理/如何判断一个对象已经死亡? 讲一下可达性分析算法的流程。【⭐⭐⭐⭐】
简单来说就是当对象不再被引用的时候宣判对象已经死亡
- 引用计数法
给对象添加一个引用计数器,每当有一个地方引用,计数器就+1;当引用失效,计数器就-1;引用计数器为0的对象就是不可能再被使用的
但是这个方法没法解决对象间的循环引用问题
- 可达性分析算法
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
JDK 中有几种引用类型?分别的特点是什么?【⭐⭐】
强引用
表示必不可少。垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。软引用(SoftReference)
表示可有可无。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。弱引用(WeakReference)
表示可有可无。弱引用与软引用的区别在于:弱引用的生命周期更短,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。虚引用
表示形同虚设,任何时候都可能被垃圾回收
如何回收方法区?【⭐⭐⭐】
主要有两种方法:
- 类卸载:当一个类不再被引用,即没有任何对象实例引用该类,且该类的类加载器已经被回收时,JVM 可以卸载这个类。类卸载会导致该类在方法区中所占用的内存被释放,从而实现方法区的回收。
- 常量池回收:在方法区中的运行时常量池中存储着类信息、常量、静态变量等数据。常量池的大小是有限的,当常量池用尽时,JVM 会对常量池进行回收。常量池回收通常是通过 Full GC(Full Garbage Collection)来实现的。
标记清除、标记复制、标记整理分别是怎样清理垃圾的?各有什么优缺点?【⭐⭐⭐⭐⭐】
标记清除
首先标记出所有不需要回收的对象,标记完之后回收所有没有标记的对象
优点是简单,缺点是效率不高,容易产生大量不连续的内存碎片标记复制
将内存分为大小相同的两块,每次使用其中的一块,当内存使用完之后,把还存活的对象复制到另外一块,然后再把前面的空间一次性清理掉
优点是解决了内存碎片问题
缺点是可用内存变小,而且如果存活的对象表较大,复制的效率也会很低所以不适合老年代标记整理算法
标记所有不需要回收的对象,然后让所有存活的对象向一端移动,然后直接清理掉端边界外的内存
优点是减少了内存碎片,缺点是多了整理的这一步,效率不高分代收集算法
根据新生代和老年代的特点,选择上述三个中适合的垃圾收集算法
[!TIP]
标记都是标记出不需要被回收的对象
JVM 中的安全点和安全区各代表什么?写屏障你了解吗?【⭐⭐⭐⭐】
在执行GC的时候,所有的工作线程都必须停顿,安全点就代表在这个时间节点,所有线程的工作状态是确定的,JVM可以安全的执行GC
安全点是对正在执行的线程设定的。如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
安全区是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
TODO:并发标记要解决什么问题?并发标记带来了什么问题?如何解决并发扫描时对象消失问题?⭐⭐⭐⭐
新生代垃圾收集器有哪些?老年代垃圾收集器有哪些?哪些是单线程垃圾收集器,哪些是多线程垃圾收集器?各有什么特点?各基于哪一种垃圾收集算法?【⭐⭐⭐⭐】
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
Serial 串行收集器是单线程的垃圾收集器,新生代采用标记复制算法 老年代采用标记整理算法 缺点是执行GC的时候需要stop the world 优点是简单高效
ParNew 收集器是Serial收集器的多线程版本 新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Scavenge 收集器也是用标记-复制算法的垃多线程收集器,它更加关注如何提高吞吐量 新生代采用标记-复制算法,老年代采用标记-整理算法。
Serial Old 收集器,是serial收集器的老年版本,是单线程的 与 Parallel Scavenge 收集器搭配使用
Parallel Old 收集器 是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记整理”算法,在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep 并发标记清除) 并发收集器,可以让用户进程和垃圾回收同时进行 ,基于标记清除算法
有4个步骤:
- 初始标记:暂停所有其他线程,并记录下与root相连的对象
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象,用这个算法去跟踪记录用户进的执行导致可达对象更新的地方
- 重新标记:把并发标记期间用户程序继续运行导致的标记变动的那一部分对象进行标记和记录
- 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点:并发收集,停顿少,用户体验好
缺点:对CPU的压力比较大;无法处理浮动垃圾;基于标记清楚算法会产生大量的内存碎片
- G1 (Garbage-First) 面向服务器的垃圾收集器,内存得大,cpu性能牛逼 也是默认的垃圾收集器,G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
特点: - 充分利用并行和并发,既可以使用多个cpu来缩短stop the world 的时间,又可以通过并发使得Java程序在gc期间继续执行
- 分代收集
- 空间整合 整体上是基于标记整理算法,但是局部上基于标记复制算法
- 可以预测停顿的时间
讲一下内存分配策略?【⭐⭐⭐⭐】
常见的内存分配策略有两种
第一种:对象优先在Eden区进行分配,当Eden区满了之后,进行一次minorGC(新生代垃圾回收),仍然存活的对象被移动到survivor区或老年代
第二种:如果对象大小超过了一定的阈值,那么JVM会自动将其分配到老年代,因为大对象往往拥有较长的生命周期,直接分配到老年代可以减少在新生代的复制操作
第三种:长期存活的对象进入老年代,JVM给每个对象一个年龄计数器,在eden区的对象经过一次minorGC之后如果仍然存活会被移动到survivor区,且年龄+1;后续在survivor区每经历一次minorGC,年龄就+1,直到达到阈值默认为15,就会被移动到老年代。
第四种:动态对象年龄判定,如果survivor区中相同年龄的对象的总内存超过survivor空间一半,年龄大于等于这个年龄的对象可直接进入老年代。
空间分配担保机制
minorGC之前,需要检查老年代可用空间是否足够容纳新生代所有对象。如果够的话说明minorGC是安全的。
什么是字节码?类文件结构的组成了解吗?【⭐⭐⭐⭐】
JVM可以理解的代码称为字节码
类文件结构有魔数、class文件版本号、常量池、访问标识、当前类、父类、字段表、方法表、属性表
类的生命周期?类加载的过程了解么?加载这一步主要做了什么事情?初始化阶段中哪几种情况必须对类初始化?【⭐⭐⭐⭐⭐】
类从被加载到JVM内存开始到卸载出内存,生命周期主要有7个阶段:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备和解析三个阶段统称为连接
系统加载类文件主要有三步:加载 连接和初始化
加载这一步首先是通过全类名获取定义此类的二进制字节流;其次将字节流代表的静态存储结构转化为方法区的运行时数据结构;然后在内存中生成一个代表该类的class对象作为数据的访问入口
初始化阶段有6种情况必须对类进行初始化
- 当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时
- 对类进行反射调用时
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,虚拟机需要定义一个主类,这个主类需要被初始化
- MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle 来初始化要调用的类。
- 当一个接口中定义了被default关键字修饰的接口方法的时候,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
讲一下双亲委派模型。【⭐⭐⭐⭐⭐】
类加载器有很多种:启动类加载器,扩展类加载器、应用程序类加载器等等
双亲委派模型就是用来确定具体用哪一个类加载器加载类。
- 每当一个类加载器接到加载请求的时候,先判断类是不是被加载过,如果加载过就直接返回,否则才会尝试加载;
- 进行类加载的时候,会先把这个请求委派给父类的加载器,这样依次类推,所有的请求都会最终传送到顶层的启动类加载器;只有当父类加载器没有找到所需要的类的时候,子加载器才会自己尝试加载;(全力倚父)
3.如果最终子类加载器也无法加载这个类,会抛出classnotfoundexception异常
双亲委派模型的好处是可以避免类的重复加载,保证Java的核心API不被修改