diff --git a/README.md b/README.md index 33ab457..cb388d8 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,13 @@ - [2020届秋招面试题总结——多线程篇](/interviews/2020届秋招面试题总结-多线程篇.md) - [2020届秋招面试题总结——Spring篇](/interviews/2020届秋招面试题总结-Spring篇.md) - ## 笔记整理 - [记动态代理](/notes/记动态代理.md) - [记单例模式](/notes/记单例模式.md) - [记BitMap/BitSet](/notes/记BitMap-BitSet.md) -- [记循环依赖](/notes/记循环依赖.md) \ No newline at end of file +- [记循环依赖](/notes/记循环依赖.md) +- [记System.identityHashCode(obj)与obj.hashCode()的关系](/notes/记System.identityHashCode(obj)与obj.hashCode()的关系.md) +- [从字节码角度分析try、catch、finally运行细节](/notes/从字节码角度分析try、catch、finally运行细节.md) +- [Java常量池理解及总结](/notes/Java常量池理解及总结.md) +- [浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) diff --git "a/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" "b/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" new file mode 100644 index 0000000..454b0b0 Binary files /dev/null and "b/image/String345円217円230円351円207円217円345円255円230円345円202円250円344円275円215円347円275円256円.jpg" differ diff --git "a/image/catch345円244円232円344円270円252円345円274円202円345円270円270円346円203円205円345円206円265円.png" "b/image/catch345円244円232円344円270円252円345円274円202円345円270円270円346円203円205円345円206円265円.png" new file mode 100644 index 0000000..7cceeea Binary files /dev/null and "b/image/catch345円244円232円344円270円252円345円274円202円345円270円270円346円203円205円345円206円265円.png" differ diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" index 1549975..986cd58 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JAVA345円237円272円347円241円200円347円257円207円.md" @@ -11,7 +11,7 @@ Java语言提供了八种基本数据类型。六种数据类型(四个整数 - float:4字节,表示范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。 - double:8字节,表示范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。 - boolean:只有true和false两个取值。 -- char:2字节,存储Unicode码,用单引号赋值。 +- char:2字节,存储Unicode码,用单引号赋值。 **2、String类能被继承吗,为什么。** @@ -19,7 +19,7 @@ Java语言提供了八种基本数据类型。六种数据类型(四个整数 ```java public final class String implements java.io.Serializable, Comparable, CharSequence { - // 省略... + // 省略... } ``` @@ -40,7 +40,7 @@ String变量创建后是放入方法区的常量池(或者常量池)中, - 方式二:String str2=new String("123"); -首先"123"是一个常量字符串,因此会先在常量池创建"123"字符串对象,然后在堆中再创建一个字符串对象,将"123"的字符数组复制到堆中新创建的对象字符数组中,因此该方式不仅会在堆中,还会在常量池中创建"123"字符串对象。 +首先"123"是一个常量字符串,因此会先在常量池创建"123"字符串对象,然后在堆中再创建一个字符串对象,将常量池中的"123"字符串复制到堆中新创建的对象字符数组中,因此该方式不仅会在堆中,还会在常量池中创建"123"字符串对象。 - 方式三:String str3="123".intern(); @@ -182,8 +182,8 @@ I/O多路复用是指内核一旦发现进程中指定的一个或者多个IO条 反射获取Class对象有三种方式:使用Class.forName("类路径名称")静态方法。 -* 使用类的.class方法。 -* 使用实例对象的getClass()方法。 +- 使用类的.class方法。 +- 使用实例对象的getClass()方法。 根据Class获取实例对象有两种方式: @@ -192,9 +192,9 @@ I/O多路复用是指内核一旦发现进程中指定的一个或者多个IO条 ```java / /获取构造函数类的对象 -Constroctor constroctor = clazz.getConstructor(String.class,Integer.class); +Constroctor constroctor = clazz.getConstructor(String.class,Integer.class); // 使用构造器对象的newInstance方法初始化对象 -Object obj = constroctor.newInstance("龙哥", 29); +Object obj = constroctor.newInstance("龙哥", 29); ``` **13、反射中,Class.forName 和ClassLoader区别。** @@ -227,9 +227,9 @@ cglib动态代理是继承并重写目标类,所以目标类和方法不能被 **17、final的用途。** - * final修饰的对象不能被修改。 - * final修饰的类不能被继承。 - * final修饰的方法不能被重写(但可以被重载)。 +- final修饰的对象不能被修改。 +- final修饰的类不能被继承。 +- final修饰的方法不能被重写(但可以被重载)。 **18、写出三种单例模式实现。** @@ -290,10 +290,8 @@ OO面向对象编程的设计理念是: - Exception(违例)表示需要捕捉或者需要程序进行处理的异常,他处理的是因为程序设计的瑕疵而引起的问题或者在外的输入等引起的一般性问题,是程序必须处理的,是可预料的异常情况。 - Exception又分为运行时异常,受检查异常。 - - CheckedException(检查性异常)必须在编写代码时,使用try catch捕获(比如:IOException异常)。 - RuntimeException(运行时异常)在代码编写时可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。 @@ -370,9 +368,10 @@ Transient关键字修饰的成员变量在序列化过程中会被自动忽略 **34、二叉树、平衡二叉树、红黑树的性质。** 普通的二叉查找树的性质是: - * 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值; - * 若它的右子树上所有结点的值均大于它的根节点的值; - * 它的左、右子树也分别为二叉排序树。 + +- 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值; +- 若它的右子树上所有结点的值均大于它的根节点的值; +- 它的左、右子树也分别为二叉排序树。 平衡二叉树,又被称为AVL树,它的左右两个子树的高度差不能超过1。平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。 @@ -454,8 +453,8 @@ epoll的优点主要是三个方面:第一是没有最大并发连接的限制 综上: - * 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 - * select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。 +- 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 +- select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。 **38、final修饰的变量是引用不可变,还是引用的对象不能改变。** @@ -516,6 +515,5 @@ public class Test { 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 - - -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JVM347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JVM347円257円207円.md" index d6e156b..9046259 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JVM347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-JVM347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——JVM篇 +# 2020届秋招面试题总结——JVM篇 **1、什么情况下会发生栈内存溢出。** @@ -48,10 +48,10 @@ GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区 jvm参数: -- -Xms:初始堆大小 -- -Xmx:堆最大内存 -- -Xss:栈内存 -- -XX:PermSize 初始永久代内存 +- -Xms:初始堆大小 +- -Xmx:堆最大内存 +- -Xss:栈内存 +- -XX:PermSize 初始永久代内存 - -XX:MaxPermSize 最大永久带内存 **5、你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。** @@ -240,7 +240,7 @@ User user= constructor.newInstance(); 1. 虚拟机遇到new指令,到常量池定位到这个类的符号引用。 2. 检查符号引用代表的类是否被加载、解析、初始化过 ,如果没有的话,则执行相应的类加载过程。 3. 虚拟机为对象分配内存。 根据Java内存是否规整,分别通过"指针碰撞"或"空闲列表"来分配。 -4. 虚拟机将分配到的内存空间都初始化为零值。 +4. 虚拟机将分配到的内存空间都初始化为零值。 5. 虚拟机对对象进行必要的设置。 6. 执行方法,成员变量进行初始化。 @@ -278,7 +278,7 @@ Full GC触发条件: Java虚拟机中类加载的全过程包括:加载、验证、准备、解析和初始化这5个阶段。 -**加载** +**加载:** 在加载阶段,虚拟机主要完成以下3个事情。 @@ -286,7 +286,7 @@ Java虚拟机中类加载的全过程包括:加载、验证、准备、解析 - 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 - 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各类数据的访问入口。 -**验证** +**验证:** 验证时连接阶段的第一步,这一阶段的目的是为了**确保Class文件的字节流中包含的信息符合当前虚拟机的要求**,并且不会危害虚拟机自身的安全。 @@ -294,17 +294,17 @@ Java虚拟机中类加载的全过程包括:加载、验证、准备、解析 - 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范。简单来说就是验证Java语法的正确性。 - 字节码验证:主要验证程序的控制流程,如循环、分支等。 -**准备** +**准备:** 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在**方法区**中分配。需要注意的是,这时候进行内存分配的仅包括**类变量(被static修饰的变量)**,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。 -**解析** +**解析:** 解析阶段是虚拟机在常量池内寻找类、接口、字段和方法的符号引用,并且将这些**符号引用替换为直接引用**的过程。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 -**初始化** +**初始化:** -初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行(),该方法**会为所有的静态变量赋予正确的值**。 +初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行<clinit>(),该方法**会为所有的静态变量赋予正确的值**。 参考文章:[万万没想到,JVM内存结构的面试题可以问的这么难?](https://mp.weixin.qq.com/s?__biz=MzI5NTYwNDQxNA==&mid=2247485214&idx=1&sn=32aa3d83464435188be9ac52c8e9c588&chksm=ec505ecfdb27d7d97897f74b36d28bc67536b55b0f252cb1f5f006b5a3439a2e70965f86a1ff&scene=0&xtrack=1&key=546b3b791faf1f5f02217e88c54f3b221d19b0d2e192b57f4847f7cf607d2c54b426e5e62c83ab9bd93c35a1b850ecf229accf8266fc1ac362a34ae7d687b9cc4a342ed4f54c7809a3e40847fff4160a&ascene=1&uin=MjQ3MzkwMTc2Mw==&devicetype=Windows+10&version=62060844&lang=zh_CN&pass_ticket=W35VcrckR39Y5Fn9My7l/KozGVDszT28Gg6T3fIMrYKZDiAMan7yl4BY759W+Uo+) @@ -400,4 +400,5 @@ a=null; 以上主要参考来源为:《深入理解Java虚拟机:JVM高级特征与最佳实践》 -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-MySQL347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-MySQL347円257円207円.md" index 3330a75..8382c6b 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-MySQL347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-MySQL347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——MySQL篇 +# 2020届秋招面试题总结——MySQL篇 **1、数据库隔离级别有哪些,各自的含义是什么,MySQL默认的隔离级别是多少。** @@ -107,7 +107,7 @@ SQL优化步骤一般是: 除了超时机制外,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测,这是一种更为主动的死锁检测方式,InnoDB存储引擎中也采用这种方式。 -**7、MySQL的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。** +**7、MySQL的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。** MySQL中索引采用的数据结构主要是B+Tree,Hash,平衡二叉树等。 @@ -129,7 +129,7 @@ MySQL中索引采用的数据结构主要是B+Tree,Hash,平衡二叉树等 **8、聚集索引和非聚集索引的区别。** -非聚集索引也称之为辅助索引。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的数据。聚集索引叶子节点存放的即为整张表的行记录数据;而辅助索引叶子节点除了包含键值以外,还包含了一个书签(bookmark),该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此**InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。**所以,聚集索引一般比辅助索引体积大。 +非聚集索引也称之为辅助索引。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的数据。聚集索引叶子节点存放的即为整张表的行记录数据;而辅助索引叶子节点除了包含键值以外,还包含了一个书签(bookmark),该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此**InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。** 所以,聚集索引一般比辅助索引体积大。 由于实际的数据页只能按照一颗B+树进行排序,因此**每张表只能拥有一个聚集索引**。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上只能找到数据。聚集索引的好处在于,它对于主键的排序查找和范围查找速度都非常快,叶子节点的数据就是用户所要查询的数据。需要注意的是: @@ -137,7 +137,7 @@ MySQL中索引采用的数据结构主要是B+Tree,Hash,平衡二叉树等 - 如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引。 - 如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,改列的值会随着数据的插入自增。 -另外,切记的是,**聚集索引的存储并不是物理上连续的,而是逻辑上连续的。**这其中有两点: +另外,切记的是,**聚集索引的存储并不是物理上连续的,而是逻辑上连续的。** 这其中有两点: - 每个表的数据页通过双向链表链接,页按照主键的顺序排序; - 每个页的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。 @@ -204,9 +204,9 @@ select for update会锁定查询出来的数据,其他事务将不能再对其 参考链接: [如何写sql能够有效的使用到复合索引](https://blog.csdn.net/riemann_/article/details/94840416) -另外,**联合索引具有最左匹配原则,即最左优先。**比如,我们建立了一个2列的联合索引(col1,col2),实际上已经建立了两个联合索引(col1)、(col1,col2),解释如下。 +另外,**联合索引具有最左匹配原则,即最左优先。** 比如,我们建立了一个2列的联合索引(col1,col2),实际上已经建立了两个联合索引(col1)、(col1,col2),解释如下。 -B+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道第一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了(这种情况下无法使用联合索引)。 +B+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道第一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了(这种情况下无法使用联合索引)。 联合索引的意义在于: @@ -356,7 +356,7 @@ ORDER BY ${orderParam} 如果我们给参数"orderParam"赋值为"id",将SQL打印出来是这样的: -``` +```sql SELECT id,title,author,content FROM blog ORDER BY id ``` @@ -447,7 +447,6 @@ SQL是一套标准,是用来完成和数据库之间的通信的编程语言 (10) LIMIT ``` - 建议直接去看这篇文章:[SQL查询之执行顺序解析](http://zouzls.github.io/2017/03/23/SQL%E6%9F%A5%E8%AF%A2%E4%B9%8B%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F%E8%A7%A3%E6%9E%90/) **28、MySQL中int(11)中的11代表什么含义。** @@ -487,7 +486,7 @@ B+Tree 相对于 BTree 有几点不同: ![Y4yhtK.png](https://s1.ax1x.com/2020/05/19/Y4yhtK.png) -**总结一波:**因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低。 +**总结一波:** 因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低。 参考文章:[为什么数据库选B-tree或B+tree而不是二叉树作为索引结构](https://blog.csdn.net/sinat_27602945/article/details/80118362) @@ -540,11 +539,11 @@ innodb事务日志包括redo log和undo log。redo log是重做日志,提供 - redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。又叫做重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。如数据库掉电后,InnoDB存储引擎会使用redo log来恢复到掉电前的时刻,以此来保证数据的完整性。 - undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。 -**redo log** +**redo log :** 事务中所有操作会先写到redo log中,然后再同步到数据库文件中。所以数据库文件进行事务操作修改时,redo log肯定已经记录了所有事务操作,此时即使数据库挂掉,事务操作也都已经持久化到redo log中了,数据库恢复后可以继续执行剩下操作。它保证了事务的**一致性**。 -**undo log** +**undo log :** undo log有两个作用: @@ -572,11 +571,11 @@ JOIN 按照功能大致分为如下三类: **36、数据库第一、第二、第三范式的理解。** -**第一范式,是指没有重复的列,**表示数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。 +**第一范式,是指没有重复的列,** 表示数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。 -**第二范式,是指属性完全依赖主键,**要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。例如员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个惟一属性列被称为主关键字或主键、主码。 +**第二范式,是指属性完全依赖主键,** 要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。例如员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个惟一属性列被称为主关键字或主键、主码。 -**第三范式,是要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。**例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。 也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。 +**第三范式,是要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。** 例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。 也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。 **37、MySQL半同步复制原理。** @@ -596,6 +595,5 @@ distinct简单来说就是用来去重的,而group by的设计目的则是用 单纯的去重操作使用distinct,速度是快于group by的。 - - -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Redis347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Redis347円257円207円.md" index d920138..75c5ca9 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Redis347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Redis347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——Redis篇 +# 2020届秋招面试题总结——Redis篇 **1、Redis的优势。** @@ -177,7 +177,7 @@ zset是Redis下的有序集合对象,是通过ziplist或者skiplist编码实 ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点则保存元素的分值。对于skiplist编码实现,它同时内部包含一个字典和跳跃表,程序都可以用O(log(N))的复杂度往集合中添加成员,并可以用O(1)的复杂度查找给定成员的分值。 -**16、分布式使用场景(存储session)** +**16、分布式使用场景(存储session)。** 还没遇到过 @@ -200,24 +200,24 @@ ziplist编码的有序集合对象使用压缩列表作为底层实现,每个 在这个读写模式下,也会出现数据不一致问题。 -**最初级的数据不一致问题分析** +**最初级的数据不一致问题分析:** 问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 解决方案:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。 -**比较复杂的数据不一致问题分析** +**比较复杂的数据不一致问题分析:** 问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。在这个场景下,数据库和缓存中的数据不一样了。 针对这个方法有一种解决方案,叫做**延时双删策略**。伪代码如下: ```java -public void write(String key,Object data){ -redis.delKey(key); -db.updateData(data); -Thread.sleep(1000); -redis.delKey(key); +public void write(String key,Object data){ +redis.delKey(key); +db.updateData(data); +Thread.sleep(1000); +redis.delKey(key); } ``` @@ -297,11 +297,11 @@ Redis支持物种数据类型:string(字符串)、hash(哈希)、list 使用场景分别为: -- **String:**是简单的key-value类型,value其实不仅可以是String,也可以是数字。常规key-value缓存应用;常规计数:微博数,粉丝数等。 -- **hash:**是一个string类型的field和value的映射表,hash特别适合用于存储对象。存储部分变更的信息,比如说用户信息等。 -- **list:**是一个链表。可以轻松实现最新消息排行榜等功能。另外一个应用就是可以实现一个消息队列。可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。也可以通过zset构建有优先级的队列系统。此外,还可以将redis用作日志收集器,实际上还是一个队列,多个端点将日志信息写入redis,然后一个worker统一将所有日志写到磁盘。 -- **set:**是一个没有重复值得集合。可以存储一些集合性的数据。在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合中。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好等功能。 -- **zset:**相比set,zset增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。也可以利用zset设计带有优先级的队列。另外,还可以做排行榜应用,取TOP N操作。 +- **String:** 是简单的key-value类型,value其实不仅可以是String,也可以是数字。常规key-value缓存应用;常规计数:微博数,粉丝数等。 +- **hash:** 是一个string类型的field和value的映射表,hash特别适合用于存储对象。存储部分变更的信息,比如说用户信息等。 +- **list:** 是一个链表。可以轻松实现最新消息排行榜等功能。另外一个应用就是可以实现一个消息队列。可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。也可以通过zset构建有优先级的队列系统。此外,还可以将redis用作日志收集器,实际上还是一个队列,多个端点将日志信息写入redis,然后一个worker统一将所有日志写到磁盘。 +- **set:** 是一个没有重复值得集合。可以存储一些集合性的数据。在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合中。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好等功能。 +- **zset:** 相比set,zset增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。也可以利用zset设计带有优先级的队列。另外,还可以做排行榜应用,取TOP N操作。 **22、Redis数据淘汰策略。** @@ -355,6 +355,5 @@ SDS结构中拥有len(字符串长度)、free(未使用字节的数量) - 杜绝缓冲区溢出。 - 减少修改字符串时带来的内存重分配次数,因为有空间预分配和惰性空间释放。 - - -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Spring347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Spring347円257円207円.md" index 323ea10..4ff71d2 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Spring347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-Spring347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——Spring篇 +# 2020届秋招面试题总结——Spring篇 **1、讲讲Spring的加载流程。** @@ -97,15 +97,15 @@ DispatcherServlet工作流程可以用一幅图来说明。 ![Y5PCYn.jpg](https://s1.ax1x.com/2020/05/19/Y5PCYn.jpg) -**1发送请求** +**1发送请求 :** 用户向服务器发送HTTP请求,请求被Spring MVC的调度控制器DispatcherServlet捕获。 -**2映射处理器** +**2映射处理器 :** DispatcherServlet根据请求URL,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExectuionChain对象的形式返回。 -**3处理器适配** +**3处理器适配 :** DispatcherServlet根据获得Handler,选择一个合适的HandlerAdapter。(如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler()方法) @@ -120,11 +120,11 @@ Handler(Controller)执行完成后,向DispatcherServlet返回一个ModelAn 图中没有4。 -**5解析试图** +**5解析试图 :** 根据返回的ModelAndView,选择一个合适的ViewResolver(必须是已经注册到Spring容器中的ViewResolver),解析出View对象,然后返回给DispatcherServlet。 -**67渲染视图+相应请求** +**67渲染视图+相应请求 :** ViewResolver结合Model和View,来渲染视图,并写回给用户浏览器。 @@ -152,14 +152,14 @@ Spring Boot启动时的关键步骤,主要在两个方面: Spring在设计中用了几种常用的设计模式。 -**a,工厂模式** +**a,工厂模式 :** 在Spring中我们一般是将Bean的实例化直接交给容器去管理的,实现了使用和创建的分离,这时容器直接管理对象,还有种情况是,bean的创建过程我们交给一个工厂去实现,而Spring容器管理这个工厂。Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。两者对比如下: - BeanFactory:延迟注入(使用到某个bean的时候才会注入),相比于ApplicationContext来说会占用更少的内存,程序启动速度更快。 - ApplicationContext:容器启动的时候,不管bean是否用到,一次性创建所有的bean。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。 -**b,单例设计模式** +**b,单例设计模式 :** 在系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象等。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能导致一些问题的产生,比如:程序的行为异常、资源使用量、或者不一致性的结果。 @@ -183,7 +183,7 @@ private final Map singletonObjects = new ConcurrentHashMap(256 public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); synchronized (this.singletonObjects) { - + //检查缓存中是否存在实例 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { @@ -212,7 +212,7 @@ protected void addSingleton(String beanName, Object singletonObject) { } ``` -**c,代理模式** +**c,代理模式 :** AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 @@ -222,17 +222,17 @@ Spring AOP就是基于动态代理的,其中有两种不同的代理方法:J 另外,Spring AOP属于运行时增强,而Aspect J是编译时增强。Spring AOP基于代理来实现,而Aspect J是基于字节码操作。 -**d,模板方法** +**d,模板方法 :** 模板方法是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。 -**e,观察者模式** +**e,观察者模式 :** 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。 Spring中Observer模式常用的地方是listener的实现。如ApplicationListener。 -**f,适配器模式** +**f,适配器模式 :** 适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 @@ -242,7 +242,7 @@ Spring中Observer模式常用的地方是listener的实现。如ApplicationListe 为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样: -``` +```java if(mappedHandler.getHandler() instanceof MultiActionController){ ((MultiActionController)mappedHandler.getHandler()).xxx }else if(mappedHandler.getHandler() instanceof XXX){ @@ -254,11 +254,11 @@ if(mappedHandler.getHandler() instanceof MultiActionController){ 假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。具体可以参考[SpringMVC中的适配器(适配者模式)](https://www.cnblogs.com/tongkey/p/7919401.html) -**g,策略模式** +**g,策略模式 :** 策略模式对应于解决某一个问题的一个算法族,允许用户从该算法族中任选一个算法解决某一问题,同时可以方便的更换算法或者增加新的算法。并且由客户端决定调用哪个算法,spring中在实例化对象的时候用到Strategy模式。 -总结如下: +总结如下 : Spring 框架中用到了哪些设计模式? @@ -283,7 +283,7 @@ Spring 框架中用到了哪些设计模式? ``` @@ -387,4 +387,5 @@ MyBatis实现分页操作时,有逻辑分页和物理分页这两个区别。 我习惯在项目中使用PageHelper来实现分页。 -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-345円244円232円347円272円277円347円250円213円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-345円244円232円347円272円277円347円250円213円347円257円207円.md" index b632d04..e606543 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-345円244円232円347円272円277円347円250円213円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-345円244円232円347円272円277円347円250円213円347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——多线程篇 +# 2020届秋招面试题总结——多线程篇 **1、多线程的几种实现方式,什么是线程安全。** @@ -64,7 +64,7 @@ Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用 在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized 。 -**6、synchronized的原理是什么,一般用在什么地方(比如加载静态方法和非静态方法的区别)** +**6、synchronized的原理是什么,一般用在什么地方(比如加载静态方法和非静态方法的区别)?** synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。 @@ -183,11 +183,11 @@ TreadLocal和线程同步机制都是为了解决多线程中相同变量的访 - 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。 - 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。 -- 概括起来说,对于多线程资源共享的问题,同步机制采用了**"以时间换空间"**的方式,而ThreadLocal采用了"以空间换时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 +- 概括起来说,对于多线程资源共享的问题,同步机制采用了 **"以时间换空间"** 的方式,而ThreadLocal采用了"以空间换时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 在默认单例的Spring bean中,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。 -需要注意的是,TheadLocalMap作为hash表的一种实现方式,是通过**开放寻址法**来解决哈希冲突,这点不同于HashMap。开放寻址法的核心是如何出现了散列冲突,就重新探测一个空闲位置,将其插入。当我们往散列表插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,以此往后查找,看是否有空闲位置,直到找到为止。 +需要注意的是,TheadLocalMap作为hash表的一种实现方式,是通过**开放寻址法** 来解决哈希冲突,这点不同于HashMap。开放寻址法的核心是如何出现了散列冲突,就重新探测一个空闲位置,将其插入。当我们往散列表插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,以此往后查找,看是否有空闲位置,直到找到为止。 另外,ThreadLocalMap的初始长度为16。当集合中size数量大于规定长度的1/2()时,则执行resize()操作,扩容到原来两倍。具体代码如下: @@ -283,7 +283,7 @@ public final void acquireShared(int arg) { 锁的公平性,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获得锁,也就是说锁获取是顺序的。当然,公平锁机制往往没有非公平的效率高,但也能够减少"饥饿"发生的概率。 -**公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。**释放锁不存在公平性和非公平性。 +**公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。** 释放锁不存在公平性和非公平性。 比较公平锁和非公平锁获取同步状态的tryAcquire()方法(以独占式排他锁ReentrantLock为例,所以是tryAcquire(),如果是共享锁,则是tryAcquireShared()),两者区别在于公平锁在获取同步状态时多了一个限制条件hasQueuedPredecessors(),定义如下。 @@ -441,7 +441,7 @@ Java中的线程分为两种:守护线程(Daemon)和用户线程(User) ![Y51ziq.png](https://s1.ax1x.com/2020/05/19/Y51ziq.png) -注意,线程状态waiting和blocked的区别在于: +注意,线程状态waiting和blocked的区别在于: - 线程可以通过notify,join,LockSupport.park方式进入wating状态,进入wating状态的线程等待唤醒(notify或notifyAll)才有机会获取cpu的时间片段来继续执行。 - 线程的 blocked状态往往是无法进入同步方法/代码块来完成的。这是因为无法获取到与同步方法/代码块相关联的锁。 @@ -471,7 +471,7 @@ Java中的线程分为两种:守护线程(Daemon)和用户线程(User) - **地址空间与其他资源**:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程不可见。 - **通信**:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要线程同步和互斥手段的辅助,以保证数据的一致性。 - **调度和切换**:线程上下文切换比进程上下文切换要快得多。并且线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 -- **系统开销:**由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。 +- **系统开销**:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。 **30、进程间通信的方式。** @@ -508,4 +508,5 @@ notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续 参考链接:[将ThreadLocal变量设置为private static的好处是啥? - Viscent大千的回答 - 知乎](https://www.zhihu.com/question/35250439/answer/101676937) -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円223円215円344円275円234円347円263円273円347円273円237円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円223円215円344円275円234円347円263円273円347円273円237円347円257円207円.md" index 94b8cc4..a9bce62 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円223円215円344円275円234円347円263円273円347円273円237円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円223円215円344円275円234円347円263円273円347円273円237円347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——操作系统篇 +# 2020届秋招面试题总结——操作系统篇 **1、进程和线程的区别。** @@ -56,11 +56,11 @@ 两者的不同如下: -- **目的不同:**段是信息的逻辑单位,它是根据用户的需求划分的,因此段是对用户可见的;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。 -- **大小不同:**段的大小不固定,有它所完成的功能决定;页的大小是固定的,由系统决定。 -- **地址空间不同:**段向用户提供二维地址空间;页向用户提供的是一维地址空间。 -- **信息共享:**段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享收到限制。 -- **内存碎片:**页式存储管理的优点是没有外碎片,但是会产生内碎片。而段式管理的优点是没有内碎片,但会产生外碎片。 +- **目的不同:** 段是信息的逻辑单位,它是根据用户的需求划分的,因此段是对用户可见的;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。 +- **大小不同:** 段的大小不固定,有它所完成的功能决定;页的大小是固定的,由系统决定。 +- **地址空间不同:** 段向用户提供二维地址空间;页向用户提供的是一维地址空间。 +- **信息共享:** 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享收到限制。 +- **内存碎片:** 页式存储管理的优点是没有外碎片,但是会产生内碎片。而段式管理的优点是没有内碎片,但会产生外碎片。 在分页系统中,允许将进程的每一页离散地存储在内存的任一物理块中,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立了一张页面映射表,简称页表。页表的作用就是实现从页号到物理块号的地址映射。 @@ -70,19 +70,19 @@ 首先讨论**批处理系统**。批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 -**先来先服务 first-come first-serverd(FCFS):**非抢占式,FCFS是一种最简单的调度算法,该算法即可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建线程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。 +**先来先服务 first-come first-serverd(FCFS):** 非抢占式,FCFS是一种最简单的调度算法,该算法即可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建线程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。 -**短作业优先 shortest job first(SJF):**非抢占式,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。它们可以分别用于作业调度和进程调度。短作业优先的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一致执行到完成,或发生事件而被阻塞放弃处理机时再重新调度。 +**短作业优先 shortest job first(SJF):** 非抢占式,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。它们可以分别用于作业调度和进程调度。短作业优先的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一致执行到完成,或发生事件而被阻塞放弃处理机时再重新调度。 -**最短剩余时间优先 shortest remaining time next(SRTN):**最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 +**最短剩余时间优先 shortest remaining time next(SRTN):** 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 然后讨论**交互式系统**,交互式系统有大量的用户交互操作,在该系统中调度算法的目标就是快速地进行响应。 -**时间片轮转算法:**将所有就绪进程按照FCFS地原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序边停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大的关系。时间片过小,会导致进程切换太频繁。如果时间片过长,那么实时性就不能得到保证。 +**时间片轮转算法:** 将所有就绪进程按照FCFS地原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序边停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大的关系。时间片过小,会导致进程切换太频繁。如果时间片过长,那么实时性就不能得到保证。 -**优先级调度:**为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远得不到调度,可以随着时间的推移增加等待线程的优先级。 +**优先级调度:** 为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远得不到调度,可以随着时间的推移增加等待线程的优先级。 -**多级反馈队列:**一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8....。进程在第一个队列没执行完,就会转移到下一个队列。这种方式下,之前的进行就只需要交换7次。每个丢列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排休,才能调度当前队列上的进程。 +**多级反馈队列:** 一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8....。进程在第一个队列没执行完,就会转移到下一个队列。这种方式下,之前的进行就只需要交换7次。每个丢列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排休,才能调度当前队列上的进程。 **8、死锁的必要条件和处理方法。** @@ -90,17 +90,17 @@ 死锁的必要条件有四个。 -- **互斥:**每个资源要么已经分配给一个进程,要么就是可用的。 -- **占有和等待:**已经得到了某个资源的进程可以再请求新的资源。 -- **不可抢占:**已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。 -- **环路等待:**有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 +- **互斥:** 进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。简单来说,就是一个资源只能被一个进程所获取。 +- **占有和等待:** 进程所获得的资源在未使用完毕之前(包括阻塞时),不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。 +- **不可抢占:** 已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。 +- **环路等待:** 有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 主要有以下四种处理方法: -- **鸵鸟策略:**不才与任何措施,假装没有发生。 -- **死锁检测与死锁恢复:**不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。每种类型一个资源的死锁检测算法是通过有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到了死锁的发生。还有一种是每种资源多个资源的死锁检测。死锁也可以通过抢占、回滚、杀死进程等方式恢复。 -- **死锁预防:**在程序运行之前预防发生死锁。有多种方式,比如破坏互斥条件、破坏占有和等待条件、破坏不可抢占条件和破坏环路等待等。 -- **死锁避免:**基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说,如果存在一个安全序列,那么系统就处于安全状态。资源分配图算法和银行家算法是两种经典地死锁避免的算法,其可以确保系统始终处于安全状态。 +- **鸵鸟策略:** 不才与任何措施,假装没有发生。 +- **死锁检测与死锁恢复:** 不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。每种类型一个资源的死锁检测算法是通过有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到了死锁的发生。还有一种是每种资源多个资源的死锁检测。死锁也可以通过抢占、回滚、杀死进程等方式恢复。 +- **死锁预防:** 在程序运行之前预防发生死锁。有多种方式,比如破坏互斥条件、破坏占有和等待条件、破坏不可抢占条件和破坏环路等待等。 +- **死锁避免:** 基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说,如果存在一个安全序列,那么系统就处于安全状态。资源分配图算法和银行家算法是两种经典地死锁避免的算法,其可以确保系统始终处于安全状态。 **9、Linux中的文件描述符与打开文件之间的关系。** @@ -143,13 +143,13 @@ **12、页面置换算法。** -**FIFO先进先出算法:**在操作系统中经常被用到,比如作业调度。 +**FIFO先进先出算法:** 在操作系统中经常被用到,比如作业调度。 -**LRU最近最少使用算法:**根据使用时间到现在的长短来判断。 +**LRU最近最少使用算法:** 根据使用时间到现在的长短来判断。 -**LFU最少使用次数算法:**根据使用次数来判断。 +**LFU最少使用次数算法:** 根据使用次数来判断。 -**OPT最优置换算法:**理论的最优,当然,这是理论情况。就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。 +**OPT最优置换算法:** 理论的最优,当然,这是理论情况。就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。 **13、颠簸。** @@ -173,6 +173,5 @@ 多线程与fork()的协作性很差。fork()一般不会在多线程程序中调用,因为**Linux的fork()只克隆当前线程的thread of control,不克隆其他线程。fork()之后,除了当前线程之外,其他线程都消失了。**也就是不能一下子fork()出一个和父进程一样的多线程子进程。 - - -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円225円260円346円215円256円347円273円223円346円236円204円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円225円260円346円215円256円347円273円223円346円236円204円347円257円207円.md" index 8046fbb..8caf73b 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円225円260円346円215円256円347円273円223円346円236円204円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-346円225円260円346円215円256円347円273円223円346円236円204円347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——数据结构篇 +# 2020届秋招面试题总结——数据结构篇 **1、ConcurrentHashMap为何读不加锁。** @@ -30,10 +30,10 @@ public V get(Object key) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } - // eh(hash值)为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来 - //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。 - //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。 - //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。 + // eh(hash值)为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable。 + // eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。 + // eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。 + // eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; @@ -309,7 +309,7 @@ final Node[] resize() { 这样我们就可以拥有相同哈希值的对象组织成的一个链表放在hash值对应的bucket下,但相比Key.hashCode()返回的int类型,我们HashMap初始的**容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方为16)要远小于int类型的范围**,所以我们如果只是单纯的使用hashcode取余来获取对应位置的bucket,这将会大大增加哈希碰撞的几率,并且最坏情况下还会将HashMap变成一个单链表。所以肯定要对hashCode做一定优化。 -来看HashMap的hash()函数。上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让**hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,**在JDK 1.8中的hash()函数如下: +来看HashMap的hash()函数。上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让**hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,** 在JDK 1.8中的hash()函数如下: ```java static final int hash(Object key) { @@ -632,6 +632,5 @@ ListIterator是一个功能更强大的迭代器,继承于Iterator,可以通 - ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有这个功能。 - 都可以实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。 - - -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-347円275円221円347円273円234円347円257円207円.md" "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-347円275円221円347円273円234円347円257円207円.md" index 58baa90..1d8e5fb 100644 --- "a/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-347円275円221円347円273円234円347円257円207円.md" +++ "b/interviews/2020345円261円212円347円247円213円346円213円233円351円235円242円350円257円225円351円242円230円346円200円273円347円273円223円-347円275円221円347円273円234円347円257円207円.md" @@ -1,4 +1,4 @@ -## 2020届秋招面试题总结——网络篇 +# 2020届秋招面试题总结——网络篇 **1、http1.0和http1.1的区别。** @@ -19,8 +19,6 @@ ![三次握手](https://s1.ax1x.com/2020/05/18/YhwdED.png) - - 主要流程为: - 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态。 @@ -76,7 +74,7 @@ HTTP响应码主要分为五种: - 4XX:客户端错误,请求不合符。比如400,Bad Request,客户端请求有语法错误,不能被服务器所理解;401,Unauthrized,请求未经授权,这个状态代码必须和WWW-Authenticate 报头域一起使用;403,Forbidden,服务器收到请求,但是拒绝提供服务;404,Not Found,请求资源不存在,输入了错误的URL。 - 5XX:服务器端错误,服务器不能处理合法请求。比如500,Internal Servel Error,服务器发生不可预期的错误;503,Server Unavailable,服务器当前不能处理客户端的请求,一段时间后可能恢复正常。 -**5、当你用浏览器打开一个链接(如:http://www.baidu.com )的时候,计算机做了哪些工作步骤。** +**5、当你用浏览器打开一个链接(如: )的时候,计算机做了哪些工作步骤。** 计算机的工作主要是将域名解析成ip地址。 @@ -104,14 +102,14 @@ TCP提供一种面向连接的、可靠的字节流服务。其中,面向连 对于可靠性,TCP通过以下方式进行保证: -- **数据包校验:**目的是检验数据在传输过程中的变化,若检验包有错,则丢弃报文段并且不给出响应,这时TCP发送数据超时后会重发数据。 -- **序号机制(序号、确认号):**确保了数据是按序、完整到达。 -- **对失序数据包重排序:**既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层。TCP传输时将每个字节的数据都进行了编号,这就是序列号。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。 -- **丢弃重复数据:**对于重复数据,能够丢弃重复数据。这是在超时重传情况下可能发生,判断依据就是序列号。 -- **应答机制:**当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。 -- **超时重发:**当TCP发出一个段后,他启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段。 -- **流量控制:**TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。TCP使用的流量控制协议是可变大小的滑动窗口协议。 -- **拥塞控制:** TCP传输过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题,网络可能在开始的时候就很拥堵,如果给网络再扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,以及大量的超时重传,严重影响传输。所以TCP引入了慢启动的机制,在刚开始发送数据时,先发送少量的数据探路,探清当前的网络状态如何,再决定多大的速度进行传输。这个时候就引入了一个叫做拥塞窗口的概念。拥塞窗口是发送端根据网络拥塞情况确定的窗口值。在刚刚开始发送报文的时候,先把拥塞窗口设置1,每经过一个传输轮次(把拥塞窗口所允许发送的报文段都连续发送出去,并收到接受方的确认应答),拥塞窗口就加倍,这个增长速度是指数级别的,为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。慢开始的"慢"并不是指拥塞窗口的增长速率慢,而是指在TCP开始发送报文段时先设置拥塞窗口=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大拥塞窗口。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取最小的值作为实际发送的窗口。一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。 +- **数据包校验:** 目的是检验数据在传输过程中的变化,若检验包有错,则丢弃报文段并且不给出响应,这时TCP发送数据超时后会重发数据。 +- **序号机制(序号、确认号):** 确保了数据是按序、完整到达。 +- **对失序数据包重排序:** 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层。TCP传输时将每个字节的数据都进行了编号,这就是序列号。TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。 +- **丢弃重复数据:** 对于重复数据,能够丢弃重复数据。这是在超时重传情况下可能发生,判断依据就是序列号。 +- **应答机制:** 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。 +- **超时重发:** 当TCP发出一个段后,他启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段。 +- **流量控制:** TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。TCP使用的流量控制协议是可变大小的**滑动窗口协议**。 +- **拥塞控制:** TCP传输过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题,网络可能在开始的时候就很拥堵,如果给网络再扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,以及大量的超时重传,严重影响传输。所以TCP引入了**慢启动**的机制,在刚开始发送数据时,先发送少量的数据探路,探清当前的网络状态如何,再决定多大的速度进行传输。这个时候就引入了一个叫做拥塞窗口的概念。**拥塞窗口是发送端根据网络拥塞情况确定的窗口值。** 在刚刚开始发送报文的时候,**先把拥塞窗口设置1,每经过一个传输轮次(把拥塞窗口所允许发送的报文段都连续发送出去,并收到接受方的确认应答),拥塞窗口就加倍,** 这个增长速度是指数级别的,为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。慢开始的"慢"并不是指拥塞窗口的增长速率慢,而是指在TCP开始发送报文段时先设置拥塞窗口=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大拥塞窗口。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取最小的值作为实际发送的窗口。一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。 参考文章:[网络基础:TCP协议-如何保证传输可靠性](https://blog.csdn.net/liuchenxia8/article/details/80428157) @@ -180,7 +178,7 @@ HTTP的响应报文如图,也是由四部分构成: HTTP1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求,此外,由于大多数网页的流量都比较小,一次TCP连接很少能通过slow-start区,不利于提高带宽利用率。 -HTTP 1.1支持**长连接(PersistentConnection)和请求的流水线(Pipelining)**处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。 +HTTP 1.1支持**长连接(PersistentConnection)和请求的流水线(Pipelining)** 处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。例如:一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。 HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间(请求的流水线)。 @@ -323,7 +321,7 @@ SSH中用了非对称加密方式。但与HTTPS可以通过CA认证不同,SSH ![YhsL4S.png](https://s1.ax1x.com/2020/05/18/YhsL4S.png) -**23、网络模型** +**23、网络模型。** ![七层网络模型](https://s1.ax1x.com/2020/05/18/YhySun.png) @@ -401,11 +399,11 @@ TCP报文分为首部和数据两部分,TCP的全部功能体现在首部的 首部固定部分各字段的意义。 -- **源端口和目的端口:**各占两个字节。 -- **序号:**占四个字节。 -- **确认号:**占四个字节。 -- **数据偏移:**占四个字节,指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。 -- **保留:**占六个字节,保留为今后使用,但目前应置为0。 +- **源端口和目的端口:** 各占两个字节。 +- **序号:** 占四个字节。 +- **确认号:** 占四个字节。 +- **数据偏移:** 占四个字节,指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。 +- **保留:** 占六个字节,保留为今后使用,但目前应置为0。 还有六个控制位,就在27题里讲述了。TCP示意图如下所示。 @@ -417,4 +415,5 @@ UDP报文首部相对简单。主要分为四部分: - **数据包长度。** - **校验值。** -弥有,2019年9月 \ No newline at end of file +弥有,2019年9月 +[EOF] diff --git "a/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" "b/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" new file mode 100644 index 0000000..20d0402 --- /dev/null +++ "b/notes/Java345円270円270円351円207円217円346円261円240円347円220円206円350円247円243円345円217円212円346円200円273円347円273円223円.md" @@ -0,0 +1,292 @@ +# Java常量池理解及总结 + +## 1、相关概念 + +Java中的常量池,实际上分为两种形态:**静态常量池** 和 **运行时常量池** 。 + +### 1.1 静态常量池 + +在Class文件结构中,最头的4个字节用于存储魔数Magic Number(OxCAFEBABE),用于确定一个文件是否能被JVM接受;再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号;再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。这部分常量池称为静态常量池。与Java中语言习惯不同,常量池容量计数是从1而不是0开始的。 + +Class常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等,而符号引用则属于编译原理方面的概念。Class常量将在类加载后进入方法区的运行时常量池中存放(稍后讲解)。其中,符号引用主要包括下面几类常量: + +- 被模块导出或者开放的包(Package) +- 类和接口的全限定名(Fully Qualified Name) +- 字段的名称和描述符(Descriptor) +- 方法的名称和描述符 +- 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) +- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant) + +常量池中每一项常量都是一个表,共有17种不同类型的表。它们共有一个特定,表结构起始的第一位是个u1类型的标志位(tag,用于区分常量类型),代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如下表所示。 +| 类 型 | 标 志 | 描 述 | +| :-----| :----: | :----: | +| CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 表示方法类型 | +| CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +| CONSTANT_Module_info | 19 | 表示一个模块 | +| CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 | + +以简单的Java代码为例: + +```java +public static void main(String[] args) { + String str="zingbug";// +} +``` + +利用javap命令,生成字节码后,静态常量池如下,具体含义见上表。 + +```java +Constant pool: + #1 = Methodref #2.#3 // java/lang/Object."":()V + #2 = Class #4 // java/lang/Object + #3 = NameAndType #5:#6 // "":()V + #4 = Utf8 java/lang/Object + #5 = Utf8 + #6 = Utf8 ()V + #7 = String #8 // zingbug + #8 = Utf8 zingbug + #9 = Class #10 // Main + #10 = Utf8 Main + ... +``` + +顺便提一下,最常见的CONSTANT_Utf8_info型常量结构体如下表所示。 +| 类 型 | 名 称 | 数 量 | +| :----: :----: | :----- | +| u1 | tag | 1 | +| u2 | length | 1 | +| u1 | bytes | length | + +其中,length值说明了这个UTF-8编码的字符串长度是多少字节,它后面就紧跟着长度为length字节的连续数据,用于表示UTF-8编码的字符串。 +由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。 + +### 1.2 运行时常量池 + +一个类加载到JVM中后对应一个运行时常量池。运行时常量池,是指jvm虚拟机在类加载的解析阶段,将class常量池内的符号引用替换为直接引用(具体请参考另一篇笔记),,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。这其中涉及到类加载的解析阶段。 + +运行时常量池相对于CLass文件常量池的另外一个重要特征是**具备动态性**,Class文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。 + +Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 + +> String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。 + +我自己的理解是,**运行时常量池是Class文件常量池在运行时的表示。** + +我们都知道运行时常量池是放在方法区的,但需要注意的是,方法区本身就是一个逻辑性的区域。在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;但在JDK8及以后,取消了永久代,新增了元空间(Metaspace),方法区就"四分五裂了",不再是在单一的一个去区域内进行存储,其中,元空间只存储类和类加载器的元数据信息,符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这时候方法区只是对逻辑概念的表述罢了。 + +### 1.3 常量池的优势 + +常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 + +- **节省内存空间**:常量池中所有相同的字符串常量被合并,只占用一个空间。 +- **节省运行时间**:比较字符串时,==比equals()快。对于两个引用变量,只用==判断地址引用是否相等,也就可以判断实际值是否相等。 + +> 双等号==的含义: +> 基本数据类型之间应用双等号,比较的是他们的数值。 +> 复合数据类型(字符串、类)之间应用双等号,比较的是他们在内存中的存放地址。当然,内存地址相同,值也必定相同。 + +## 2、Java基本数据类型的包装类和缓存池 + +Java基本数量类型有8种,分别是byte、short、int、long、float、double、char和boolean。 + +### 2.1 6种基本数据类型的包装类实现了缓存池技术,即Byte、Short、Integer、Long、Character和Boolean + +这五种基本数据类型默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。以Integer为例: + +```java +Integer i1=100; +Integer i2=100; +System.out.println(i1==i2);//输出true +Integer i3=300; +Integer i4=300; +System.out.println(i3==i4);//输出false +``` + +对于"Integer i1=100;",Java在编译的时候会直接将代码封装成"Integer i1=Integer.valueOf(100);",从而使用常量池的缓存。进一步去看Integer.valueOf方法的实现源码。 + +```java +public static Integer valueOf(int i) { + if (i>= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +其中,IntegerCache.low为-128,IntegerCache.high默认为127,但也可以通过JVM的启动参数 -XX:AutoBoxCacheMax=size 修改。 + +更多比较场景: + +```java +Integer i1 = 100; +Integer i2 = 0; +Integer i3 = new Integer(100); +Integer i4 = new Integer(100); +Integer i5 = new Integer(0); + +System.out.println("i1=i3 " + (i1 == i3)); +System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); +System.out.println("i3=i4 " + (i3 == i4)); +System.out.println("i3=i4+i5 " + (i3 == i4 + i5)); +System.out.println("100=i4+i5 " + (100 == i4 + i5)); + +//输出 +i1=i3 false +i1=i2+i3 true +i3=i4 false +i3=i4+i5 true +100=i4+i5 true +``` + +对此,有如下解释: + +- Integer i3 = new Integer(100);这种情况下会创建新的对象,所以i1不等于i3; + +- 语句i1 == i2 + i3,因为+这个操作符不适用于Integer对象,首先i2和i3会自动拆箱操作,进行数值相加,即i1 == 100,然后Integer对象无法与数值进行直接比较,所以i1自动拆箱转为int值100,最终这条语句转为100 == 100进行数值比较,故输出true; + +- i3和i4分别是不同的新对象,地址不同,故而不相等。 + +- 语句i3 == i4 + i5,和100 == i4 + i5,同样是自动拆箱操作,最后为数值比较而已。 + +### 2.2 2种浮点数类型的包装类Float,Double并没有实现缓存池技术 + +```java +Double d1=1.8; +Double d2=1.8; +System.out.println(d1==d2);//输出false +``` + +## 3、String类和常量池 + +### 3.1 不同场景下的String比较 + +String与常量池的关系比较复杂多样,我们直接以实际代码为例。 + +```java +String s1 = "Hello"; +String s2 = "Hello"; +String s3 = "Hel" + "lo"; +String s4 = "Hel" + new String("lo"); +String s5 = new String("Hello"); +String s6 = s5.intern(); +String s7 = "H"; +String s8 = "ello"; +String s9 = s7 + s8; + +System.out.println(s1 == s2); // true +System.out.println(s1 == s3); // true +System.out.println(s1 == s4); // false +System.out.println(s1 == s9); // false +System.out.println(s4 == s5); // false +System.out.println(s1 == s6); // true +``` + +首先说明一点,在String中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。对上述场景一一分析: + +- s1 == s2 这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。 +- s1 == s3 这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello",所以s1 == s3成立。**只有使用引号包含文本的方式创建的String对象之间使用"+"连接产生的新对象才会被加入字符串池中。** +- s1 == s4 当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,不清楚s4被分配到哪去了,所以地址肯定不同。**对于所有包含new方式新建对象(包括null)的"+"连接表达式,它所产生的新对象都不会被加入字符串池中。** +- s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。jvm常量池,堆,栈内存分布如下: +![N2xxLd.jpg](https://s1.ax1x.com/2020/06/28/N2xxLd.jpg) +- s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。 +- s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。 + +### 3.2 静态变量赋值 + +```java +public class Main { + public static final String A = "ab"; // 常量A + public static final String B = "cd"; // 常量B + + public static void main(String[] args) { + String s = A + B; // 将两个常量用+连接对s进行初始化 + String t = "abcd"; + System.out.println(s == t);//true + } +} +``` + +此时A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd"; + +### 3.3 静态方法赋值 + +```java +public class Main { + public static final String A; // 常量A + public static final String B; // 常量B + + static { + A = "ab"; + B = "cd"; + } + + public static void main(String[] args) { + // 将两个常量用+连接对s进行初始化 + String s = A + B; + String t = "abcd"; + System.out.println(s == t);//false + } +} +``` + +此时A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 + +### 3.4 intern方法 + +运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 + +```java +public class Main { + public static void main(String[] args) { + String s1 = new String("Hello"); + String s2 = s1.intern(); + String s3 = "Hello"; + System.out.println(s1 == s2);//false + System.out.println(s3 == s2);//true + } +} +``` + +String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回指向该字符串的引用(CONSTAT_String_info),如果没有则添加自己的字符串进入常量池,也同时返回引用。 + +### 3.5 延伸 + +```java +String s1 = new String("Hello"); +``` + +上面这行代码中,一共创建了几个对象? + +"Hello"会首先在常量池中里创建一个字符常量,然后在new创建对象时,会将常量池中"Hello"的字符串复制到堆中新创建的对象字符数组中,并建立引用s1指向它。所以,这条语句共创建了2个对象,分别位于常量池和堆。 + +## 4、总结 + +- 运行时常量池中的常量,基本来源于各个Class文件中的常量池。也就是说,运行时常量池是Class文件常量池在运行时的表示。 +- 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则JVM不会自动添加常量到常量池。 + +## 5、参考资料 + +[《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) + +[浅谈JVM中的符号引用和直接引用](/notes/浅谈JVM中的符号引用和直接引用.md) + +[深入浅出java常量池](https://www.cnblogs.com/syp172654682/p/8082625.html) + +[Java常量池理解与总结](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485452&idx=1&sn=64178cb2b4e2768b2feedbe0d0971ee1&chksm=fd2d7e4eca5af7587be00fccbec403117cdb7eb681c0542ff542335b1239049bd0216fc1bb1d&mpshare=1&scene=1&srcid=0623b0MIhBaU2sZEhvxEjoME&sharer_sharetime=1592875293438&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=242af3e89b1070825a226d604188d71bc5c856baa07c40c96c01389ddc232167ec2a8196658f02f540cce13a26664b46549909f0156708f4d7d26fc7c470faeb92a6c0d4f3ad7328a7cbe59a5c3a0cd6&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=62090523&lang=zh_CN&exportkey=AVRh0T9RxdKUe0T%2BS0Vu7Jk%3D&pass_ticket=mrrMmVKWvl4QF8i0mBVDO7Xre7kYXlm7qLoXUV%2FJeUsPvRILMjcMWMW1A%2BzBALMH) + +ZingBug,2020/6/28 +[EOF] diff --git "a/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" "b/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" new file mode 100644 index 0000000..ce946bb --- /dev/null +++ "b/notes/344円273円216円345円255円227円350円212円202円347円240円201円350円247円222円345円272円246円345円210円206円346円236円220円try343円200円201円catch343円200円201円finally350円277円220円350円241円214円347円273円206円350円212円202円.md" @@ -0,0 +1,225 @@ +# 从字节码角度分析try、catch、finally运行细节 + +## 0、字节码指令 + +回顾一下字节码的指令含义(参考《Java虚拟机规范》-2.11.2节 加载和存储指令): +加载和存储指令用于将数据从栈帧的本地变量表和操作数栈之间来回传递。 + +> - 将一个本地变量加载到操作数栈的指令包括:*iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>*。 +> - 将一个数值从操作数栈存储到局部变量表的指令包括:*istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>*。 +> - 将一个常量加载到操作数栈的指令包括:*bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_null、iconst_<i>、lconst_<i>、fconst_<i>、dconst_<i>*。 +> - 用户扩充局部变量表的访问索引或立即数的指令:*wide*。 + +上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如*iload_<n>*),这些指令助记符实际上代表了一组指令(例如*iload_<n>*代表了*iload_0、iload_1、iload_2和iload_3*这几个指令,需要注意的是,n是从0开始计数的)。这几组指令都是某个带有一个操作数的通用指令(例如*iload*)的特殊形式。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表了long类型,<f>表示float类型,<d>代表了double类型。操作byte、char和short类型数据时,经常用int类型的指令来表示。 +另外,根据int值范围,JVM整型入栈指令分为四类:当int取值-1\~5采用iconst指令,取值-128\~127采用bipush指令,取值-32768\~32767采用sipush指令,取值-2147483648\~2147483647采用 ldc 指令。(参考[JVM字节码之整型入栈指令(iconst、bipush、sipush、ldc)](https://blog.csdn.net/zhaow823/article/details/81199093)) + +## 1、简单的try、catch、finally例子 + +写一个最简单的try、catch、finally例子,分析最后返回的结果,并用javap -verbose 命令来显示目标文件(.class文件)字节码信息,首先确定一下运行环境和JDK版本,不同JDK版本的字节码信息有所不同。 + +> 系统运行环境:Microsoft Windows [版本 10.0.19041.329] 系统 64bit +> JDK信息:Java(TM) SE Runtime Environment (build 14.0.1+7) +Java HotSpot(TM) 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing) + +字节码部分略过了常量池等部分,只对Code和Exception table部分进行分析。 + +```java +//Java代码 +public int inc() { + int x; + try { + x = 1; + return x; + } catch (Exception e) { + x = 2; + return x; + } finally { + x = 3; + } +} +//编译后的ByteCode字节码和异常表 +public int inc(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=5, args_size=1 + 0: iconst_1 //try块,将int类型值(1)压入栈顶 + 1: istore_1 //将栈顶数据(1)存入到第二个本地变量(从0开始计数) + 2: iload_1 //将第二个本地int变量(1)压入栈顶 + 3: istore_2 //将栈顶数据(1)存入第三个本地变量 + 4: iconst_3 //将int类型值(3)压入栈顶,第一个finally + 5: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 6: iload_2 //将第三个本地int变量(1)压入栈顶 + 7: ireturn //返回栈顶数据值(1) + 8: astore_2 //给catch中定义的Exception e赋值,并存储到第三个变量槽中 + 9: iconst_2 //将int类型值(2)压入栈顶 + 10: istore_1 //将栈顶数据(2)存入到第二个本地变量 + 11: iload_1 //将第二个本地int变量(2)压入栈顶 + 12: istore_3 //将栈顶数据(2)存入到第四个本地变量 + 13: iconst_3 //将int类型值(3)压入栈顶,第二个finally + 14: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 15: iload_3 //将第四个本地int变量(2)压入栈顶 + 16: ireturn //返回栈顶数据值(2) + 17: astore 4 //如果出现了不属于java.lang.Exception及其子类的异常才会走到这 + 19: iconst_3 //将int类型值(3)压入栈顶,第三个finally + 20: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 21: aload 4 //将异常放入栈顶 + 23: athrow //抛出栈顶的异常 + Exception table: //异常表 + from to target type + 0 4 8 Class java/lang/Exception + 0 4 17 any + 8 13 17 any + 17 19 17 any +``` + +先解释一下异常表组成: + +- From : 从第几行开始检测; +- To :到第几行结束; +- Target : 假如From和To中的代码执行发生异常跳到哪一行处理; +- Type : Java代码中定义的全部异常类型。 +Type 为 any 的情况,就是抛出了捕获不到的类型。所以全部跳到17行去处理。17行就是Java代码中定义的finally语句块,相当于最后一道防线。 + +编译器为这段Java代码生成了四条异常表记录,对应四条代码可能出现的执行路径,从Java代码的语义上讲,这四条执行路径分别为: + +- 如果try语句块中出现属于Exception及其子类的异常时,会转到catch语句块处理; +- 如果try语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; +- 如果catch语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; +- 如果finally语句块中出现不属于Exception及其子类的异常时,会转到finally语句块处理; + +返回到一开始的问题,这段代码的返回值是多少?答案是: + +- 如果没有出现异常值,返回值是1; +- 如果出现了Exception异常,返回值是2; +- 如果出现了Exception以外的异常,没有返回值。 + +具体的运行逻辑根据上文字节码的注释可以知道,字节码中第0-3行所做的操作是将整数1赋值给变量x,并且将此时x的值复制一份副本到第三个本地变量槽中,作为后面方法返回值使用。如果这时候没有出现异常,则会继续走到第4-5行,将变量x赋值为3,然后将之前保存在第三个本地变量槽中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。如果出现了异常,PC寄存器指针转到第8行,第9-12行所做的事情就是将2赋值给变量x,然后将变量x此时的值赋给第四个本地变量槽,最后再将变量x的值改为3。方法返回前同样将第四个本地变量槽中保留的整数2读到了操作栈顶。从第17行开始的代码,作用是将变量x的值赋为3,并将栈顶的异常抛出,方法结束。 + +## 2、finally中加入return的例子 + +改一下代码,在finally中加入return,这样这段代码中有两个return语句,但是程序到底返回的是try 还是 finally。接下来我们还是看字节码信息。 + +```java +//Java代码 +public int inc() { + int x; + try { + x = 1; + return x; + } catch (Exception e) { + x = 2; + return x; + } finally { + x = 3; + return x;//加入了return + } +} +//编译后的ByteCode字节码和异常表 +public int inc(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=5, args_size=1 + 0: iconst_1 //try块,将int类型值(1)压入栈顶 + 1: istore_1 //将栈顶数据(1)存入到第二个本地变量(从0开始计数) + 2: iload_1 //将第二个本地int变量(1)压入栈顶 + 3: istore_2 //将栈顶数据(1)存入第三个本地变量 + 4: iconst_3 //将int类型值(3)压入栈顶,第一个finally + 5: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 6: iload_1 //将第二个本地int变量(3)压入栈顶 + 7: ireturn //返回栈顶数据值(3) + 8: astore_2 //给catch中定义的Exception e赋值,并存储到第三个变量槽中 + 9: iconst_2 //将int类型值(2)压入栈顶 + 10: istore_1 //将栈顶数据(2)存入到第二个本地变量 + 11: iload_1 //将第二个本地int变量(2)压入栈顶 + 12: istore_3 //将栈顶数据(2)存入到第四个本地变量 + 13: iconst_3 //将int类型值(3)压入栈顶,第二个finally + 14: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 15: iload_1 //将第二个本地int变量(3)压入栈顶 + 16: ireturn //返回栈顶数据值(3) + 17: astore 4 //如果出现了不属于java.lang.Exception及其子类的异常才会走到这 + 19: iconst_3 //将int类型值(3)压入栈顶,第三个finally + 20: istore_1 //将栈顶数据(3)存入到第二个本地变量 + 21: iload_1 //将第2个本地int变量(3)压入栈顶 + 22: ireturn //异常都不用抛出了,直接返回栈顶值(3) + Exception table: + from to target type + 0 4 8 Class java/lang/Exception + 0 4 17 any + 8 13 17 any + 17 19 17 any +``` + +与第一个例子相比,大部分代码相似,但区别在于,try语句块中的return被忽略覆盖掉了,它没有进行真正的返回,只是进行一个赋值操作而已,直到finally重新修改了x=3,再return。需要注意的是,异常athrow操作指令消失了。 + +正常来说我们catch到的Exception e,在这一步处理的时候,如果finally有return,那么会发生吞噬异常的情况,也就是抛出的异常不见了。 + +## 3、catch多个异常类型的例子 + +直接去看代码和字节码中的异常表: + +```java +//Java代码 +public void inc() { + int x; + try { + x = 1; + } catch (NullPointerException e) { + x = 2; + } catch (RuntimeException e) { + x = 3; + } catch (Exception e) { + x = 4; + } finally { + x = 5; + } +} +//字节码中的异常表部分 +Exception table: + from to target type + 0 2 7 Class java/lang/NullPointerException + 0 2 15 Class java/lang/RuntimeException + 0 2 23 Class java/lang/Exception + 0 2 31 any + 7 10 31 any + 15 18 31 any + 23 26 31 any +``` + +当catch多个异常的时候,字节码中异常表会有对应的体现。需要注意的是,小的异常要放在前面,大的异常类放在后面。不然编译不会通过。这样是为了防止大炮打蚊子的浪费,如下所示,我们将大范围的Exception异常放在RuntimeException异常前面的时候,会出现报错现象。 + +![Nwd5tS.png](https://s1.ax1x.com/2020/06/24/Nwd5tS.png) + +## 4、总结 + +对以上所有的例子进行总结 + +- try、catch、finally语句中,在如果try语句有return语句,则返回的之后当前try中变量此时对应的值,此后对变量做任何的修改,都不影响try中return的返回值。 + +- 如果finally块中有return 语句,则返回try或catch中的返回语句忽略。 + +- 如果finally块中抛出异常,则整个try、catch、finally块中抛出异常 + +所以使用try、catch、finally语句块中需要注意的是: + +- 尽量在try或者catch中使用return语句。通过finally块中达到对try或者catch返回值修改是不可行的。 + +- finally块中避免使用return语句,因为finally块中如果使用return语句,会显示的消化掉try、catch块中的异常信息,屏蔽了错误的发生。 + +- finally块中避免再次抛出异常,否则整个包含try语句块的方法回抛出异常,并且会消化掉try、catch块中的异常。 + +## 5、参考资料 + +[《Java核心技术系列:Java虚拟机规范(Java SE 8版)》](https://book.douban.com/subject/26418340/) + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) + +[Java字节码指令收集大全](https://www.cnblogs.com/longjee/p/8675771.html) + +[try catch finally异常机制Java源码+JVM字节码(内含小彩蛋)](https://blog.csdn.net/whiteBearClimb/article/details/104131005) + +[Java中关于try、catch、finally中的细节分析](https://mp.weixin.qq.com/s?__biz=MzU3NDg0MTY0NQ==&mid=2247485213&idx=1&sn=982c5e92d2c15a48b59194258a40c2d2&chksm=fd2d715fca5af84976b8cd034fc52aa94e78c2750ba6bcb586f979438bf38baee283fee7de9b&mpshare=1&scene=1&srcid=0623mdjtJSGrvIRPkaj6HhNx&sharer_sharetime=1592875254832&sharer_shareid=e9e6d524172161e8393308ae6db3aa63&key=c32b5c198f0599707981fa0619311426b3104054b7dd310f3346ed0d2159420a6119aaa288eb9df1d369808227d2bb1eec8f9cbe2510d831061991b4215d7bd786a293f6f9c1033009510d6df3fc2f7f&ascene=1&uin=MjQ3MzkwMTc2Mw%3D%3D&devicetype=Windows+10+x64&version=6209007b&lang=zh_CN&exportkey=AYaxCNjrIahDUSf8A0fYYT4%3D&pass_ticket=4yLw2AbquUCok67oAc4LWcLTUZY6fTDbcUcWHB4Mj69rD%2BHCBbKgabd4I%2BGIDkfk) + +ZingBug,2020年6月24日 +[EOF] diff --git "a/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" "b/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" new file mode 100644 index 0000000..11c30ee --- /dev/null +++ "b/notes/346円265円205円350円260円210円JVM344円270円255円347円232円204円347円254円246円345円217円267円345円274円225円347円224円250円345円222円214円347円233円264円346円216円245円345円274円225円347円224円250円.md" @@ -0,0 +1,194 @@ +# 浅谈JVM中的符号引用和直接引用 + +## 1、前言 + +在 JVM 类加载过程的解析阶段,Java虚拟机将常量池中的符号引用替换为直接引用。符号引用存在于 Class 常量池,在Class文件格式中多以 CONSTANT_Class_info 、 CONSTANT_Fieldref_info 、 CONSTANT_Methodref_info 等类型的常量出现,符号引用和直接引用的区别在于: + +- 符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只用使用时能无歧义地定位到目标即可,与当前虚拟机实现的内存布局无关。 +- 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在虚拟机的内存中存在。 + +此外,在JVM 中方法调用(分派、执行)过程中,有五条相关指令用于方法调用: + +- invokevirtual 指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这 也是 Java 语言中最常见的方法分配方式。 +- invokeinterface 指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。 +- invokespecial 指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。 +- invokestatic 指令:用于调用类静态方法( static 方法)。 +- invokedynamic 指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面四条调用指令的分派逻辑都固化在 Java 虚拟机内部,用户无法改变,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。 + +方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括 ireturn (当返回值是 boolean、 byte、 char、 short和 int类型时使用)、 ireturn、 ireturn、ireturn 和 ireturn,另外还有一个 return 指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。 + +以上的概念介绍,结合一个实例来分析举证。看下面的Java代码: + +```java +public class Main { + public static void main(String[] args) { + Sub sub = new Sub(); + sub.a(); + } +} + +class Sub { + public void a() { + } +} +``` + +编译后使用javap工具,得到Class字节码,如下: + +```java +Constant pool: + #1 = Methodref #2.#3 // java/lang/Object."":()V + #2 = Class #4 // java/lang/Object + #3 = NameAndType #5:#6 // "":()V + #4 = Utf8 java/lang/Object + #5 = Utf8 + #6 = Utf8 ()V + #7 = Class #8 // Sub + #8 = Utf8 Sub + #9 = Methodref #7.#3 // Sub."":()V + #10 = Methodref #7.#11 // Sub.a:()V + #11 = NameAndType #12:#6 // a:()V + #12 = Utf8 a + #13 = Class #14 // Main + #14 = Utf8 Main + #15 = Utf8 Code + #16 = Utf8 LineNumberTable + #17 = Utf8 LocalVariableTable + #18 = Utf8 this + #19 = Utf8 LMain; + #20 = Utf8 main + #21 = Utf8 ([Ljava/lang/String;)V + #22 = Utf8 args + #23 = Utf8 [Ljava/lang/String; + #24 = Utf8 sub + #25 = Utf8 LSub; + #26 = Utf8 SourceFile + #27 = Utf8 Main.java +{ + public Main(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + LineNumberTable: + line 8: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this LMain; + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=2, args_size=1 + 0: new #7 // class Sub + 3: dup + 4: invokespecial #9 // Method Sub."":()V + 7: astore_1 + 8: aload_1 + 9: invokevirtual #10 // Method Sub.a:()V + 12: return + LineNumberTable: + line 10: 0 + line 11: 8 + line 12: 12 + LocalVariableTable: + Start Length Slot Name Signature + 0 13 0 args [Ljava/lang/String; + 8 5 1 sub LSub; +} +``` + +因为篇幅有限,上面的内容只保留了静态常量池和 Code 部分。 + +- Sub 实例初始化的指令如下,印证invokespecial指令。 + +```java +4: invokespecial #9 // Method Sub."":()V +``` + +- void 方法的返回指令如下,印证return指令。 + +```java +12: return +``` + +下面我们主要对 Sub.a() 方法的调用来进行说明。 + +## 2、符号引用 + +在 main 方法的字节码中,调用 Sub.a() 方法的指令如下: + +```java +9: invokevirtual #10 // Method Sub.a:()V +``` + +invokevirtual 指令就是调用实例方法的指令,后面的操作数 10 是 Class 文件中常量池的下标,表示用来指定要调用的目标方法。我们再来看常量池在这个位置上的内容: + +```java +#10 = Methodref #7.#11 // Sub.a:()V +``` + +这是一个 Methodref 类型的数据,我们再来看看虚拟机规范中对该类型的说明: + +```java +CONSTANT_Methodref_info { + u1 tag; + u2 class_index; + u2 name_and_type_index; +} +``` + +这实际上就是一种引用类型,tag 表示了常量池数据类型,这里固定是 10 (参照 Class 常量池项目类型表)。class_index 表示了类的索引,name_and_type_index 表示了名称与类型的索引,这两个也都是常量池的下标。在 javap 的输出中,已经将对应的关系打印了出来,我们可以直接的观察到它都引用了哪些类型: + +```java +#10 = Methodref #7.#11 // Sub.a:()V 类中方法的符号引用 +|--#7 = Class #8 // Sub 类或接口的符号引用 +| |--#8 = Utf8 Sub +|--#11 = NameAndType #12:#6 // a:()V 字段或者方法的部分符号引用 +| |--#12 = Utf8 a +| |--#6 = Utf8 ()V +``` + +这里我们将其表现为树的形式。可以看到,我们可以得到该方法所在的类,以及方法的名称和描述符。于是我们根据 invokevirtual 的操作数,找到了常量池中方法对应的 Methodref,进而找到了方法所在的类以及方法的名称和描述符,当然这些内容最终都是 UTF8 字符串形式。 + +实际上这就是一个符号引用的例子,符号引用可以理解为利用文字形式来描述引用关系,简单来说是一个包含足够信息的字符串,以供实际使用时可以找到相应的位置。比如说"java/io/PrintStream.println:(Ljava/lang/String;)V",这里面有类、方法名和方法参数等信息。 + +## 3、直接引用 + +在第一次运行时,发现指令还没有被解析,根据指令去把常量池中有关系的所有项找出来,得到以"UTF-8"编码描述的此方法所属的"类,方法名,描述符"的常量池项,这就是上文中提到的**符号引用**的字符串信息,类的加载解析过程会根据字符串的内容,到该类的方法表中搜索这个方法,得到放发的偏移量(指针),这个偏移量(指针)就是**直接引用** 。再将偏移量赋给常量池的#10 (根据指令,在常量池中找到的第一个项),最后再将 Code 中方法调用的 invokevirtual 指令修改为invokevirtual_quick,并把操作数修改成指向方法表的偏移量(指针), 并加上参数个数,类似于以下格式: + +```java +#10 = Methodref 偏移量(指针) // Sub.a:()V +9: invokevirtual_quick 偏移量(指针) // Method Sub.a:()V +``` + +运行一次之后,符号引用会被替换为直接引用,下次就不用重新搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。 + +上面提到的"invokevirtual_quick"是R大借用Sun JDK 1.0.2 虚拟机为例,提出解释直接引用过程的,详细说明请去看 RednaxelaFX 的回答,参考链接在后文。 + +## 4、总结 + +谈一下我自己的理解,Java代码中所有方法调用的目标方法在 Class 文件里面都是一个常量池的符号引用,在类加载的解析阶段,会将其中符号引用转换为直接引用,这个根据字符串搜素具体方法的过程有些类似于类加载器的运行机制。 + +此外,解析能够顺利进行的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译的那一刻就已经确定下来了,这类方法的调用就叫解析。 + +解析调用一定是一个静态的过程,在编译期间就已经完成,将涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。而另一种主要的方法调用形式,分派调用则复杂很多,它可能是静态的也可能是动态的,与继承、多态相关,有机会去学习理解一波,这里就先挖个坑,不再做多介绍了。 + +## 5、参考资料 + +[《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》](https://book.douban.com/subject/34907497/) + +[《自己动手写Java虚拟机》](https://book.douban.com/subject/26802084/) + +[浅析 JVM 中的符号引用与直接引用](https://www.dazhuanlan.com/2019/10/18/5da94ef24e667/) + +[JVM里的符号引用如何存储? - RednaxelaFX的回答 - 知乎](https://www.zhihu.com/question/30300585/answer/51335493) + +[JVM方法调用(invokevirtual)](https://www.cnblogs.com/Jc-zhu/p/4482294.html) + +ZingBug,2020年6月28日 +[EOF] diff --git "a/notes/350円256円260円BitMap-BitSet.md" "b/notes/350円256円260円BitMap-BitSet.md" index b94f53d..5205b9c 100644 --- "a/notes/350円256円260円BitMap-BitSet.md" +++ "b/notes/350円256円260円BitMap-BitSet.md" @@ -1,4 +1,6 @@ -## 2020届秋招面试题总结——网络篇 +# 2020届秋招面试题总结——网络篇 + +## 前言 提到一个bitmap,肯定想到它是非常经典的海量数据处理工具,其本质是用bit数组的某一位来表示某一数据,从而一个bit数组可以表示海量数据。 @@ -8,6 +10,8 @@ 一个1G的空间,有 `8*1024*1024*1024=8.58*10^9bit`,也就是可以表示85亿个不同的数。 +## 源码实现 + 我们来看一下其内部实现,首先看一下**构造函数**。 ```java @@ -82,6 +86,8 @@ public boolean get(int bitIndex) { 整体意思就是判断bitIndex所对应的位数是否为1。 +## 应用场景 + 怎么去**运用**这个bitMap思想呢。 首先,在原理上(1L << bitIndex),这个联想到LeetCode上一个题,第268题,**Missing Number**,从0到n之间取出n个不同的数,找出漏掉的那个。 @@ -111,7 +117,6 @@ public int missingNumberInByBitSet(int[] array) { ```java public class BitSetTest { - public static void main(String[] args) { BitSet bitSet=new BitSet(2000000000); @@ -183,11 +188,11 @@ public class HugeBitset { ); ``` -参考文章: +## 参考文章 [Java的BitSet原理及应用](https://www.jianshu.com/p/4fbad3a6d253) -[无限下标超大型bitset的java实现,超越原生int 20亿下标的限制](https://blog.csdn.net/flyflyflyflyflyfly/article/details/82952529) - +[无限下标超大型bitset的java实现,超越原生int 20亿下标的限制](https://blog.csdn.net/flyflyflyflyflyfly/article/details/82952529) -弥有,2019年8月5日。 \ No newline at end of file +弥有,2019年8月5日 +[EOF] diff --git "a/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" "b/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" new file mode 100644 index 0000000..2cc754e --- /dev/null +++ "b/notes/350円256円260円System.identityHashCode(obj)344円270円216円obj.hashCode()347円232円204円345円205円263円347円263円273円.md" @@ -0,0 +1,127 @@ +# System.identityHashCode(obj)与obj.hashCode()的关系 + +有时候看一些源码的时候,经常出现System.identityHashCode(obj) 的使用,这里仔细去讨论一下这个方法与平常的obj.hashCode()方法的关系。 + +## 首先去回顾一下hashcode的概念 + +hashcode是jdk根据对象的地址算出来的一个int数字,即对象的哈希码值,代表了该对象在哈希表中的存储位置。(这里不直接使用物理地址是为了散列存储查找的快捷性) + +很多场景下,只要判断两个对象的hashCode是否一致,就知道这两个对象是否相同。hashCode()的性能要比"=="性能高出很多,因为hashCode()的结果计算后会缓存起来,下次直接调用而不需要重新计算。 + +**回到正文,去依次对比看一下两者的不同。** + +## System.identityHashCode(obj) + +identityHashCode是System里面提供的本地方法。 + +```java + /** + * Returns the same hash code for the given object as + * would be returned by the default method hashCode(), + * whether or not the given object's class overrides + * hashCode(). + * The hash code for the null reference is zero. + * + * @param x object for which the hashCode is to be calculated + * @return the hashCode + * @since JDK1.1 + */ + public static native int identityHashCode(Object x); +``` + +看官方注释可知,该方法给定对象的哈希码,与默认的方法hashCode()返回的代码一样,无论给定对象的类是否重写了hashCode(),null引用的哈希码为0。 + +## obj.hashCode() + +```java +public native int hashCode(); +``` + +hashCode()方法是顶级类Object类提供的一个方法,所有的类都可以对hashCode方法就i选哪个重写,此时hash值计算是根据重写后的hashCode方法计算。 + +从上面概念来说,很清晰的看出来identityHashCode是根据Object类hashCode()方法来计算hash值,无论子类是否重写了hashCode()方法,而obj.hashcode()是根据obj的hashcode()方法计算hash值。 + +举个例子来验证一下。 + +```java +public class Main { + + private static class A { + String a; + + A(String a) { + this.a = a; + } + } + + private static class B { + String b; + + B(String b) { + this.b = b; + } + + //重写了hashCode方法 + @Override + public int hashCode() { + return 17 * 34 + b.hashCode(); + } + } + + public static void main(String[] args) { + String s1 = "hello world"; + String s2 = new String("hello world"); + System.out.println("s1 identityHashCode: " + System.identityHashCode(s1)); + System.out.println("s1 hashcode: " + s1.hashCode()); + System.out.println("s2 identityHashCode: " + System.identityHashCode(s2)); + System.out.println("s2 hashcode: " + s2.hashCode()); + + A a = new A("hello world"); + System.out.println("a identityHashCode: " + System.identityHashCode(a)); + System.out.println("a hashcode: " + a.hashCode()); + + B b = new B("hello world"); + System.out.println("b identityHashCode: " + System.identityHashCode(b)); + System.out.println("b hashcode: " + b.hashCode()); + } +} + +//输出 +s1 identityHashCode: 21685669 +s1 hashcode: 1794106052 +s2 identityHashCode: 2133927002 +s2 hashcode: 1794106052 +a identityHashCode: 2133927002 +a hashcode: 2133927002 +b identityHashCode: 1836019240 +b hashcode: 1794106630 +``` + +从上面案例分析: + +- A类没有重写hashCode()方法,所以两个都是直接调用Object的HashCode()方法,结果都一样。 + +- B类重写了hashCode()方法,identityHashCode是调用了Object的HashCode()方法,而hashCode是调用了重写后的hashCode()计算方法,两者结果不一样。 + +- String类重写了hashCode()方法,它是根据String具体值来计算的哈希值,只要值一样,hashCode也一样。所以会看到s1和s2的hashCode是一致的,但是identityHashCode结果不一样(**不同VM中计算方法不同,Google Dalvik VM就是使用对象地址来实现identityHashCode,而HotSpot VM默认是通过伪随机数生成器来实现**),当然,s1和s2的equal是一样的(equal方法是比较实际值),但"=="不一样(因为地址引用不同)。 + +到这时就有一个疑惑,**identityHashCode到底有什么用处吗?** + +identityHashCode()方法保证了返回的值在对象的生命周期内不会改变,注意的是,identityHashCode不能严格保证唯一!这个标识符在理论上可以用于哈希和哈希表以外的其他用途。 + +## 总结 + +hashCode方法可以被重写,并返回重写后的值。 + +identityHashCode方法是返回对象的hash值,而不管对象是否重写了hashCode方法。 + +## 参考资料 + +[System.identityHashCode(obj) 与 obj.hashcode()](https://www.jianshu.com/p/24fa4bdb9b9d) + +[关于Object的identity hash code是否存在重复的问题](https://hllvm-group.iteye.com/group/topic/39183) + +[GC复制存活的对象,内存地址会变吗?以前的引用怎么办]( https://www.zhihu.com/question/49631727/answer/120113928) + +弥有,2020年5月22日 +[EOF] diff --git "a/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" "b/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" index 3891176..582386d 100644 --- "a/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" +++ "b/notes/350円256円260円345円212円250円346円200円201円344円273円243円347円220円206円.md" @@ -1,4 +1,6 @@ -## 动态代理 +# 动态代理 + +## 前言 先说普遍的知识点,动态代理是实现AOP的方法,分为两种,JDK原生和CGLIB方法,两者**主要区别**在于: @@ -13,7 +15,7 @@ - JDK动态代理,类中内部方法间的调用,被调用的方法不会被代理。 - CGLIB动态代理,类中内部方法间的调用,被调用的方法会被代理。 -**下面进行具体讲解。** +**我们结合具体实例,来进行具体细节讲解。** 首先新建一个用于代理的接口(后续委托类和代理类的公共接口)。 @@ -40,7 +42,7 @@ public class UserServiceImpl implements UserService { } ``` -**先介绍动态代理。** +## 源码分析 创建代理类,需要实现InvocationHandler接口,并重写invoke方法。 @@ -271,75 +273,75 @@ after CGLIB ... 其中UserServiceImpl$$EnhancerByCGLIB$$dd4bde41.class就是CGLIB生成的代理类,它继承了UserServiceImpl类。无用代码不再展示。 ```java -public class UserServiceImpl$$EnhancerByCGLIB$$dd4bde41 extends UserServiceImpl implements Factory { - private boolean CGLIB$BOUND; - public static Object CGLIB$FACTORY_DATA; - private static final ThreadLocal CGLIB$THREAD_CALLBACKS; - private static final Callback[] CGLIB$STATIC_CALLBACKS; +public class UserServiceImpl$$EnhancerByCGLIB$$dd4bde41 extends UserServiceImpl implements Factory { + private boolean CGLIB$BOUND; + public static Object CGLIB$FACTORY_DATA; + private static final ThreadLocal CGLIB$THREAD_CALLBACKS; + private static final Callback[] CGLIB$STATIC_CALLBACKS; private MethodInterceptor CGLIB$CALLBACK_0;//拦截器 注意这行!!! - private static Object CGLIB$CALLBACK_FILTER; + private static Object CGLIB$CALLBACK_FILTER; private static final Method CGLIB$printUser0ドル$Method;//被代理方法printUser() 注意这行!!! - private static final MethodProxy CGLIB$printUser0ドル$Proxy; - private static final Object[] CGLIB$emptyArgs; + private static final MethodProxy CGLIB$printUser0ドル$Proxy; + private static final Object[] CGLIB$emptyArgs; private static final Method CGLIB$vaildUser1ドル$Method;//被代理方法vaildUser() 注意这行!!! - private static final MethodProxy CGLIB$vaildUser1ドル$Proxy; - private static final Method CGLIB$equals2ドル$Method; - private static final MethodProxy CGLIB$equals2ドル$Proxy; - private static final Method CGLIB$toString3ドル$Method; - private static final MethodProxy CGLIB$toString3ドル$Proxy; - private static final Method CGLIB$hashCode4ドル$Method; - private static final MethodProxy CGLIB$hashCode4ドル$Proxy; - private static final Method CGLIB$clone5ドル$Method; - private static final MethodProxy CGLIB$clone5ドル$Proxy; - static void CGLIB$STATICHOOK1() { - CGLIB$THREAD_CALLBACKS = new ThreadLocal(); - CGLIB$emptyArgs = new Object[0]; + private static final MethodProxy CGLIB$vaildUser1ドル$Proxy; + private static final Method CGLIB$equals2ドル$Method; + private static final MethodProxy CGLIB$equals2ドル$Proxy; + private static final Method CGLIB$toString3ドル$Method; + private static final MethodProxy CGLIB$toString3ドル$Proxy; + private static final Method CGLIB$hashCode4ドル$Method; + private static final MethodProxy CGLIB$hashCode4ドル$Proxy; + private static final Method CGLIB$clone5ドル$Method; + private static final MethodProxy CGLIB$clone5ドル$Proxy; + static void CGLIB$STATICHOOK1() { + CGLIB$THREAD_CALLBACKS = new ThreadLocal(); + CGLIB$emptyArgs = new Object[0]; Class var0 = Class.forName("com.example.demo.service.UserServiceImpl$$EnhancerByCGLIB$$dd4bde41");//代理类 注意这行!!! - Class var1;//委托类UserServiceImpl 注意这行!!! - Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods()); - CGLIB$equals2ドル$Method = var10000[0]; - CGLIB$equals2ドル$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals2ドル"); - CGLIB$toString3ドル$Method = var10000[1]; - CGLIB$toString3ドル$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString3ドル"); - CGLIB$hashCode4ドル$Method = var10000[2]; - CGLIB$hashCode4ドル$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode4ドル"); - CGLIB$clone5ドル$Method = var10000[3]; - CGLIB$clone5ドル$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone5ドル"); - var10000 = ReflectUtils.findMethods(new String[]{"printUser", "()V", "vaildUser", "()V"}, (var1 = Class.forName("com.example.demo.service.UserServiceImpl")).getDeclaredMethods()); //注意这行!!! + Class var1;//委托类UserServiceImpl 注意这行!!! + Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods()); + CGLIB$equals2ドル$Method = var10000[0]; + CGLIB$equals2ドル$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals2ドル"); + CGLIB$toString3ドル$Method = var10000[1]; + CGLIB$toString3ドル$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString3ドル"); + CGLIB$hashCode4ドル$Method = var10000[2]; + CGLIB$hashCode4ドル$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode4ドル"); + CGLIB$clone5ドル$Method = var10000[3]; + CGLIB$clone5ドル$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone5ドル"); + var10000 = ReflectUtils.findMethods(new String[]{"printUser", "()V", "vaildUser", "()V"}, (var1 = Class.forName("com.example.demo.service.UserServiceImpl")).getDeclaredMethods()); //注意这行!!! CGLIB$printUser0ドル$Method = var10000[0]; //注意这行!! - CGLIB$printUser0ドル$Proxy = MethodProxy.create(var1, var0, "()V", "printUser", "CGLIB$printUser0ドル"); //注意这行!! - CGLIB$vaildUser1ドル$Method = var10000[1]; //注意这行!! + CGLIB$printUser0ドル$Proxy = MethodProxy.create(var1, var0, "()V", "printUser", "CGLIB$printUser0ドル"); //注意这行!! + CGLIB$vaildUser1ドル$Method = var10000[1]; //注意这行!! CGLIB$vaildUser1ドル$Proxy = MethodProxy.create(var1, var0, "()V", "vaildUser", "CGLIB$vaildUser1ドル"); } //注意这行!! - //代理方法(methodProxy.invokeSuper会调用) - final void CGLIB$printUser0ドル() { super.printUser(); } - //代理方法(methodProxy.invoke会调用,这就是为什么在拦截器中调用methodProxy.invoke会死循环,一直在调用拦截器) - public final void printUser() { - MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; - if (var10000 == null) { - CGLIB$BIND_CALLBACKS(this); - var10000 = this.CGLIB$CALLBACK_0; - } - if (var10000 != null) { - var10000.intercept(this, CGLIB$printUser0ドル$Method, CGLIB$emptyArgs, CGLIB$printUser0ドル$Proxy); - } else { - super.printUser(); - } - } - final void CGLIB$vaildUser1ドル() { - super.vaildUser(); - } - public final void vaildUser() { - MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; - if (var10000 == null) { - CGLIB$BIND_CALLBACKS(this); - var10000 = this.CGLIB$CALLBACK_0; - } - if (var10000 != null) { - var10000.intercept(this, CGLIB$vaildUser1ドル$Method, CGLIB$emptyArgs, CGLIB$vaildUser1ドル$Proxy); - } else { - super.vaildUser(); - } - } + //代理方法(methodProxy.invokeSuper会调用) + final void CGLIB$printUser0ドル() { super.printUser(); } + //代理方法(methodProxy.invoke会调用,这就是为什么在拦截器中调用methodProxy.invoke会死循环,一直在调用拦截器) + public final void printUser() { + MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; + if (var10000 == null) { + CGLIB$BIND_CALLBACKS(this); + var10000 = this.CGLIB$CALLBACK_0; + } + if (var10000 != null) { + var10000.intercept(this, CGLIB$printUser0ドル$Method, CGLIB$emptyArgs, CGLIB$printUser0ドル$Proxy); + } else { + super.printUser(); + } + } + final void CGLIB$vaildUser1ドル() { + super.vaildUser(); + } + public final void vaildUser() { + MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; + if (var10000 == null) { + CGLIB$BIND_CALLBACKS(this); + var10000 = this.CGLIB$CALLBACK_0; + } + if (var10000 != null) { + var10000.intercept(this, CGLIB$vaildUser1ドル$Method, CGLIB$emptyArgs, CGLIB$vaildUser1ドル$Proxy); + } else { + super.vaildUser(); + } + } ....... } ``` @@ -469,7 +471,7 @@ private void init() { 至此,CGLIB动态代理原理就差不多搞清楚了,还是深究源码才能弄懂。 -参考链接: +## 参考链接 [jdk和cglib原理区别](https://blog.csdn.net/weixin_39158271/article/details/78074442) @@ -477,4 +479,5 @@ private void init() { [CGLIB动态代理实现方式](https://www.cnblogs.com/monkey0307/p/8328821.html) -弥有,2019/7/25. \ No newline at end of file +弥有,2019/7/25 +[EOF] diff --git "a/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" "b/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" index 06f02a3..27e0a07 100644 --- "a/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" +++ "b/notes/350円256円260円345円215円225円344円276円213円346円250円241円345円274円217円.md" @@ -1,8 +1,8 @@ -## 单例模式 +# 单例模式 本文将一步步介绍单例模式。 -**一、懒汉模式** +## 1、懒汉模式 ```java public class Singleton { @@ -19,9 +19,9 @@ public class Singleton { 这是最简单的,缺点很明显,第一次加载类的时候会连带着创建Singleton实例,这样的结果与我们所期望的不同,因为创建实例的时候可能并不是我们需要这个实例的时候。同时如果这个Singleton实例的创建非常消耗系统资源,而应用始终都没有使用Singleton实例,那么创建Singleton消耗的系统资源就被白白浪费了。但是也有优点,这个是线程安全的。 -为了避免这种情况,我们通常使用**惰性加载**的机制,也就是在使用的时候才去创建。但简单的惰性加载会有线程安全问题,一般会加入锁来保证每次只有唯一线程获取实例。 +为了避免这种情况,我们通常使用**惰性加载** 的机制,也就是在使用的时候才去创建。但简单的惰性加载会有线程安全问题,一般会加入锁来保证每次只有唯一线程获取实例。 -**二、饿汉模式** +## 2、饿汉模式 ```java public class Singleton { @@ -41,7 +41,7 @@ public class Singleton { 这种方法解决了多线程并发安全问题,但是它却很影响性能,每次调用getInstance()方法的时候都必须获得Singleton类的锁(静态方法,synchronized锁针对的是类)。而实际上,当单例实例被创建后,其后的请求没有必要再使用互斥机制了。为了解决这个问题,提出了double-checked locking的解决方案。 -**三、双重检查锁** +## 3、双重检查锁 ```java public static Singleton getInstance() { @@ -66,7 +66,7 @@ public static Singleton getInstance() { 4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。 5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。 -**四、内部类** +## 4、内部类 ```java public class Singleton { @@ -86,7 +86,5 @@ public class Singleton { JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,避免了双重检查方案的问题。此外该方法也只会在第一次调用的时候使用互斥机制,这样就解决了懒汉模式的低效问题。最后instance是在第一次加载SingletonContainer类时被创建的,而SingletonContainer类则在调用getInstance方法的时候才会被加载,因此也实现了惰性加载。 - - 弥有,2019年7月 - +[EOF] diff --git "a/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" "b/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" index 40c85cf..af6305c 100644 --- "a/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" +++ "b/notes/350円256円260円345円276円252円347円216円257円344円276円235円350円265円226円.md" @@ -1,6 +1,8 @@ -## 循环依赖 +# 循环依赖 -**什么是循环依赖。** +## 问题概述 + +### 什么是循环依赖 Bean A 依赖 B,Bean B 依赖 A这种情况下出现循环依赖。 @@ -10,7 +12,7 @@ Bean A → Bean B → Bean A Bean A → Bean B → Bean C → Bean D → Bean E → Bean A -**循环依赖会产生什么结果?** +### 循环依赖会产生什么结果 当Spring正在加载所有Bean时,Spring尝试以能正常创建Bean的顺序去创建Bean。 @@ -81,17 +83,15 @@ The dependencies of some of the beans in the application context form a cycle: └────┘ ``` - - -**解决方法。** +## 解决方法 处理这种问题有几种常见的方式。 -**1、重新设计** +### 1、重新设计 重新设计结构,消除循环依赖。 -**2、使用懒注解@Lazy** +### 2、使用懒注解@Lazy 最简单的消除循环依赖的方式是通过延迟加载。具体代码如下。 @@ -110,7 +110,7 @@ CircularDependencyB是被延迟加载了,它在注入CircularDependencyA中时 总结为:对于单实例bean,默认是在容器启动的时候创建对象。但当使用懒加载时,将对象的创建推迟到第一次获取的时候。 -**3、直接使用Autowired单独注入** +### 3、直接使用Autowired单独注入 直接使用@Autowired注入依赖,不要使用构造器的方式注入,如下。 @@ -130,7 +130,7 @@ public class CircularDependencyB { } ``` -**4、 使用Setter注入** +### 4、使用Setter注入 如下: @@ -158,10 +158,13 @@ public class CircularDependencyB { 另外,还有一些其他方法,比如使用@PostConstruct等等。 -**总结** +## 总结 最后,当遇到循环依赖时。首先考虑是否能够通过重新设计依赖来避免循环依赖。如果确实需要循环依赖,那么可以通过前文提到的方式来处理。优先建议使用setter注入来解决。 -参考链接:[Circular Dependencies in Spring](https://www.baeldung.com/circular-dependencies-in-spring) +## 参考链接 + +[Circular Dependencies in Spring](https://www.baeldung.com/circular-dependencies-in-spring) -ZingBug,2019/7/26. \ No newline at end of file +弥有,2019/7/26 +[EOF]

AltStyle によって変換されたページ (->オリジナル) /