引言
众所周知,软件系统有三高:**高并发、高性能、高可用。**三者既有区别也有联系,门门道道很多,全面讨论可以大战三天三夜。
高并发对于Java开发者来说都不陌生,每年天猫双十一,秒杀大促等场景阿里都稳稳的扛住了如此大的并发量,因此说,阿里在这方面也有绝对的话语权。
可以从阿里等其他互联网大厂的招聘要求上看到,有高并发开发经验优先考虑。因此,Java并发问题一直是各个大厂面试的重点之一。很多程序员每天忙着搬砖,平时接触不到高并发,哪天受不了跑去面试,还常常会被面试官犀利的高并发问题直接KO。
我们都知道高并发的基础是并发编程,而阿里新推出的这份《新高并发宝典》层层深入,形成了一个很好的知识系统,让你在应对面试官的时候完全不慌,所以今天我们就一起来学习一下阿里的高并发架构吧。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这7个阶段.其中其中验证、准备、解析3个部分统称为连接.
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
注意,这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载:查找并加载类的二进制数据
在加载阶段,虚拟机需要完成以下3件事情:
- 1)通过一个类的全限定名来获取定义此类的二进制字节流。
- 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以
0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 - 元数据验证:对字节码描述的信息进行语义分析(注意: 对比
javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object
之外。 - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
该阶段的注意事项:
- 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如
0
、0L
、null
、false
等),而不是被在Java代码中被显式地赋予的值。
比如:假设一个类变量的定义为: public static int value = 3
;那么变量value在准备阶段过后的初始值为0
,而不是3
,因为这时候尚未开始执行任何Java方法,而把value赋值为3的put static
指令是在程序编译后,存放于类构造器()
方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
-
对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
-
对于同时被
static
和final
修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 -
对于引用数据类型
reference
来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null
。 -
如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
-
如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。假设上面的类变量value被定义为:
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final
常量在编译期就将其结果放入了调用它的类的常量池中
解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类
或接口
、字段
、类方法
、接口方法
、方法类型
、方法句柄
和调用点
限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用
就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化:对类的静态变量,静态代码块执行初始化操作
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
类初始化的步骤
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
触发类初始化的时机
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
-
使用new关键字实例化对象的时候。
-
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
-
调用一个类型的静态方法的时候。
-
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
-
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
以下几种情况不会执行类初始化
-
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
-
定义对象数组,不会触发该类的初始化。
-
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类。
-
通过类名获取 Class 对象,不会触发类的初始化。
-
通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初 始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
-
通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
使用
类访问方法区内的数据结构的接口, 对象是Heap区的数据。
卸载
Java虚拟机将结束生命周期的几种情况
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
类加载器
什么是类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。 实现这个动作的代码模块称为“类加载器”。
类加载器的层次
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器:
启动类加载器(Bootstrap ClassLoader)
这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
扩展类加载器(Extension ClassLoader)
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。
它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
最后
为什么我不完全主张自学?
①平台上的大牛基本上都有很多年的工作经验了,你有没有想过之前行业的门槛是什么样的,现在行业门槛是什么样的?以前企业对于程序员能力要求没有这么高,甚至十多年前你只要会写个“Hello World”,你都可以入门这个行业,所以以前要入门是完全可以入门的。
②现在也有一些优秀的年轻大牛,他们或许也是自学成才,但是他们一定是具备优秀的学习能力,优秀的自我管理能力(时间管理,静心坚持等方面)以及善于发现问题并总结问题。
如果说你认为你的目标十分明确,能做到第②点所说的几个点,以目前的市场来看,你才真正的适合去自学。
除此之外,对于绝大部分人来说,报班一定是最好的一种快速成长的方式。但是有个问题,现在市场上的培训机构质量参差不齐,如果你没有找准一个好的培训班,完全是浪费精力,时间以及金钱,这个需要自己去甄别选择。
我个人建议线上比线下的性价比更高,线下培训价格基本上没2W是下不来的,线上教育现在比较成熟了,此次疫情期间,学生基本上都感受过线上的学习模式。相比线下而言,线上的优势以我的了解主要是以下几个方面:
①价格:线上的价格基本上是线下的一半;
②老师:相对而言线上教育的师资力量比线下更强大也更加丰富,资源更好协调;
③时间:学习时间相对而言更自由,不用裸辞学习,适合边学边工作,降低生活压力;
④课程:从课程内容来说,确实要比线下讲的更加深入。
应该学哪些技术才能达到企业的要求?(下图总结)
应该学哪些技术才能达到企业的要求?(下图总结)*
[外链图片转存中…(img-x2tOn3d1-1624444120528)]
[外链图片转存中…(img-Avamedja-1624444120529)]