备战秋招2020/5/1

个人觉得JVM内存模型还是要好好背背的,所以这点东西背了好多天(因为每天可能就背一个小时左右)

1、说一下JVM内存模型

JVM内存模型包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区
在这里插入图片描述
程序计数器

程序计数器是一块比较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

它是线程私有的,并且此内存区域是唯一一个在《Java虚拟机规范》中没有明确规定任何OutOfMemoryError情况的区域。

为什么要设计为线程私有?

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的程序计数器互不影响,独立存储。

程序计数器中记录的内容是什么?

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。

Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法返回地址等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常

局部变量表:

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

局部变量表以变量槽为最小单位,《Java虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小,只是说每个变量操都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。《Java虚拟机规范》允许变量槽的长度可以随着处理器、操作系统或者虚拟机实现的不同而发生变化,但是它会保证即使在64位虚拟机中使用了64位物理空间去实现一个变量槽,虚拟机仍要使用对齐和补白的手段让变量槽在外观上看起来与32位虚拟机中的一致。

对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。

Java虚拟机使用索引定位的方式使用局部变量表,索引值是从0开始的。需要注意的是如果访问的是64位数据类型的变量由于它是使用两个变量槽实现的,Java虚拟机不允许采用任何方式单独访问其中的某一个。

当一个方法被调用的时候,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字this来访问这个隐含的参数。其余参数按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完后,再根据方法体内部定义的变量顺序和作用域分配其余的槽。

为了尽可能节省栈帧耗用的内存空间,局部变量表的槽是可以重用的,方法体中定义的变量,其作用域不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的槽就可以交给其他变量来重用。

局部变量如果定义了但是没有被赋初始值,那它是完全不能使用的。

操作数栈

操作数栈也被称为操作栈或者表达式栈,它具有栈先入后出的性质。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈中的元素可以是任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。举个例子:整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器必须要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令只能用于整数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

在概念模型当中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在方法调用时就可以直接公用一部分数据,无序进行额外的参数赋值传递。

Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分将在每一次运行期间都转化为直接引用,这部分就成为动态连接。

方法返回地址
当一个方法开始执行后,只有这两种方式退出这个方法。

第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成“

另一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成”。一个方法使用异常完成出后的方式退出,是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用于帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值而方法异常退出时,返回地址是奥通过异常处理表来确定,栈帧中就一般不会保存这部分信息。

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似,其区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

