
本文目录:讲一下JVM内存结构?程序计数器虚拟机栈本地方法栈堆方法区运行时常量池直接内存Java对象的定位方式说一下堆栈的区别?什么情况下会发生栈溢出?类文件结构什么是类加载?类加载的过程?什么是双亲委派模型?为什么需要双亲委派模型?什么是类加载器,类加载器有哪些?类的实例化顺序?如何判断一个对象是否存活?可作为GC Roots的对象有哪些?什么情况下类会被卸载?强引用、软引用、弱引用、虚引用是什么,有什么区别?Minor GC 和 Full GC的区别?内存的分配策略?Full GC 的触发条件?垃圾回收算法有哪些?有哪些垃圾回收器?常用的 JVM 调优的命令都有哪些?对象头了解吗?如何排查 OOM 的问题?GC是什么?为什么要GC?本文已经收录到github仓库,此仓库用于分享互联网大厂高频面试题、Java核心知识总结,包括Java基础、并发、MySQL、Springboot、MyBatis、Redis、RabbitMQ等等,面试必备!欢迎大家star!github地址:https://github.com/Tyson0314/Java-learning讲一下JVM内存结构?JVM内存结构分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。程序计数器线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。虚拟机栈Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。局部变量表是用于存放方法参数和方法内的局部变量。每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接部分符号引用在运行期间转化为直接引用,这种转化就是动态链接Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。可以通过 -Xss 参数来指定每个线程的虚拟机栈内存大小:java -Xss2M本地方法栈虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++等)编写的。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。堆堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。堆可以细分为:新生代(Eden空间、From Survivor、To Survivor空间)和老年代。通过 -Xms设定程序启动时占用内存大小,通过-Xmx设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。java -Xms1M -Xmx2M方法区方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。永久代方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,并且只有 HotSpot 有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。元空间JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。为什么要将永久代替换为元空间呢?永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。运行时常量池运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。图片来源:https://blog.csdn.net/soonfly直接内存直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。Java对象的定位方式Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。说一下堆栈的区别?堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。堆是线程共享的;栈是线程私有的。什么情况下会发生栈溢出?当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常。这种情况通常是因为方法递归没终止条件。新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出OutOfMemoryError异常。比如线程启动过多就会出现这种情况。类文件结构Class 文件结构如下:ClassFile { u4 magic; //类文件的标志 u2 minor_version;//小版本号 u2 major_version;//大版本号 u2 constant_pool_count;//常量池的数量 cp_info constant_pool[constant_pool_count-1];//常量池 u2 access_flags;//类的访问标记 u2 this_class;//当前类的索引 u2 super_class;//父类 u2 interfaces_count;//接口 u2 interfaces[interfaces_count];//一个类可以实现多个接口 u2 fields_count;//字段属性 field_info fields[fields_count];//一个类会可以有个字段 u2 methods_count;//方法数量 method_info methods[methods_count];//一个类可以有个多个方法 u2 attributes_count;//此类的属性表中的属性数 attribute_info attributes[attributes_count];//属性表集合 }主要参数如下:魔数:class文件标志。文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。常量池:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。访问标志:识别类或者接口的访问信息,比如这个Class是类还是接口,是否为 public 或者 abstract 类型等等。当前类的索引:类索引用于确定这个类的全限定名。什么是类加载?类加载的过程?类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。加载通过类的全限定名获取定义此类的二进制字节流将字节流所代表的静态存储结构转换为方法区的运行时数据结构在内存中生成一个代表该类的Class对象,作为方法区类信息的访问入口验证确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。准备为类变量分配内存并设置类变量初始值的阶段。解析虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。初始化开始执行类中定义的Java代码,初始化阶段是调用类构造器的过程。什么是双亲委派模型?一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。源码如下:public abstract class ClassLoader { // The parent class loader for delegation private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }为什么需要双亲委派模型?双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。什么是类加载器,类加载器有哪些?实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()获取它。自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。类的实例化顺序?父类中的static代码块,当前类的static代码块父类的普通代码块父类的构造函数当前类普通代码块当前类的构造函数如何判断一个对象是否存活?对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。引用计数法给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,obj1 和 obj2 互相引用,这种情况下,引用计数器的值都是1,不会被垃圾回收。public class ReferenceCount { Object instance = null; public static void main(String[] args) { ReferenceCount obj1 = new ReferenceCount(); ReferenceCount obj2 = new ReferenceCount(); obj1.instance = obj2; obj2.instance = obj1; obj1 = null; obj2 = null; } }可达性分析通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。可作为GC Roots的对象有哪些?虚拟机栈中引用的对象本地方法栈中Native方法引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象什么情况下类会被卸载?需要同时满足以下 3 个条件类才可能会被卸载 :该类所有的实例都已经被回收。加载该类的类加载器已经被回收。该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。强引用、软引用、弱引用、虚引用是什么,有什么区别?强引用:在程序中普遍存在的引用赋值,类似Object obj = new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。//软引用 SoftReference<String> softRef = new SoftReference<String>(str);弱引用:在进行垃圾回收时,不管当前内存空间足够与否,都会回收只具有弱引用的对象。//弱引用 WeakReference<String> weakRef = new WeakReference<String>(str);虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要是为了能在对象被收集器回收时收到一个系统通知。Minor GC 和 Full GC的区别?Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC会频繁执行,执行的速度一般也会比较快。Full GC:回收老年代和新生代,老年代的对象存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。内存的分配策略?对象优先在 Eden 分配大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC。大对象直接进入老年代大对象是指需要连续内存空间的对象,最典型的大对象有长字符串和大数组。可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配。长期存活的对象进入老年代通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在Survivor区每经过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。动态对象年龄判定并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。空间分配担保在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值为不允许担保失败,那么就要进行一次 Full GC。Full GC 的触发条件?对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 full GC:调用 System.gc()只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。老年代空间不足老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。空间分配担保失败使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。JDK 1.7 及以前的永久代空间不足在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。垃圾回收算法有哪些?垃圾回收算法有四种,分别是标记清除法、标记整理法、复制算法、分代收集算法。标记清除算法首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。复制清除算法半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。标记整理算法根据老年代的特点提出的一种标记算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。分类收集算法根据各个年代的特点采用最适当的收集算法。一般将堆分为新生代和老年代。新生代使用复制算法老年代使用标记清除算法或者标记整理算法在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。有哪些垃圾回收器?垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。这7种垃圾收集器的特点:收集器串行、并行or并发新生代/老年代算法目标适用场景Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用G1并发both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMSSerial 收集器单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程( Stop The World ),直到它收集结束。特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。ParNew 收集器Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 Serial 收集器基本一致。Parallel Scavenge 收集器新生代收集器,基于复制清除算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。CMS 垃圾收集器关注点更多的是用户线程的停顿时间。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。-XX:MaxGCPauseMillis参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。-XX:GCTimeRatio参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。Serial Old 收集器Serial 收集器的老年代版本,单线程收集器,使用标记整理算法。Parallel Old 收集器Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记整理算法。CMS 收集器Concurrent Mark Sweep ,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作。CMS 垃圾回收基于标记清除算法实现,整个过程分为四个步骤:初始标记: 暂停所有用户线程(Stop The World),记录直接与 GC Roots 直接相连的对象 。并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程。并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的。在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。优点:并发收集,停顿时间短。缺点:标记清除算法导致收集结束有大量空间碎片。产生浮动垃圾,在并发清理阶段用户线程还在运行,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中回收它们,只好等到下一次垃圾回收再处理;G1收集器G1垃圾收集器的目标是在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。G1将整个堆分成相同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old和Humongous。分区的大小取值范围为 1M 到 32M,都是2的幂次方。分区大小可以通过-XX:G1HeapRegionSize参数指定。Humongous区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。G1 收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。特点:可以由用户指定期望的垃圾收集停顿时间。G1 收集器的回收过程分为以下几个步骤:初始标记。暂停所有其他线程,记录直接与 GC Roots 直接相连的对象,耗时较短 。并发标记。从GC Roots开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长,不过可以和用户程序并发执行。最终标记。需对其他线程做短暂的暂停,用于处理并发标记阶段对象引用出现变动的区域。筛选回收。对各个分区的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全部空间。这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成。常用的 JVM 调优的命令都有哪些?jps:列出本机所有 Java 进程的进程号。常用参数如下:-m 输出main方法的参数-l 输出完全的包名和应用主类名-v 输出JVM参数jps -lvm //output //4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8jstack:查看某个 Java 进程内的线程堆栈信息。使用参数-l可以打印额外的锁信息,发生死锁时可以使用jstack -l pid观察锁持有情况。jstack -l 4124 | more输出结果如下:"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - NoneWAITING (parking)指线程处于挂起中,在等待某个条件发生,来把自己唤醒。jstat:用于查看虚拟机各种运行状态信息(类装载、内存、垃圾收集等运行数据)。使用参数-gcuitl可以查看垃圾回收的统计信息。jstat -gcutil 4124 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275参数说明:S0:Survivor0区当前使用比例S1:Survivor1区当前使用比例E:Eden区使用比例O:老年代使用比例M:元数据区使用比例CCS:压缩使用比例YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间jmap:查看堆内存快照。通过jmap命令可以获得运行中的堆内存的快照,从而可以对堆内存进行离线分析。查询进程4124的堆内存快照,输出结果如下:>jmap -heap 4124 Attaching to process ID 4124, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.221-b11 using thread-local object allocation. Parallel GC with 6 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4238344192 (4042.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1412431872 (1347.0MB) OldSize = 177733632 (169.5MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 327155712 (312.0MB) used = 223702392 (213.33922576904297MB) free = 103453320 (98.66077423095703MB) 68.37795697725736% used From Space: capacity = 21495808 (20.5MB) used = 0 (0.0MB) free = 21495808 (20.5MB) 0.0% used To Space: capacity = 23068672 (22.0MB) used = 0 (0.0MB) free = 23068672 (22.0MB) 0.0% used PS Old Generation capacity = 217579520 (207.5MB) used = 41781472 (39.845916748046875MB) free = 175798048 (167.65408325195312MB) 19.20285144484187% used 27776 interned Strings occupying 3262336 bytes.对象头了解吗?Java 内存中的对象由以下三部分组成:对象头、实例数据和对齐填充字节。而对象头由以下三部分组成:mark word、指向类信息的指针和数组长度(数组才有)。mark word包含:对象的哈希码、分代年龄和锁标志位。对象的实例数据就是 Java 对象的属性和值。对齐填充字节:因为JVM要求对象占的内存大小是 8bit 的倍数,因此后面有几个字节用于把对象的大小补齐至 8bit 的倍数。内存对齐的主要作用是:平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因:经过内存对齐后,CPU的内存访问速度大大提升。main方法执行过程以下是示例代码:public class Application { public static void main(String[] args) { Person p = new Person("大彬"); p.getName(); } } class Person { public String name; public Person(String name) { this.name = name; } public String getName() { return this.name; } }执行main方法的过程如下:编译Application.java后得到 Application.class 后,执行这个class文件,系统会启动一个 JVM 进程,从类路径中找到一个名为 Application.class 的二进制文件,将 Application 类信息加载到运行时数据区的方法区内,这个过程叫做类的加载。JVM 找到 Application 的主程序入口,执行main方法。main方法的第一条语句为 Person p = new Person("大彬") ,就是让 JVM 创建一个Person对象,但是这个时候方法区中是没有 Person 类的信息的,所以 JVM 马上加载 Person 类,把 Person 类的信息放到方法区中。加载完 Person 类后,JVM 在堆中分配内存给 Person 对象,然后调用构造函数初始化 Person 对象,这个 Person 对象持有指向方法区中的 Person 类的类型信息的引用。执行p.getName()时,JVM 根据 p 的引用找到 p 所指向的对象,然后根据此对象持有的引用定位到方法区中 Person 类的类型信息的方法表,获得 getName() 的字节码地址。执行getName()方法。对象创建过程类加载检查:当虚拟机遇到一条 new 指令时,首先检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。分配内存:在类加载检查通过后,接下来虚拟机将为对象实例分配内存。初始化。分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。设置对象头。Hotspot 虚拟机的对象头包括:存储对象自身的运行时数据(哈希码、分代年龄、锁标志等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。按照Java代码进行初始化。如何排查 OOM 的问题?排查 OOM 的方法:增加JVM参数 -XX:+HeapDumpOnOutOfMemoryError 和-XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;jstat 查看监控 JVM 的内存和 GC 情况,评估问题大概出在什么区域;使用 MAT 工具载入 dump 文件,分析大对象的占用情况 。GC是什么?为什么要GC?GC 是垃圾收集的意思(Gabage Collection)。内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。参考资料周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社本文目录:讲一下JVM内存结构?程序计数器虚拟机栈本地方法栈堆方法区运行时常量池直接内存Java对象的定位方式说一下堆栈的区别?什么情况下会发生栈溢出?类文件结构什么是类加载?类加载的过程?什么是双亲委派模型?为什么需要双亲委派模型?什么是类加载器,类加载器有哪些?类的实例化顺序?如何判断一个对象是否存活?可作为GC Roots的对象有哪些?什么情况下类会被卸载?强引用、软引用、弱引用、虚引用是什么,有什么区别?Minor GC 和 Full GC的区别?内存的分配策略?Full GC 的触发条件?垃圾回收算法有哪些?有哪些垃圾回收器?常用的 JVM 调优的命令都有哪些?对象头了解吗?如何排查 OOM 的问题?GC是什么?为什么要GC?我将大厂常见的高频面试题整理成PDF了,方便大家阅读,需要的小伙伴可以自行下载(复制链接到浏览器打开):链接:https://pan.baidu.com/s/16GnVoALA1r6BhumuUrXIRg提取码:6666讲一下JVM内存结构?JVM内存结构分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。程序计数器线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用:当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。虚拟机栈Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。局部变量表是用于存放方法参数和方法内的局部变量。每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接部分符号引用在运行期间转化为直接引用,这种转化就是动态链接Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。可以通过 -Xss 参数来指定每个线程的虚拟机栈内存大小:java -Xss2M本地方法栈虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++等)编写的。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。堆堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。堆可以细分为:新生代(Eden空间、From Survivor、To Survivor空间)和老年代。通过 -Xms设定程序启动时占用内存大小,通过-Xmx设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。java -Xms1M -Xmx2M方法区方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。永久代方法区是 JVM 的规范,而永久代PermGen是方法区的一种实现方式,并且只有 HotSpot 有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。元空间JDK 1.8 的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。为什么要将永久代替换为元空间呢?永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。运行时常量池运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。图片来源:https://blog.csdn.net/soonfly直接内存直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。Java对象的定位方式Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。说一下堆栈的区别?堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。堆存放的是对象的实例和数组;栈存放的是局部变量,操作数栈,返回结果等。堆是线程共享的;栈是线程私有的。什么情况下会发生栈溢出?当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常。这种情况通常是因为方法递归没终止条件。新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出OutOfMemoryError异常。比如线程启动过多就会出现这种情况。类文件结构Class 文件结构如下:ClassFile { u4 magic; //类文件的标志 u2 minor_version;//小版本号 u2 major_version;//大版本号 u2 constant_pool_count;//常量池的数量 cp_info constant_pool[constant_pool_count-1];//常量池 u2 access_flags;//类的访问标记 u2 this_class;//当前类的索引 u2 super_class;//父类 u2 interfaces_count;//接口 u2 interfaces[interfaces_count];//一个类可以实现多个接口 u2 fields_count;//字段属性 field_info fields[fields_count];//一个类会可以有个字段 u2 methods_count;//方法数量 method_info methods[methods_count];//一个类可以有个多个方法 u2 attributes_count;//此类的属性表中的属性数 attribute_info attributes[attributes_count];//属性表集合 }主要参数如下:魔数:class文件标志。文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。常量池:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。访问标志:识别类或者接口的访问信息,比如这个Class是类还是接口,是否为 public 或者 abstract 类型等等。当前类的索引:类索引用于确定这个类的全限定名。什么是类加载?类加载的过程?类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。加载通过类的全限定名获取定义此类的二进制字节流将字节流所代表的静态存储结构转换为方法区的运行时数据结构在内存中生成一个代表该类的Class对象,作为方法区类信息的访问入口验证确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。准备为类变量分配内存并设置类变量初始值的阶段。解析虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。初始化开始执行类中定义的Java代码,初始化阶段是调用类构造器的过程。什么是双亲委派模型?一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。双亲委派模型的具体实现代码在 java.lang.ClassLoader中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。源码如下:public abstract class ClassLoader { // The parent class loader for delegation private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }为什么需要双亲委派模型?双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。什么是类加载器,类加载器有哪些?实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:启动类加载器:用来加载 Java 核心类库,无法被 Java 程序直接引用。扩展类加载器:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。系统类加载器:它根据应用的类路径来加载 Java 类。可通过ClassLoader.getSystemClassLoader()获取它。自定义类加载器:通过继承java.lang.ClassLoader类的方式实现。类的实例化顺序?父类中的static代码块,当前类的static代码块父类的普通代码块父类的构造函数当前类普通代码块当前类的构造函数如何判断一个对象是否存活?对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。引用计数法给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,obj1 和 obj2 互相引用,这种情况下,引用计数器的值都是1,不会被垃圾回收。public class ReferenceCount { Object instance = null; public static void main(String[] args) { ReferenceCount obj1 = new ReferenceCount(); ReferenceCount obj2 = new ReferenceCount(); obj1.instance = obj2; obj2.instance = obj1; obj1 = null; obj2 = null; } }可达性分析通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。可作为GC Roots的对象有哪些?虚拟机栈中引用的对象本地方法栈中Native方法引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象什么情况下类会被卸载?需要同时满足以下 3 个条件类才可能会被卸载 :该类所有的实例都已经被回收。加载该类的类加载器已经被回收。该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。强引用、软引用、弱引用、虚引用是什么,有什么区别?强引用:在程序中普遍存在的引用赋值,类似Object obj = new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。//软引用 SoftReference<String> softRef = new SoftReference<String>(str);弱引用:在进行垃圾回收时,不管当前内存空间足够与否,都会回收只具有弱引用的对象。//弱引用 WeakReference<String> weakRef = new WeakReference<String>(str);虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要是为了能在对象被收集器回收时收到一个系统通知。Minor GC 和 Full GC的区别?Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC会频繁执行,执行的速度一般也会比较快。Full GC:回收老年代和新生代,老年代的对象存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。内存的分配策略?对象优先在 Eden 分配大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC。大对象直接进入老年代大对象是指需要连续内存空间的对象,最典型的大对象有长字符串和大数组。可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配。长期存活的对象进入老年代通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在Survivor区每经过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。动态对象年龄判定并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。空间分配担保在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值为不允许担保失败,那么就要进行一次 Full GC。Full GC 的触发条件?对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 full GC:调用 System.gc()只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。老年代空间不足老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。空间分配担保失败使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。JDK 1.7 及以前的永久代空间不足在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。垃圾回收算法有哪些?垃圾回收算法有四种,分别是标记清除法、标记整理法、复制算法、分代收集算法。标记清除算法首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。复制清除算法半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。标记整理算法根据老年代的特点提出的一种标记算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。分类收集算法根据各个年代的特点采用最适当的收集算法。一般将堆分为新生代和老年代。新生代使用复制算法老年代使用标记清除算法或者标记整理算法在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。有哪些垃圾回收器?垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。这7种垃圾收集器的特点:收集器串行、并行or并发新生代/老年代算法目标适用场景Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用G1并发both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMSSerial 收集器单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程( Stop The World ),直到它收集结束。特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。ParNew 收集器Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 Serial 收集器基本一致。Parallel Scavenge 收集器新生代收集器,基于复制清除算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。CMS 垃圾收集器关注点更多的是用户线程的停顿时间。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。-XX:MaxGCPauseMillis参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。-XX:GCTimeRatio参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。Serial Old 收集器Serial 收集器的老年代版本,单线程收集器,使用标记整理算法。Parallel Old 收集器Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记整理算法。CMS 收集器Concurrent Mark Sweep ,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作。CMS 垃圾回收基于标记清除算法实现,整个过程分为四个步骤:初始标记: 暂停所有用户线程(Stop The World),记录直接与 GC Roots 直接相连的对象 。并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程。并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的。在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。优点:并发收集,停顿时间短。缺点:标记清除算法导致收集结束有大量空间碎片。产生浮动垃圾,在并发清理阶段用户线程还在运行,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中回收它们,只好等到下一次垃圾回收再处理;G1收集器G1垃圾收集器的目标是在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。G1将整个堆分成相同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old和Humongous。分区的大小取值范围为 1M 到 32M,都是2的幂次方。分区大小可以通过-XX:G1HeapRegionSize参数指定。Humongous区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。G1 收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。特点:可以由用户指定期望的垃圾收集停顿时间。G1 收集器的回收过程分为以下几个步骤:初始标记。暂停所有其他线程,记录直接与 GC Roots 直接相连的对象,耗时较短 。并发标记。从GC Roots开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长,不过可以和用户程序并发执行。最终标记。需对其他线程做短暂的暂停,用于处理并发标记阶段对象引用出现变动的区域。筛选回收。对各个分区的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全部空间。这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成。常用的 JVM 调优的命令都有哪些?jps:列出本机所有 Java 进程的进程号。常用参数如下:-m 输出main方法的参数-l 输出完全的包名和应用主类名-v 输出JVM参数jps -lvm //output //4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8jstack:查看某个 Java 进程内的线程堆栈信息。使用参数-l可以打印额外的锁信息,发生死锁时可以使用jstack -l pid观察锁持有情况。jstack -l 4124 | more输出结果如下:"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - NoneWAITING (parking)指线程处于挂起中,在等待某个条件发生,来把自己唤醒。jstat:用于查看虚拟机各种运行状态信息(类装载、内存、垃圾收集等运行数据)。使用参数-gcuitl可以查看垃圾回收的统计信息。jstat -gcutil 4124 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275参数说明:S0:Survivor0区当前使用比例S1:Survivor1区当前使用比例E:Eden区使用比例O:老年代使用比例M:元数据区使用比例CCS:压缩使用比例YGC:年轻代垃圾回收次数FGC:老年代垃圾回收次数FGCT:老年代垃圾回收消耗时间GCT:垃圾回收消耗总时间jmap:查看堆内存快照。通过jmap命令可以获得运行中的堆内存的快照,从而可以对堆内存进行离线分析。查询进程4124的堆内存快照,输出结果如下:>jmap -heap 4124 Attaching to process ID 4124, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.221-b11 using thread-local object allocation. Parallel GC with 6 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 4238344192 (4042.0MB) NewSize = 88604672 (84.5MB) MaxNewSize = 1412431872 (1347.0MB) OldSize = 177733632 (169.5MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.796875MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 327155712 (312.0MB) used = 223702392 (213.33922576904297MB) free = 103453320 (98.66077423095703MB) 68.37795697725736% used From Space: capacity = 21495808 (20.5MB) used = 0 (0.0MB) free = 21495808 (20.5MB) 0.0% used To Space: capacity = 23068672 (22.0MB) used = 0 (0.0MB) free = 23068672 (22.0MB) 0.0% used PS Old Generation capacity = 217579520 (207.5MB) used = 41781472 (39.845916748046875MB) free = 175798048 (167.65408325195312MB) 19.20285144484187% used 27776 interned Strings occupying 3262336 bytes.对象头了解吗?Java 内存中的对象由以下三部分组成:对象头、实例数据和对齐填充字节。而对象头由以下三部分组成:mark word、指向类信息的指针和数组长度(数组才有)。mark word包含:对象的哈希码、分代年龄和锁标志位。对象的实例数据就是 Java 对象的属性和值。对齐填充字节:因为JVM要求对象占的内存大小是 8bit 的倍数,因此后面有几个字节用于把对象的大小补齐至 8bit 的倍数。内存对齐的主要作用是:平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因:经过内存对齐后,CPU的内存访问速度大大提升。main方法执行过程以下是示例代码:public class Application { public static void main(String[] args) { Person p = new Person("大彬"); p.getName(); } } class Person { public String name; public Person(String name) { this.name = name; } public String getName() { return this.name; } }执行main方法的过程如下:编译Application.java后得到 Application.class 后,执行这个class文件,系统会启动一个 JVM 进程,从类路径中找到一个名为 Application.class 的二进制文件,将 Application 类信息加载到运行时数据区的方法区内,这个过程叫做类的加载。JVM 找到 Application 的主程序入口,执行main方法。main方法的第一条语句为 Person p = new Person("大彬") ,就是让 JVM 创建一个Person对象,但是这个时候方法区中是没有 Person 类的信息的,所以 JVM 马上加载 Person 类,把 Person 类的信息放到方法区中。加载完 Person 类后,JVM 在堆中分配内存给 Person 对象,然后调用构造函数初始化 Person 对象,这个 Person 对象持有指向方法区中的 Person 类的类型信息的引用。执行p.getName()时,JVM 根据 p 的引用找到 p 所指向的对象,然后根据此对象持有的引用定位到方法区中 Person 类的类型信息的方法表,获得 getName() 的字节码地址。执行getName()方法。对象创建过程类加载检查:当虚拟机遇到一条 new 指令时,首先检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。分配内存:在类加载检查通过后,接下来虚拟机将为对象实例分配内存。初始化。分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。设置对象头。Hotspot 虚拟机的对象头包括:存储对象自身的运行时数据(哈希码、分代年龄、锁标志等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。按照Java代码进行初始化。如何排查 OOM 的问题?排查 OOM 的方法:增加JVM参数 -XX:+HeapDumpOnOutOfMemoryError 和-XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;jstat 查看监控 JVM 的内存和 GC 情况,评估问题大概出在什么区域;使用 MAT 工具载入 dump 文件,分析大对象的占用情况 。GC是什么?为什么要GC?GC 是垃圾收集的意思(Gabage Collection)。内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。参考资料周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社文章对你有用的话,点个赞,支持一下~我是大彬,非科班转码,校招拿了多家互联网中大厂offer,专注分享Java技术干货,欢迎关注~
大家好,我是大彬~今天给大家分享MySQL常考的面试题,看看你们能答对多少。本期MySQL面试题的目录如下:事务的四大特性?事务隔离级别有哪些?索引什么是索引?索引的优缺点?索引的作用?什么情况下需要建索引?什么情况下不建索引?索引的数据结构Hash索引和B+树索引的区别?为什么B+树比B树更适合实现数据库索引?索引有什么分类?什么是最左匹配原则?什么是聚集索引?什么是覆盖索引?索引的设计原则?索引什么时候会失效?什么是前缀索引?常见的存储引擎有哪些?MyISAM和InnoDB的区别?MVCC 实现原理?快照读和当前读共享锁和排他锁大表怎么优化?bin log/redo log/undo logbin log和redo log有什么区别?讲一下MySQL架构?分库分表什么是分区表?分区表类型查询语句执行流程?更新语句执行过程?exist和in的区别?truncate、delete与drop区别?having和where的区别?什么是MySQL主从同步?为什么要做主从同步?乐观锁和悲观锁是什么?用过processlist吗?事务的四大特性?事务特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。一致性是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。隔离性。跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。事务隔离级别有哪些?先了解下几个概念:脏读、不可重复读、幻读。脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。不可重复读是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。幻读是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除。事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。MySQL数据库为我们提供的四种隔离级别:Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。查看隔离级别:select @@transaction_isolation;设置隔离级别:set session transaction isolation level read uncommitted;索引什么是索引?索引是存储引擎用于提高数据库表的访问速度的一种数据结构。索引的优缺点?优点:加快数据查找的速度为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度加快表与表之间的连接缺点:建立索引需要占用物理空间会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长索引的作用?数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。什么情况下需要建索引?经常用于查询的字段经常用于连接的字段建立索引,可以加快连接的速度经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度什么情况下不建索引?where条件中用不到的字段不适合建立索引表记录较少需要经常增删改参与列计算的列不适合建索引区分度不高的字段不适合建立索引,如性别等索引的数据结构索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类型有B+树索引和哈希索引,默认的索引类型为B+树索引。B+树索引 B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ 树中,节点中的 key 从左到右递增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。进行查找操作时,首先在根节点进行二分查找,找到key所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出key所对应的数据项。MySQL 数据库使用最多的索引类型是BTREE索引,底层基于B+树数据结构来实现。mysql> show index from blog\G; *************************** 1. row *************************** Table: blog Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: blog_id Collation: A Cardinality: 4 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: Index_comment: Visible: YES Expression: NULL哈希索引哈希索引是基于哈希表实现的,对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码,并且哈希算法要尽量保证不同的列值计算出的哈希码值是不同的,将哈希码的值作为哈希表的key值,将指向数据行的指针作为哈希表的value值。这样查找一个数据的时间复杂度就是O(1),一般多用于精确查找。Hash索引和B+树索引的区别?哈希索引不支持排序,因为哈希表是无序的。哈希索引不支持范围查找。哈希索引不支持模糊查询及多列索引的最左前缀匹配。因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。为什么B+树比B树更适合实现数据库索引?由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。B+树的查询效率更加稳定,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。索引有什么分类?1、主键索引:名为primary的唯一非空索引,不允许有空值。2、唯一索引:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一约束的列可以为null且可以存在多个null值。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。创建唯一索引的SQL语句如下:ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...);3、组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。4、全文索引:只有在MyISAM引擎上才能使用,只能在CHAR、VARCHAR和TEXT类型字段上使用全文索引。什么是最左匹配原则?如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。如果查询条件为a = 1 and b > 2 and c = 3,那么a、b个字两段能用到索引,而c无法使用索引,因为b字段是范围查询,导致后面的字段无法使用索引。如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当执行a = 1 and b = 2时a和b字段能用到索引。而对于查询条件a < 4 and b = 2时,a字段能用到索引,b字段则用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b的值不是有序的,因此b字段无法使用索引。什么是聚集索引?InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么InnoDB内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。什么是覆盖索引?select的数据列只用从索引中就能够取得,不需要回表进行二次查询,也就是说查询列要被所使用的索引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引。对于使用了覆盖索引的查询,在查询前面使用explain,输出的extra列会显示为using index。比如user_like 用户点赞表,组合索引为(user_id, blog_id),user_id和blog_id都不为null。explain select blog_id from user_like where user_id = 13;explain结果的Extra列为Using index,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,不需要回表查询数据。explain select user_id from user_like where blog_id = 1;explain结果的Extra列为Using where; Using index, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。索引的设计原则?索引列的区分度越高,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。利用最左前缀原则。索引什么时候会失效?导致索引失效的情况:对于组合索引,不是使用组合索引最左边的字段,则不会使用索引以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效判断索引列是否不等于某个值时对索引列进行运算查询条件使用or连接,也会导致索引失效什么是前缀索引?有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。创建前缀索引的关键在于选择足够长的前缀以保证较高的索引选择性。索引选择性越高查询效率就越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的数据行。建立前缀索引的方式:// email列创建前缀索引 ALTER TABLE table_name ADD KEY(column_name(prefix_length));常见的存储引擎有哪些?MySQL中常用的四种存储引擎分别是: MyISAM、InnoDB、MEMORY、ARCHIVE。MySQL 5.5版本后默认的存储引擎为InnoDB。InnoDB存储引擎InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。优点:支持事务和崩溃修复能力;引入了行级锁和外键约束。缺点:占用的数据空间相对较大。适用场景:需要事务支持,并且有较高的并发读写频率。MyISAM存储引擎数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。优点:访问速度快。缺点:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。适用场景:对事务完整性没有要求;表的数据都会只读的。MEMORY存储引擎MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。优点:访问速度较快。缺点:哈希索引数据不是按照索引值顺序存储,无法用于排序。不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。只支持等值比较,不支持范围查询。当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的行。ARCHIVE存储引擎ARCHIVE存储引擎非常适合存储大量独立的、作为历史记录的数据。ARCHIVE提供了压缩功能,拥有高效的插入速度,但是这种引擎不支持索引,所以查询性能较差。MyISAM和InnoDB的区别?是否支持行级锁 : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。是否支持事务和崩溃后的安全恢复: MyISAM 不提供事务支持。而InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。是否支持外键: MyISAM不支持,而InnoDB支持。是否支持MVCC :MyISAM不支持,InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效。MyISAM不支持聚集索引,InnoDB支持聚集索引。MVCC 实现原理?MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。MVCC 实现原理如下:MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。DB_TRX_ID:当前事务id,通过事务id的大小判断事务的时间顺序。DB_ROLL_PRT:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链。DB_ROLL_ID:主键,如果数据表没有主键,InnoDB会自动生成主键。每条表记录大概是这样的:使用事务更新行记录的时候,就会生成版本链,执行过程如下:用排他锁锁住该行;将该行原本的值拷贝到undo log,作为旧版本用于回滚;修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链。下面举个例子方便大家理解。1、初始数据如下,其中DB_ROW_ID和DB_ROLL_PTR为空。2、事务A对该行数据做了修改,将age修改为12,效果如下:3、之后事务B也对该行记录做了修改,将age修改为8,效果如下:4、此时undo log有两行记录,并且通过回滚指针连在一起。接下来了解下read view的概念。read view可以理解成将数据在每个时刻的状态拍成“照片”记录下来。在获取某时刻t的数据时,到t时间点拍的“照片”上取数据。在read view内部维护一个活跃事务链表,表示生成read view的时候还在活跃的事务。这个链表包含在创建read view之前还未提交的事务,不包含创建read view之后提交的事务。不同隔离级别创建read view的时机不同。read committed:每次执行select都会创建新的read_view,保证能读取到其他事务已经提交的修改。repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即可重复读。read view的记录筛选方式前提:DATA_TRX_ID 表示每个数据行的最新的事务ID;up_limit_id表示当前快照中的最先开始的事务;low_limit_id表示当前快照中的最慢开始的事务,即最后一个事务。如果DATA_TRX_ID < up_limit_id:说明在创建read view时,修改该数据行的事务已提交,该版本的记录可被当前事务读取到。如果DATA_TRX_ID >= low_limit_id:说明当前版本的记录的事务是在创建read view之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的记录对当前事务的可见性。如果up_limit_id <= DATA_TRX_ID < low_limit_i:需要在活跃事务链表中查找是否存在ID为DATA_TRX_ID的值的事务。如果存在,因为在活跃事务链表中的事务是未提交的,所以该记录是不可见的。此时需要通过版本链找到上一个版本,然后重新判断该版本的可见性。如果不存在,说明事务trx_id 已经提交了,这行记录是可见的。总结:InnoDB 的MVCC是通过 read view 和版本链实现的,版本链保存有历史版本记录,通过read view 判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。快照读和当前读表记录有两种读取方式。快照读:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加锁。当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。下面举个例子说明下:1、首先,user表只有两条记录,具体如下:2、事务a和事务b同时开启事务start transaction;3、事务a插入数据然后提交;insert into user(user_name, user_password, user_mail, user_state) values('tyson', 'a', 'a', 0);4、事务b执行全表的update;update user set user_name = 'a';5、事务b然后执行查询,查到了事务a中插入的数据。(下图左边是事务b,右边是事务a。事务开始之前只有两条记录,事务a插入一条数据之后,事务b查询出来是三条数据)以上就是当前读出现的幻读现象。那么MySQL是如何避免幻读?在快照读情况下,MySQL通过mvcc来避免幻读。在当前读情况下,MySQL通过next-key来避免幻读(加行锁和间隙锁来实现的)。next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。Serializable隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不会使用。共享锁和排他锁SELECT 的读取锁定主要分为两种方式:共享锁和排他锁。select * from table where id<6 lock in share mode;--共享锁 select * from table where id<6 for update;--排他锁这两种方式主要的不同在于LOCK IN SHARE MODE 多个事务同时更新同一个表单时很容易造成死锁。申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。SELECT... FOR UPDATE 使用注意事项:for update 仅适用于innodb,且必须在事务范围内才能生效。根据主键进行查询,查询条件为like或者不等于,主键字段产生表锁。根据非索引字段进行查询,会产生表锁。大表怎么优化?某个表有近千万数据,查询比较慢,如何优化?当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内;读写分离: 经典的数据库拆分方案,主库负责写,从库负责读;通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。bin log/redo log/undo logMySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是 bin log(二进制日志)和 redo log(重做日志)和 undo log(回滚日志)。bin logbin log是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。redo logredo log是innodb引擎级别,用来记录innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,innoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数innodb_flush_log_at_tx_commit设置为1,那么在执行commit时会将redo log同步写到磁盘。undo log除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。bin log和redo log有什么区别?bin log会记录所有日志记录,包括InnoDB、MyISAM等存储引擎的日志;redo log只记录innoDB自身的事务日志。bin log只在事务提交前写入到磁盘,一个事务只写一次;而在事务进行过程,会有redo log不断写入磁盘。bin log是逻辑日志,记录的是SQL语句的原始逻辑;redo log是物理日志,记录的是在某个数据页上做了什么修改。讲一下MySQL架构?MySQL主要分为 Server 层和存储引擎层:Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。存储引擎: 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。Server 层基本组件连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql 是否执行过,如果有缓存这个 sql,就会直接返回给客户端,如果没有命中,就会执行后续的操作。分析器: 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。分库分表当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。数据切分可以分为两种方式:垂直划分和水平划分。垂直划分垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。缺点:主键出现冗余,需要管理冗余列;会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;依然存在单表数据量过大的问题。水平划分水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。缺点:分片事务一致性难以解决跨节点join性能差,逻辑复杂数据分片在扩容时需要迁移什么是分区表?分区表是一个独立的逻辑表,但是底层由多个物理子表组成。当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。分区表类型按照范围分区。CREATE TABLE test_range_partition( id INT auto_increment, createdate DATETIME, primary key (id,createdate) ) PARTITION BY RANGE (TO_DAYS(createdate) ) ( PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ), PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ), PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ), PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ), PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ), PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ), PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ), PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ), PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ), PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ), PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ), PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') ) );在/var/lib/mysql/data/可以找到对应的数据文件,每个分区表都有一个使用#分隔命名的表文件: -rw-r----- 1 MySQL MySQL 65 Mar 14 21:47 db.opt -rw-r----- 1 MySQL MySQL 8598 Mar 14 21:50 test_range_partition.frm -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201801.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201802.ibd -rw-r----- 1 MySQL MySQL 98304 Mar 14 21:50 test_range_partition#P#p201803.ibd ...list分区对于List分区,分区字段必须是已知的,如果插入的字段不在分区时枚举值中,将无法插入。create table test_list_partiotion ( id int auto_increment, data_type tinyint, primary key(id,data_type) )partition by list(data_type) ( partition p0 values in (0,1,2,3,4,5,6), partition p1 values in (7,8,9,10,11,12), partition p2 values in (13,14,15,16,17) );hash分区可以将数据均匀地分布到预先定义的分区中。create table test_hash_partiotion ( id int auto_increment, create_date datetime, primary key(id,create_date) )partition by hash(year(create_date)) partitions 10;查询语句执行流程?查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。举个例子,查询语句如下:select * from user where id > 1 and name = '大彬';首先检查权限,没有权限则返回错误;MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;两种执行方案,先查 id > 1 还是 name = '大彬',优化器根据自己的优化算法选择执行效率最好的方案;校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。更新语句执行过程?更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo log(prepare状态)、binlog、redo log(commit状态)举个例子,更新语句如下:update user set name = '大彬' where id = 1;先查询到 id 为1的记录,有缓存会使用缓存。拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录redo log,此时redo log进入 prepare状态。执行器收到通知后记录binlog,然后调用引擎接口,提交redo log为commit状态。更新完成。为什么记录完redo log,不直接提交,而是先进入prepare状态?假设先写redo log直接提交,然后写binlog,写完redo log后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候binlog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。exist和in的区别?exists用于对外表记录做筛选。exists会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。select a.* from A awhere exists(select 1 from B b where a.id=b.id)in是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。select * from Awhere id in(select id from B)子查询的表比较大的时候,使用exists可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in可以有效减少对外查询表循环遍历来提升速度。truncate、delete与drop区别?相同点:truncate和不带where子句的delete、以及drop都会删除表内的数据。drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。不同点:truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;一般来说,执行速度: drop > truncate > delete。having和where的区别?二者作用的对象不同,where子句作用于表和视图,having作用于组。where在数据分组前进行过滤,having在数据分组后进行过滤。什么是MySQL主从同步?主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。为什么要做主从同步?读写分离,使数据库能支撑更大的并发。在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。数据备份,保证数据的安全。乐观锁和悲观锁是什么?数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。悲观锁:假定会发生并发冲突,在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加version字段,在修改提交之前检查version与原来取到的version值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS算法实现。用过processlist吗?show processlist 或 show full processlist 可以查看当前 MySQL 是否有压力,正在运行的SQL,有没有慢SQL正在执行。返回参数如下:id:线程ID,可以用kill id杀死某个线程db:数据库名称user:数据库用户host:数据库实例的IPcommand:当前执行的命令,比如Sleep,Query,Connect 等time:消耗时间,单位秒state:执行状态,主要有以下状态:Sleep,线程正在等待客户端发送新的请求Locked,线程正在等待锁Sending data,正在处理SELECT查询的记录,同时把结果发送给客户端Kill,正在执行kill语句,杀死指定线程Connect,一个从节点连上了主节点Quit,线程正在退出Sorting for group,正在为GROUP BY做排序Sorting for order,正在为ORDER BY做排序info:正在执行的SQL语句
2022年06月
2022年03月
2022年02月
2022年01月