如今的分布式系统越来越复杂,刚好目前我所负责的模块也是微服务架构,所以说深入学习分布式系统非常有必要。
系统间通信
换个角度看待BIO、NIO和AIO
之前在学习网络通信的时候接触过Java基于计算机网络TCP/IP协议和UDP协议的常用类和API,然而停留在浅尝辄止的阶段,这本书一开头我觉得我很感兴趣的原因就是因为它提供了使用的场景Demo,下面就让我们借着这些demo来复习之前的API吧,首先需要明确的是NIO和AIO都依赖于系统底层,不同的操作系其实现方式也是不同的。
TCP/IP+BIO
还记得这里使用的类是什么吗
TCP/IP+NIO
NIO对应的Java常用的API
TCP/IP+AIO
这块儿书上并没有给出例子
使用框架进行系统间通信
几种通信方式
我读过之后有印象的就是WebService,RPC这两种
承接上面的jdk提供的通信方式,采用框架来帮助我们更加便捷地构建通信,书上的案例是mina,说实话这个框架有点老(这本书是2010年出版的),还是Netty香
关于SOA
这个是行业内提出的一种规范,那么肯定是有对应的具体实现的,好像是叫SCA
分布式应用中的JVM
Java代码的执行机制
类编译机制
下面这部分并非面试的重点,但是对于开拓视野以及向下深度还是非常重要的
1.Parse and Enter
parse词法分析和语法分析 enter过程将符号输入到符号表,通常包括类的超类型和接口 、根据需要添加构造器 、将类中出现的符号输入类自身的符号表(不知道这玩意儿是干啥的)中等
2.注解处理(Annotation Processing)
JSR 269的支持下,在Annotation Processing进行后(这里具体干了什么???),再次进入Parse and Enter步骤
3.语义分析和生成class文件
这块儿是大头,大量的工作在这里进行,不必说生成class文件肯定更加重要 语义分析
- 将语法分析树的各个变量表达式进行联系 int a=1
- 检查变量是否声明
- 推导泛型方法的类型参数
- 检查类型匹配性
- 进行常量折叠
- 检查所有语句都可到达
- 检查所有checked exception... 生成class
- 将所有的实例变量收集到构造器中,具体不知道长什么样子
- 将所有的类的静态变量收集到()中,我有点印象但是不多
- 进行少量的代码转换,例如String ret="a"+"b"转换为StringBuilder,看来是编译期间帮开发人员干了好多事儿,而我居然不知道
类加载机制
类加载机制是指将.class文件加载到JVM,并形成Class对象的机制
这一块儿也是面试常问的重点 JVM将类加载过程划分为三个步骤:装载、链接和初始化。装载和链接过程完成之后,即将二进制的字节码转换为Class对象 ;初始化过程不是加载类时必须触发 的,但是最迟必须在初次主动使用对象前执行,其所作的动作为静态变量赋值、调用<clint>()(构造方法)等。
1.装载(Load)
找到二进制字节码并加载至JVM中 ,JVM通过类的全限定名+加载器实例完成类的加载
对于接口和非数组类型的类,其名称即为类名 而对于数组型的类,其名称就是[+(基本类型或L+引用类型类名),例如byte[] bytes=new byte[512],bytes的类名就是[B;Object[] objects=new Object[10],objectss的类名为[Ljava.lang.Object;型类中的元素类型由所在的ClassLoader负责加载,但是数组类则由JVM直接创建。
2.链接(Link)--重点
这块儿之前看的视频里画的图,这一步的细节挺多的
检验 (校验,校验过程中如果遇到其他的接口和类,则会对其进行加载,如果记载失败则会抛出NoClassDeFoundError)->准备 (初始化类中的静态变量(private int a=1)和静态代码块???不是静态代码块初始化是在初始化这个阶段)->解析(要调用的接口和类,同时在这个阶段对类中的所有属性、方法进行验证,比如说确保调用的属性、方法存在,以及具备相应的权限(private、public)),这里抛出的异常有NoSuchMethodError、NoSuchFieldError等
3.初始化(initialize)--重点
同样,初始化在整个加载流程肯定也是重点 执行类中的静态代码块,构造器以及静态属性(static int a=1,注意和上面的静态变量有区别) 。
以下4种情况初始化过程会被触发(最迟使用时会触发)
- 调用了new
- 反射调用了类种的方法
- 子类调用了初始化
- JVM启动过程种指定的初始化类
初始化之前必须完成链接过程给中的校验和准备,而解析阶段不做强制要求
JVM的类加载是通过ClassLoader及其子类来完成,从下面的图片可以看出分为Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader及User-Defined ClassLoader。
- Boostrap ClassLoader
代码中拿不到这个对象,sunJDK启动时会初始化此ClassLoader,并由ClassLoader完成$JAVA_HOME中jre/lib/rt.jar中所有的class加载。 - Extension ClassLoader
用来加载一些扩展到类,例如sunJDK下目录中的dns的工具jar包 - System ClassLoader
JVM用此来加载启动参数中指定的classpath中的jar包及目录(我们自己写的代码就是在这一层) - User-Defined ClassLoader
用户自定义类加载器
类加载的顺序是从根上先加载,如果能够加载到的话那么以parent的为主
那么这个User-Defined有什么作用,书上的说的是什么类隔离的需求(还真没遇到这样的场景,具体被问到再说吧)
ClassLoader常用API
- loadClass
加载指定名字的类 - findLoadedClass 负责从当前ClassLoader实例对象中的缓存中寻找已经加载的类,调用的为native方法
- findClass
此方法直接抛出ClassNotFoundException,因此要通过覆盖1.loadClass或者这个方法来以自定义的方式加载相应的类 - defineClass
此方法负责将二进制的字节码转换为Class对象 5.resloveClass 负责对象的链接,如果链接过则会直接返回
ClassLoader常见异常
- ClassNotFound
- NoClassDeFoundError
没有找到被加载类中引用的类 - LinkageError
自定义ClassLoader的情况下容易出现
类执行机制
在类编译机制 过程中将源码编译成为JVM字节码,JVM字节码是一种中间代码 的方式,要由JVM在运行期对其进行解释并执行,这种方式称为字节码解释执行方式
字节码解释执行
JVM常见指令
- invokestatic
调用静态方法 - invokevirtual
调用对象实例方法 - invokeinterface
调用接口方法 - invokespecial
调用private方法和编译源码后生成的<init>方法
SunJDK基于栈的体系结构来执行字节码,基于栈方式的好处为代码紧凑,体积小
书上大概描述了JVM内存的概念,但是没有深入理解JVM这种传统的讲得好,作为了解就可以
- 指令解释执行
上面的指令一条条生成之后如何执行呢? 执行方式为经典冯诺依曼体系中的FDX循环方式,即获取下一条指令,解码并分派,知道有这么个事儿就可以 - 栈顶缓存 将栈顶的值直接缓存在寄存器上,对于大部分只需要一个值的操作而言,无须将数据放入操作数栈
- 部分栈帧共享 顾名思义,就是A方法调用B方法时,B方法将A方法作为当前方法的局部变量(理解不了为什么这么做)
另一条路-字节编译执行
上面的解释执行效率太低,所以SunJDKC提供将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器(前面提到的支持)。
SunJDK在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释的方式
因此SunJDK又称为HotspotVM(热点虚拟机),而且在编译中又有两种模式:client compiler(-client客户端)和server complier(-server服务器)
client优化相对细一些,而server模式从全局出发,采用的时传统的图着色寄存器分配算法,C2在运行时会收集程序的运行信息
逃逸分析 (在Java并发编程实战中也有提及到)是C2进行很多优化的基础,逃逸分析是指根据运行状况来判断方法中的变量是否会被外部读取 ,如不会则认为此变量是逃逸的,基于逃逸分析C2在编译时会做标量替换、栈上分配和同步削除等优化。
- 标量替换
java
Point point=new Point(1,2);
System.out.println("point.x="+point.x+";point.y"+point.y);
标量(基本类型变量)替换聚合量(类)
- 栈上分配
上面的例子中,如果p没有逃逸,那么就会直接在栈上创建Point对象实例,而不是在JVM堆上。 3. 同步削除 这个贼好理解,就是在对代码块加锁以后发现代码块中的变量逃逸,基于此那么就没有加锁的必要,即去掉同步
默认情况下,SunJDK根据机器配置来选择client或server模式,当机器配置CPU超过2核且内存超过2GB即默认为server模式,但是WIN都是cient模式
反射执行
反射执行时Java的亮点之一,基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等
qa:反射调用中创建的过程、方法调用是动态的这句话中的动态怎么理解?
tips:基于扁编译角度,直接会调用和反射生成的字节码有何区别(发射编译之后生成的是JVM反射实现的字节码),想想idea中直接创建和反射会不会报错(类名称有问题时)
JVM内存管理
内存空间
这里JVM规范(逻辑),正好复习一下
老生常谈的几个部分
方法区
这里重点是理清方法区和元空间的关系(逻辑概念与实现)
本地方法栈
即执行操作系统本地方法的栈帧
JVM方法栈和PC寄存器
JVM方法栈和虚拟机栈(后者JVM的规范,也就是说各个厂商的JVM需要遵守这个规范,而SunJDK的空间划分实现中有这么个方法栈)
PC寄存器(SunJDK实现程序计数器???)使用的是CPU寄存器或者说操作系统内存,而JVM方法栈(我的理解是类似于本地方法栈)使用的是CPU内存,每个线程私有
堆
分为新生代和旧生代(也叫做老年代),顾名思义,新生代存放堆中新产生的对象而old generation用来存放多次GC之后仍然存活的对象,其中新生代又分为Eden、S0和S1,为什么要分为Eden、S0和S1呢???
内存回收
收集器
- 引用计数收集器
为什么不用???
每次赋值时都会进行计数器值的更新,开销大;循环引用的情况无法处理 - 跟踪收集器
全局的,集中式的管理方式,并且基于一定条件触发(定时、内存不足等)
三种实现算法- 复制(Copying)
先扫描再移动,留出一整块未使用的空间,坏处移动开销,适合存活对象少 - 标记-清除(Mark-Sweep)
先标记使用的对象,再清除,最后就是不连续的未使用空间,坏处不连续,适合存活对象多 - 标记-压缩(Mark-Compact)
先标记再清除,坏处成本高,移动开销,适合存活对象多
- 复制(Copying)
新生代下可以使用的GC
- 串行GC(Serial GC)
这里有个重要参数就是SurvivorRatio默认为8, 首先新生代GC分为3个部分,同时还有个在各种博客里面见到的-Xmn参数,这里设置为10时,那么Eden space就是8MB,s0和s1各为1MB,分配内存采用的是空闲指针(free-bump-pointer),指针保持最后一个分配的对象在新生代内存区间的位置,当有新的对象要分配时,检查空闲空间是否足够存放对象,不够的话会触发GC
这里又有一个重要概念,那么就是根集合对象,大致包括静态变量、当前线程栈上引用的对象以及传到本地方法中还没有被释放的对象
这些对象中既有新生代也有旧生代,如果仅仅只是扫描引用到的新生代,那么有的旧生代对象会引用新生代对象,他们也会被一并GC掉,这是个很严重的问题,为了解决这个问题,就要扫描就要旧生代对象
旧生代对象一般大且多,所以每次扫描开销都会很大,为了避免扫描根集合时去扫描旧生代,又出来一个重要概念remeber set ,即记忆Set,SunJDK实现方式为CardTable ,这玩意儿可以理解为根集合对象的扩展 ,在对象进行赋值的时候,这个旧生代对象是不是指向新生代对象,是的话那么就在remeber set里面加上标记 ,在这一步之前会产生一个write barrier(写屏障,将当前写操作信息传递给JVM),创建新对象或更新现有对象的引用时,写屏障机制会在这些写操作发生时,立即将相关信息传递给垃圾收集器。这些信息包括新创建的对象或被更新的对象的地址以及引用关系等信息
完整根集合有了,接下来就是扫描加清除了,为了避免在扫描过程中引用关系发生变化,SunJDK采用了暂停应用的方式,又有一个新概念就是SafePoint,这个玩意儿我们看不到但是SunJDK会为我们来添加,一般在方法中循环的结束点和方法的完毕点,在等待所有用户线程进入SP(???),然后发现要执行Minor GC,则将内存页设置为不可读的状态,从而实现暂停用户线程的执行。
Serial GC采用单线程,复制算法,适用与单CPU、新生代空间较小对暂停时间要求不是很高的应用上,也是client级别(CPU核数小于或者物理内存小于2GB,代码优化) - 并行回收GC-Parallel GC
用的也是Copying算法,但是扫描和复制过程中都是并发执行的,适用于多Cpu和暂停时间少的场景 - 并行GC-ParNew
和上面的区别是必须得配合旧生代Old的CMSGC使用,旧生代GC并发执行的时候可能会触发MinorGC???
旧生代和持久代(方法区)可用的GC
- 串行
使用的基于Mark-Sweep-Compact算法,顾名思义,就是将Mark-sweep和Mark-compact算法结合
从集合开始,使用三色标记法(根对象标记为黑色,引用到根对象的标记为灰色,未使用到的对象标记为白色),遍历整个旧生代或持久代空间,回收未被标记(白色)的对象,接下来就是滑动压缩(Sliding compaction),将存活对象向旧生代空间开始处进行滑动
串行执行和新生代一样,也需要暂停应用
串行是client级别或32位的Windows机器上默认采用的GC方式