《Java虚拟机规范》对本地方法栈中使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(比如HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一了。与虚拟机栈一样,本地方法栈也会在栈深度移除或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

Java堆

对于Java应用程序来说,Java堆(JavaHeap)是虚拟机所管理的内存中最大的一块。

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存,这里的“几乎”指的是从实现角度来看的,随着Java语言的发展,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大、栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆内存上就变得不那么绝对了。

Java堆是垃圾收集器管理的内存区域。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集的理论设计的,所以Java堆中经常会被划分为“新生代” “老年代” “永久代” “Eden空间” “From Survivor空间” “To Survivor空间”,需要注意的是这些空间并非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。

补充:Java堆在JDK7中逻辑上分为新生代+老年代+永久代,在JDK8中逻辑上分为了新生代+养老代+元空间

以G1收集器的出现为分界,在这之前它内部的垃圾收集器全部基于前面说的这种分代,但是到了今天HotSpot里面也出现了不采用分代设计的垃圾收集器

如果从内存分配的角度看,所有线程共享Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是那个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好的回收内存,或者更快的分配内存。

根据《Java虚拟机规范》的规范,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。但是对于大对象(典型的如数组对象),多数虚拟机处于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的。如果再Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做”非堆“,目的就是与Java堆区分开。

需要注意的是再JDK8以前方法区的实现叫做永久代,现在回过头来看,当时将方法区设计为永久代并不是一个好的注意,这种设计导致了Java应用更容易于导内存溢出的问题。所以再JDK8之后就废弃了永久代的概念,改用在本地内存中实现的元空间来代替了

《Java虚拟机规范》对方区的越是是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定的大小或者可扩展外,甚至还可以选择不是先垃圾回收。相对而言,垃圾收集行为再这个区域的确是比较少出现的,但并非数据进入方法区之后就永久存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的写在,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。

根据《Java虚拟机规范》的规定,如果方法去无法满足新的内存分配需求时,将会抛出OutOfMemoryError异常

方法区的演进细节

Hotspot中方法区的变化:

JDK版本变化
JDK1.6及以前有永久代(permanent generation),静态变量存储在永久代上
JDK1.7有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

JDK6
方法区由永久代实现,使用 JVM 虚拟机内存(虚拟的内存)
在这里插入图片描述
JDK7
方法区由永久代实现,使用 JVM 虚拟机内存
在这里插入图片描述
JDK8
方法区由元空间实现,使用物理机本地内存
在这里插入图片描述

2、CAS是什么?

1、什么是CAS?

CAS翻译过来就是比较并交换,它是一种CPU并发原语,就是说它一定是连续的执行若干指令并且不能被中断,因为CAS是一条CPU的原子指令,所以不会造成所谓的数据不一致的问题,是线程安全的

2、CAS的功能

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

3、CAS在java中的体现

CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。

CAS的全称是Compare-And-Swap

原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断

CAS是一种乐观锁的思想

4、CAS的底层原理?

总的来说:CAS的底层原理总的来说就是unsafe类+CAS自旋锁的思想

unsafe中的方法会先从主存当中获取某一时刻的值,然后进行cas操作跟期望的值进行比较,如果比较失败就一直循环whlie进行比较,直到比较成功并修改为新的值,并结束自旋循环

5、cas的缺点

CAS不加锁,保证一致性,但是需要多次比较

  • 竞争激烈时循环时间长,cpu开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证一个共享变量的原子操作
    • 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
    • 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 会导致ABA问题

3、MySQL的索引失效的场景有哪些?

  1. 最佳左前缀法则:

    过滤条件要使用索引必须按照索引建立的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。如果查询条件中没有这些字段的第1个字段时,多列(或联合)索引不会被使用

    “带头大哥不能死、中间兄弟不能断”

  2. 计算、函数、类型转换(自动或手动)导致索引失效

  3. 范围条件右边的列索引失效(范围条件右边的索引列失效,指的是索引当中的不是我们sql语句当中的)

  4. 不等于(!= 或者<>)索引失效

  5. is null可以使用索引,is not null无法使用索引(同理,在查询中使用not like也无法使用索引)

  6. like以通配符%开头索引失效

  7. OR 前后存在非索引的列,索引失效

    在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后的条件列没有进行索引,那么索引也会失效。也就是说,OR前后的两个条件中的列都是索引时,查询中才能使用索引。

    因为OR的含义就是两个只要满足一个即可,因此只有一个条件进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描因此索引的条件列也会失效。

  8. 字符集不同导致使用字符集转换函数从而导致索引失效

4、Redis缓存击穿、缓存雪崩、缓存穿透如何避免

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

缓存雪崩一般是由两个原因导致的,应对方案也有所不同

导致雪崩的原因之一

具体来说,当数据保存在缓存中,并且设置了过期时间时,如果在某一个时刻,大量数据同时过期,此时,应用再访问这些数据的话,就会发生缓存缺失。紧接着,应用就会把请求发送给数据库,从数据库中读取数据。如果应用的并发请求量很大,那么数据库的压力也就很大,这会进一步影响到数据库的其他正常业务请求处理

针对大量数据同时失效带来的缓存雪崩问题,有两种解决方案

  1. 首先,我们可以避免给大量的数据设置相同的过期时间。如果业务层的确要求有些数据同时失效,你可以在用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数(例如,随机增加 1~3 分钟),这样一来,不同数据的过期时间有所差别,但差别又不会太大,既避免了大量数据同时过期,同时也保证了这些数据基本在相近的时间失效,仍然能满足业务需求。

  2. 除了微调过期时间,我们还可以通过服务降级,来应对缓存雪崩

    所谓的服务降级,是指发生缓存雪崩时,针对不同的数据采取不同的处理方式。

    • 当业务应用访问的是非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;
    • 当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。

    这样一来,只有部分过期数据的请求会发送到数据库,数据库的压力就没有那么大了。

导致缓存雪崩的另一个原因

Redis 缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩。

一般来说,一个 Redis 实例可以支持数万级别的请求处理吞吐量,而单个数据库可能只能支持数千级别的请求处理吞吐量,它们两个的处理能力可能相差了近十倍。由于缓存雪崩,Redis 缓存失效,所以,数据库就可能要承受近十倍的请求压力,从而因为压力过大而崩溃。

针对这种Redis 缓存实例发生故障宕机我们有两种应对办法

  1. 第一个建议,是在业务系统中实现服务熔断或请求限流机制。

    所谓的服务熔断,是指在发生缓存雪崩时,为了防止引发连锁的数据库雪崩,甚至是整个系统的崩溃,我们暂停业务应用对缓存系统的接口访问。再具体点说,就是业务应用调用缓存接口时,缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,等到 Redis 缓存实例重新恢复服务后,再允许应用请求发送到缓存系统。

    这样一来,我们就避免了大量请求因缓存缺失,而积压到数据库系统,保证了数据库系统的正常运行。

    在业务系统运行时,我们可以监测 Redis 缓存所在机器和数据库所在机器的负载指标,例如每秒请求数、CPU 利用率、内存利用率等。如果我们发现 Redis 缓存实例宕机了,而数据库所在机器的负载压力突然增加(例如每秒请求数激增),此时,就发生缓存雪崩了。大量请求被发送到数据库进行处理。我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,从而降低对数据库的访问压力

    服务熔断虽然可以保证数据库的正常运行,但是暂停了整个缓存系统的访问,对业务应用的影响范围大。

    为了尽可能减少这种影响,我们也可以进行请求限流。这里说的请求限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。

    我给你举个例子。假设业务系统正常运行时,请求入口前端允许每秒进入系统的请求是 1 万个,其中,9000 个请求都能在缓存系统中进行处理,只有 1000 个请求会被应用发送到数据库进行处理。

    一旦发生了缓存雪崩,数据库的每秒请求数突然增加到每秒 1 万个,此时,我们就可以启动请求限流机制,在请求入口前端只允许每秒进入系统的请求数为 1000 个,再多的请求就会在入口前端被直接拒绝服务。所以,使用了请求限流,就可以避免大量并发请求压力传递到数据库层

  2. 第二个建议就是出现这种情况之前提前预防

    通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题

缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。

为了避免缓存击穿给数据库带来的激增压力,我们的解决方法也比较直接,对于访问特别频繁的热点数据,我们就不设置过期时间了

这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 Redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问

缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据

此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力

那么,缓存穿透会发生在什么时候呢?一般来说,有两种情况。

  • 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
  • 恶意攻击:专门访问数据库中没有的数据。

这里提供三种解决方案:

  • 第一种方案是,缓存空值或缺省值

    一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值(例如,库存的缺省值可以设为 0)。紧接着,应用发送的后续请求再进行查询时,就可以直接从 Redis 中读取空值或缺省值,返回给业务应用了,避免了把大量请求发送给数据库处理,保持了数据库的正常运行

  • 第二种方案是,使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力

  • 最后一种方案是,在请求入口的前端进行请求检测

    缓存穿透的一个原因是有大量的恶意请求访问不存在的数据,所以,一个有效的应对方案是在请求入口前端,对业务系统接收到的请求进行合法性检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉,不让它们访问后端缓存和数据库。这样一来,也就不会出现缓存穿透问题了

5、什么是半连接队列

服务器第一次收到客户端的连接请求报文段并回复之后,就会处于SYN_RCVD(同步接收) 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值