likes
comments
collection
share

口语化讲解JVM

作者站长头像
站长
· 阅读数 25

前言

本文对JVM相关知识做了一个相对完整的总结,涉及到JVM内存结构、JMM、类、TLAB等。特别注意,口语化八股文系列,仅作突击复习核心知识点用,推荐有一定八股基础的人食用,更细的点需要大家自行查询相关详细图文资料

正文

JVM内存结构简单描述一下(线程私有和共享、逻辑划分区域)

JVM内存结构简单描述一下(线程私有和共享、逻辑划分区域)和Java内存模型(JMM)各是什么。JVM内存结构用于描述运行时数据结构,主要区域划分为堆和栈,堆用来管理数据的存储、栈用来管理方法的运行。具体根据线程共享分为两类,线程私有的程序计数器、虚拟机栈、本地方法栈,线程共享的有堆、方法区。

JMM是一套Java用于多线程操作内存的规范,旨在屏蔽底层细节,在任何平台上操作内存都有相同的效果。JMM很复杂,涉及到很多方面,简单提两个。一是抽象了线程和内存的关系,划分出了主内存和线程本地内存,通过控制两种内存的交互来实现可见性。二是规定了从Java源代码到CPU可执行指令的这个转换过程需要遵守哪些原则或者规范,比如happen-before原则。三是定义了一些原语用于保证final,volatile,current包等底层的实现。

程序计数器(作用、特别之处)

主要两个作用,一是通过控制程序计数器来修改指令执行的顺序,从而做到类似for,if之类的流程控制。二是在多线程的情况下用于记录当前线程的执行位置,方便线程切换回来的时候继续执行。

虚拟机栈和本地方法栈(内容、特点、异常)

栈是一种快速高效的数据结构。在Java中每一个方法对应一个栈帧,方法的开始和结束对应着入栈和出栈。栈帧内部存储着各种数据,比如局部变量表、操作数栈、动态链接、方法出口。局部变量表用于存储编译期已知的各种基础数据类型变量和对象引用。操作数栈用于存储方法运行中的中间运算结果。动态链接则运用于方法外部调用中,将符号引用转换成直接引用。

栈的特点有三点,一是快速高效,在Java内存结构中速度仅次于程序计数器。二是操作足够简单,入栈出栈即可。三是不存在垃圾回收问题,随线程的生命周期。

通过参数-Xss可以设定栈的最大空间,其大小限制了方法调用的可达深处。

本地方法栈和虚拟机栈作用相似,只不过特殊服务于native方法。

说一说你了解的堆内存(内存划分)

在JDK1.7之前,方法区的实现永久代存在于堆内存,在1.8后实现改为元空间并迁移到堆外内存。为了提升垃圾收集效率,堆内存一般采用分代划分,即年轻代和老年代,按照Java官方推荐一般按照3:5的大小划分。其中年轻代按照8:1:1细化为伊甸园区、幸存者0区和1区。

年轻代的伊甸园区是大多数新建对象存入的地方,为啥说是大多数呢,因为超过指定大小的对象会直接进入老年代。当伊甸园区满了之后会触发Minor GC,一般使用标记-复制算法进行垃圾收集,也就是将伊甸园中的幸存对象放到幸存者0区。当Minor GC再次触发的时候,会将伊甸园和幸存者0区的幸存对象放到幸存者1区,也就是说每一次GC后,总有一个幸存者区是空的。每经历过一次GC,对象的年龄会+1,当到达默认值15或者JVM动态年龄的时候,这些对象会被转移到老年代。

老年代满了之后同样会触发GC,叫做Major GC,该GC和Minor不同,会造成停顿,也就是stop the world。当老年代不能为新对象分配内存的时候,会报OOM异常。

我们通常会使用-Xms和-Xmx来设置堆内存的最小值和最大值,一般优化会将两者设为相同值,避免GC后重新分配堆内存大小。

