前言
今天我们来说说Redis为什么高性能?如何做高可用?
- Redis是单线程的,避免了多线程的上下文切换和并发控制开销;
- Redis大部分操作时基于内存,读写数据不需要磁盘I/O,所以速度非常快;
- Redis采用了I/O多路复用机制,提高了网络I/O并发性;
- Redis提供高效的数据结构,如跳跃表、哈希表等;
一、Java 虚拟机架构 (JVM Architecture)
在我看来,不管学习什么样的知识或技术,首先要做的就是从全局上去认识它,这样才能避免盲人摸象,事倍功半的情况发生。既然要学习 JVM,就要先了解它的整体架构,于是我画了个 JVM 架构图来帮助大家认识它。
对 JVM 还不太了解的同学第一次看到这张花里胡哨的图肯定会一脸懵逼,不用怕,其实我们只需要重点理解并掌握其中一部分 (同时也是面试重点) 就好了,比如运行时数据区、垃圾收集器、内存分配策略和类加载机制等,类文件结构也可以学习一下,其他的稍作了解即可。既然本篇文章是要带领大家认识 JVM 架构的,那就先把图中各个部分都介绍一下吧 (注:本文只做介绍,让各位先对 JVM 有个整体的认识,后续会做深入探讨)。
1.1 Class 文件 (字节码文件)
Java 之所以号称“一次编写,处处运行”,就是得益于虚拟机和 Class 文件 (注:CLass 文件、字节码文件和类文件是一个意思) 的组合机制。程序员并不需要自己去适配不同的操作系统,大家都知道我们平时编写的 java 代码在编译成 Class 文件后才能执行,而 Class 文件可以在任何操作系统上的 JVM 上执行,这样就做到了“平台无关性”。下面是一个最简单的 HelloWorld 程序及其对应的 Class 文件。
得益于 Class 文件,JVM 还可以做到“语言无关性”,也就是说不只有 Java 程序可以运行于 JVM 之上,很多其他语言例如最近在安卓开发者中大火的 Kotlin 语言,还有 Scala、Groovy 等语言也都是基于 JVM 平台的,这些语言的代码都可以编译成 Class 文件,然后在 JVM 上运行。
1.2 类加载器子系统 (ClassLoader Subsystem)
要执行 Class 文件就需要先将其加载进内存,这一工作正是由类加载器 (ClassLoader) 完成的,系统为我们提供了三种类加载器,分别是启动类加载器 (Bootstrap ClassLoader)、扩展类加载器 (Extension ClassLoader) 和应用程序类加载器 (Application ClassLoader),如果有必要,我们也可以加入自定义的类加载器。类加载过程如下:
类加载过程分为加载、连接和初始化三个阶段,其中的连接阶段又分为验证、准备和解析三个阶段 (详细的类加载机制在后续文章中进行介绍)。
1.3 Java 虚拟机运行时数据区 (JVM Runtime Data Area)
这部分内容较多,放在本文第二部分单独进行介绍。
1.4 执行引擎 (Execution Engine)
字节码被加载进运行时数据区后,执行引擎会进行读取并执行,执行引擎主要包含以下模块:
- 解释器 (Interpreter):相信大家很久以前就听过“计算机只认识0和1”这句话,时至今日,计算机依然只认识0和1,所以任何编程语言的代码最终都要转化成机器码 (二进制代码)才能执行,Java 也不例外,而解释器的工作正是将编译得到的字节码再转化成机器码,然后才能执行。正因为如此,Java 才被称为解释型语言,也正是因为边解释边执行的特点,Java 程序在执行时才会慢于 C++ 之类的编译型语言。
- 即时编译器 ,为了弥补解释执行带来的速度劣势,JVM 引入了即时编译器,它的作用就是把热点代码,比如重复调用的方法和循环代码等,编译成机器码并存放在 code cache 中,这样之后再用到这些代码就不用重新解释执行了,可以提高程序运行效率。
- 垃圾收集器 (Garbage Collector):Java 程序员可以不用手动释放内存,全是垃圾收集器的功劳,这也是 JVM 中尤其重要的内容,后续会有多篇文章对其进行介绍。
1.5 本地库接口 (JNI,Java Native Interface)
如果你经常看 JDK 源码的话,一定会注意到 native 这个关键词,被它修饰的方法是没有方法体的,是因为它调用了计算机本地的方法库 (通常是 C 或 C++ 代码)。JDK 源码中有很多类的方法,特别是一些需要操作计算机硬件的方法,都调用了本地方法库,毕竟与硬件打交道还是用 C 和 C++ 更方便,比如下面这些方法:
// 例一:这是 Thread 类中的 currentThread 方法,用于获取当前正在执行的线程
public static native Thread currentThread();
// 例二:这是 FileInputStream 类中 open0 方法,用于打开指定文件
private native void open0(String name) throws FileNotFoundException;
1.6 本地方法库 (Native Method Library)
本地库接口所调用的对象正是位于这个库中,一般是位于计算机本地的 C 或 C++ 语言代码。
二、Java 虚拟机运行时数据区
Java 虚拟机运行时数据区是我们需要重点了解并熟悉的部分,因为这与我们写的程序息息相关,平时常见的 StackOverflowError 和 OutOfMemoryError 也几乎都是来自这个区域。说“几乎”是因为当本机直接内存不够用时也会抛出 OutOfMemoryError。如下图所示,程序计数器、Java 虚拟机栈和本地方法栈是线程私有的,堆和方法区是线程共享的,其中方法区又包含了运行时常量池。下面就对这个部分做个详细的介绍吧 (注:本部分引用内容来自《深入理解Java虚拟机》)。
2.1 程序计数器 (Program Counter Register)
怕有些小伙伴不清楚,提示一下:下面这样的段落格式就是 Markdown 里的引用格式,,一般用于引用他人的文章或别处的内容。
这里引用了《深入理解Java虚拟机》书中的内容,其实不难理解,程序计数器的作用就是保存线程的执行状态,引用部分的第三段中说“如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址”,这个地址就是字节码执行到的位置。我们平时说的 Java 多线程上下文切换就需要程序计数器的辅助,当 CPU 从一个线程切换到另一个线程时,要从程序计数器中读取线程执行状态从而恢复现场。后面又说“如果执行的是本地 (Native)方法,这个计数器值为空(Undefined)”,这是为何呢?是因为本地方法执行的是 C / C++ 代码,在原生平台直接运行,也就不存在 Java 虚拟机的概念,自然也无法保存字节码指令地址,此时要想记录代码运行状态的话,只能使用原生 CPU 的 PC 寄存器。
2.2 Java 虚拟机栈 (JVM Stacks)
Java 虚拟机栈的内部结构如下图所示:
2.2.1 局部变量表
局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。
2.2.2 操作数栈
操作数栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。下面使用 i++ 和 ++i 的区别来帮助理解操作数栈:
i++ 和 ++i 的区别:
- i++:从局部变量表取出 i 并压入操作栈,然后对局部变量表中的 i 自增 1,将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,如此线程从操作栈读到的是自增之前的值。
- ++i:先对局部变量表的 i 自增 1,然后取出并压入操作栈,再将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,线程从操作栈读到的是自增之后的值。
之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。
2.2.3 动态连接
每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。
2.2.4 方法出口
方法执行时有两种退出情况:
- 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
- 异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
- 返回值压入上层调用栈帧。
- 异常信息抛给能够处理的栈帧。
- 程序计数器指向方法调用后的下一条指令。
2.3 本地方法栈 (Native Method Stacks)
这部分比较好理解,就不做解析了。
2.4 Java 堆 (Heap)
Java 堆的唯一作用就是存放对象实例,这也是垃圾收集器最关注的内存区域,因为大多数对象实例的存活时间都很短,比如在方法内部创建的实例在方法执行完之后就没有存在价值了,所以这个区域的垃圾回收性价比最高。关于垃圾回收的详细内容,见后续文章。
2.5 方法区 (Method Area)
这部分引用内容对方法区的介绍十分全面,切记不要将方法区和永久代混为一谈,从JDK 8 以后已经没有永久代的概念了。
2.6 运行时常量池 (Runtime Constant Pool)
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
最后的内容
在开头跟大家分享的时候我就说,面试我是没有做好准备的,全靠平时的积累,确实有点临时抱佛脚了,以至于我自己还是挺懊恼的。(准备好了或许可以拿个40k,没做准备只有30k+,你们懂那种感觉吗)
如何准备面试?
1、前期铺垫(技术沉积)
程序员面试其实是对于技术的一次摸底考试,你的技术牛逼,那你就是大爷。大厂对于技术的要求主要体现在:基础,原理,深入研究源码,广度,实战五个方面,也只有将原理理论结合实战才能把技术点吃透。
下面是我会看的一些资料笔记,希望能帮助大家由浅入深,由点到面的学习Java,应对大厂面试官的灵魂追问
- Java程序员必看《Java开发核心笔记(华山版)》
- Redis学习笔记
- Java并发编程学习笔记
四部分,详细拆分并发编程——并发编程+模式篇+应用篇+原理篇
- Java程序员必看书籍《深入理解 ava虚拟机第3版》(pdf版)
- 大厂面试必问——数据结构与算法汇集笔记
其他像Spring,SpringBoot,SpringCloud,SpringCloudAlibaba,Dubbo,Zookeeper,Kafka,RocketMQ,RabbitMQ,Netty,MySQL,Docker,K8s等等我都整理好,这里就不一一展示了。
2、狂刷面试题
技术主要是体现在平时的积累实用,面试前准备两个月的时间再好好复习一遍,紧接着就可以刷面试题了,下面这些面试题都是小编精心整理的,贴给大家看看。
①大厂高频45道笔试题(智商题)
②BAT大厂面试总结(部分内容截图)
③面试总结
3、结合实际,修改简历
程序员的简历一定要多下一些功夫,尤其是对一些字眼要再三斟酌,如“精通、熟悉、了解”这三者的区别一定要区分清楚,否则就是在给自己挖坑了。当然不会包装,我可以将我的简历给你参考参考,如果还不够,那下面这些简历模板任你挑选:
以上分享,希望大家可以在金三银四跳槽季找到一份好工作,但千万也记住,技术一定是平时工作种累计或者自学(或报班跟着老师学)通过实战累计的,千万不要临时抱佛脚。
另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。
考参考,如果还不够,那下面这些简历模板任你挑选:
[外链图片转存中…(img-paPv8Utl-1649393823809)]
以上分享,希望大家可以在金三银四跳槽季找到一份好工作,但千万也记住,技术一定是平时工作种累计或者自学(或报班跟着老师学)通过实战累计的,千万不要临时抱佛脚。
另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。