有没有了解过对象在堆中的生命周期(分配过程)

  1. 新建对象如果没有超过指定大小则放入伊甸园区,超过则直接放入老年代
  2. 如果伊甸园区已满,则触发Minor GC,清理伊甸园中无用对象
  3. 将新对象放入伊甸园区,再将所有对象放入幸存者0区
  4. 如果再次触发Minor GC,通过标记-复制算法,清理无用对象后将存活对象放入另一个空的幸存者区,也就是始终保证有一个幸存者区是空的
  5. 每个对象都有一个年龄标记,一次Minor GC就会+1。JVM会遍历幸存者区存活对象的年龄大小,当某个年龄大小的对象占比超过半数,就取这个年龄和默认值15的最小值作为新的年龄阈值,超过这个阈值的会送入老年代
  6. 老年代满了之后,会触发Major GC,如果GC之后仍然没有空间分配给新对象,则会报OOM异常

知道TLAB吗,能说出它的作用吗

TLAB全名线程本地分配缓冲区,是JVM提供的一种快速分配策略。具体是在堆中的伊甸园区为每一个线程开辟了一小块独占的内存区域,不超过该区域大小的对象优先在这里分配。因为没有线程的竞争因此分配速度很快,这里需要注意的是读取、和垃圾回收依旧是共享的。

逃逸分析是个很牛的技术,有了解吗,带来了哪三种优化

逃逸分析是一种能有效减少Java程序中同步负载和堆内存分配压力的分析算法。作用是通过分析一个新的对象的引用使用范围,从而决定对象是否需要分配到堆。分析对象是否逃逸,可以判断其在方法中被定义后,是否被外部方法引用,如果有则说明发生逃逸。

通过逃逸分析,衍生出三种优化手段,分别是栈上分配、同步省略(锁消除)、标量替换。

  • 栈上分配的意思是,当一个对象通过逃逸分析发现没有发生逃逸,那么优先考虑不在堆中分配内存而是选择栈上分配
  • 同步省略同理,当被锁住的对象没有发生逃逸时,会将开发人员的锁消除
  • 标量替换也是在对象没有发生逃逸的情况下,会去选择拆分对象(聚合量)替换为基础类型变量(标量),这些变量也会选择在栈上分配

方法区的变迁、存储了什么东西

方法区存储了类的元信息(比如类型信息、字段、方法、常量)、常量池、静态变量、JIT编译后的代码缓存等信息。方法区是一个概念,1.7及以前的永久代和1.8之后的元空间都是它的具体实现

元空间相比永久代的区别有

  1. 永久代都在堆内,元空间迁移到了堆外
  2. 永久代在1.8拆分为两部分,静态变量和常量池并入堆中,类的元信息和代码缓存等则放在堆外的元空间
  3. 静态变量和常量池并入堆中主要是提高GC效率,因为永久代中只会在Full GC时才会参与,效率太低

类的生命周期,各个阶段都是干啥的

类的生命周期分为加载-连接-初始化-使用-卸载,连接可细化为验证、加载、解析。大部分情况下按照顺序执行,解析是例外,可能开始于初始化之后,这是为了支持Java的运行时绑定。

  • 加载阶段是从Class文件或者别的地方加载,生成Class对象。
  • 验证阶段是验证读取的数据中是否符合当前虚拟机的要求
  • 准备阶段要做的是为类变量分配内存以及赋初始值
  • 解析阶段则是将类中的符号引用转化为直接引用,也就是获取类、变量和方法在内存中的指针或者偏移量
  • 初始化阶段则是去执行类的初始化方法也就是构造函数
  • 卸载就是GC将无用对象从内存中清除

类的实例化时机有了解吗,顺道说一下实例化过程

类的实例化时机一般是new或者反射的时候。

类实例化的过程实际上就是调用了类的构造函数,也就是字节码中的init方法。方法执行顺序是父类变量初始化、父类代码块、父类构造方法、子类变量初始化、子类代码块、子类构造方法。

类加载器三种层次和JVM的三种类加载机制讲一下

类加载器共有三种层次,自顶向下分别是启动类加载器、扩展类加载器、应用程序加载器。启动类和扩展类分别指的是Jre包下的基础包和扩展包,应用程序加载器是默认使用的加载器。

JVM提供了三种类加载机制,分别是全盘负责、缓存机制和双亲委派机制。

全盘负责的意思是,当前类加载器加载了某个类,那这个类依赖和引用的类默认都将由该加载器加载。

缓存机制则是缓存所有加载过的类,当程序需要某个类时先从缓存中找,找不到就加载类再放到缓存中。

双亲委派机制的意思是在加载某个类时,会向上寻找最顶端的父类先加载,如果加载不了再向下传递由子类加载。这样加载有一个好处,比如我乱写一个String类想要搞破坏顶掉JDK里的String,因为有双亲委派机制的原因,加载String类会优先从JDK里面找到加载。因此代码中只要不是我显式指定要用我自己的String类,那就是默认用JDK中的String类,不会造成混乱。

一个对象是怎么创建的

对象的创建分为五个步骤,分别是类加载检查、分配内存、初始化零值、设置对象头、执行初始化方法。

类加载检查主要是检查当前需创建对象的类是否有被加载过,没有的话先执行类加载,也就是加载、验证、准备、解析、初始化这套过程。

分配内存简单来说就是在堆中分配内存空间、具体有指针碰撞和空闲列表两种方式。当堆内存空间规整也就是没有内存碎片的时候(比如使用标记-整理、标记-复制算法),选择指针碰撞方式分配内存,反之不规整也就是存在内存碎片(标记-清除)的时候。分配内存的并发问题,通过TLAB进行无同步快速分配,如果TLAB分配失败的话,选择CAS+失败重试放入堆中。

初始化零值是指内存分配完后,为对象字段赋零值

设置对象头就是往对象头中填充必要的元信息,比如对象所属类信息、哈希值、GC分代年龄、锁标识

执行初始化方法就是去执行对象的init方法(调用对应的构造方法)

对象的内存布局和访问定位简单说一下就行

对象内存布局分为对象头、实例数据和对齐填充。对象头内部存有必要的元信息,比如对象所属类信息、哈希值、GC分代年龄、锁标识。实例数据则是对象真正存储的有效信息。对齐填充则是占位用,因为对象大小必须是8字节的倍数。访问定位目前有两种主流方式、使用句柄和直接指针。

如何判断一个对象需要被回收,常见的两种方法

分析对象是否需要被回收,常用的有两种方法,分别是引用计数法以及可达性分析算法。

  • 引用计数法相当简单粗暴,只要该对象被引用就+1,问题也很致命,当两个对象互相持有引用时,引用计数始终不能为0,也就不能回收。
  • 可达性分析则通过判断某个对象是否存在一条引用链到达GC ROOT来判定是否需要被回收。GC ROOT所指对象有以下五类、虚拟机栈引用的对象、本地方法栈引用的对象、类静态属性引用的对象、常量引用的对象、被同步锁住的对象。需要注意的是可达性分析超过两次标记的对象,才会被回收。

三种常用的垃圾回收算法了解吗

三种常见的垃圾回收算法分别是标记-清除、标记-复制、标记-整理。

  • 标记清除简单粗暴,标记了该处有垃圾就直接清除,其他啥也不管。
  • 标记复制是将内存分为两块,一般使用一半内存,另一半空着,清理的时候将存活对象转移到另一半,清除当前这一半空间。
  • 标记整理则是将存活对象向一端移动,然后清除掉边界之外的内存。

JVM通过分代收集的方式来提升垃圾回收效率,新生代由于GC时一般只会存活少量对象,因此常使用标记-复制算法,在少量复制的情况下,标记-复制算法的效率高于另外两种。并且针对标记-复制算法浪费空间的问题,设置了伊甸园和幸存者01区8:1:1的比例,使内存利用率达到90%。老年代则一般使用标记-整理或者标记-清除算法。

有哪些垃圾收集器,JDK8默认使用的是什么

serial串行收集、Parallel New并行新收集、Parallel Scavenge并行清除收集、Serial Old串行老年代收集、Parallel Old并行老年代收集、CMS收集器、G1收集器。JDK8注重吞吐量,也就是为了高效率的使用CPU采用了Parallel Scavenge + Parallel Old的组合。

串行收集器是最原始的垃圾收集器,新生代采用标记-复制算法,老年代采用标记-整理方法。有个很致命的缺点就是因为是单线程,所以进行垃圾收集时会阻塞程序运行,因此后续增加了多线程版本,也就是Parallel New。在Parallel New的基础上Parallel Scavenge优化了自适应调节策略去进行内存管理,Serial Old和Parallel Old是对应的老年代版本。CMS收集器是一种以获取最短回收停顿时间为目标的收集器,内部使用标记-清除算法。G1收集器则是在服务器提供高性能的基础上同时做到了高吞吐量和低延迟。

G1垃圾收集器展开说说(收集过程、三种GC、特点)

G1(Garbage-First(首先收集尽可能多的垃圾))收集器内部仍然采用了分代收集的概念,它将内存逻辑划分为同等大小的region(区域),region可以是未分配空间、新生代、老年代、巨型对象、幸存者区域。由于将大块内存划分为小的region,因此可以充分利用多线程的优势,同时各个区域的大小不再固定会进行动态划分。G1收集器的大致流程是初始标记、并发标记、最终标记、筛选回收。标记时使用和CMS相同的三色标记算法,分为白灰黑三色。白色代表未标记、灰色代表自身被标记,引用对象未被标记、黑色代表自身和引用对象都被标记。GC一开始将GC ROOT可达的对象都压入栈中,涂成灰色,待搜索。然后遍历栈中的灰色对象引用的子对象,同样将其入栈。如此反复,最后阶段当一个对象的所有子对象都是灰色,那么将其涂为黑色,标记为存活对象,其他的都是垃圾。

G1中一共有三种GC,分别是Minor GC、Mixed GC、Full GC。

Minor GC是在伊甸园区满了之后触发,大致分为根扫描、更新处理RSet、复制对象三个步骤。根扫描的意思很明显,就是打标记。更新处理RSet,RSet是每个region都存在的一张记录其他region引用当前region的记录表。该表的作用是为了避免去扫描别的region,提高标记效率,因此垃圾回收后需要更新该表记录。复制对象就是将存活的对象往幸存者区域或者老年代迁移。

Mixed GC是在堆空间占用率达到阈值(默认45%)后触发,整个过程是初始标记、并发标记、最终标记、筛选回收。

  1. 初始标记其实就是触发了一次Minor GC,但是会追加一些额外的动作,比如生成对象引用快照、通知下一阶段开始
  2. 并发标记阶段会通过三色标记法收集各个region下的存活对象信息,比较耗时
  3. 最终标记(重新标记)阶段是为了重新标记在并发标记阶段发生过变动的对象,再次确认对象是否存活
  4. 筛选回收(清理)阶段会筛选出全部的新生代和部分垃圾较多的老年代进行回收,以避免触发Full GC,从而减少停顿。

Full GC是G1收集器极力避免出现的情况,只有在Mixed GC无法跟上用户线程分配内存速度或者老年代爆满这两种情况才会触发

G1收集器的特点有

  • 将原来的大块分代转化为多个region,从而可以动态调整新生代、老年代等分代大小
  • 使用Mixed GC每次收集新生代和部分价值高的老年代,从而减少了Full GC的发生,减少了停顿

写在最后

JVM结束后,会补充一段SPI的相关知识。以上这部分知识相当枯燥啊,我也明白,写的时候也蛮累的,为了能尽量保证完整准确,查阅了大量资料,花了略长的时间。说实话,我也觉得有些累了,接下来会同步发一篇接口性能优化的文章,喜欢的话移步观看喔。 查询接口性能优化实录,讲点新手也能用的

转载自:https://juejin.cn/post/7204998680066621498
评论
请登录