title: 2021年4月22日 阿里供应链Java开发实习面试(三面)
tags: 面经
2021年4月22日 阿里供应链Java开发实习面试(三面)
上来就手撕一道代码--输入一个数,然后顺时针打印出矩阵
然后开始聊人生和项目
计算机为什么是二进制呢?
计算机怎么去做加法运算,减法运算呢?(这个地方答的真的不好)
http,tcp,ip这三者之间的关系
Java的RPC了解吗?(答的不了解)
客户端和服务器之间是怎么交互的?(http的请求过程)
服务端之间交互,你觉得他们会怎么交互呢?(socket)
你的应用和数据库之间怎么连接呢?
Spring的IOC了解吗?
为什么需要IOC呢?(为了将手动创建对象的控制权交给Spring框架来管理,这样只要配置好xml文件,在需要的地方引用就行,这大大增加了项目的可维护性并且降低了开发难度。)
bean之间有依赖关系,如果注入的时候A依赖B,B依赖C,C依赖A,这种情况Spring会考虑这种循环关系吗?
假设我们自己来设计,我们怎么阻止这种死循环呢?(根据上一个问题提出来的)
为什么需要JVM这样的东西呢?
为什么JVM中要有s0和s1这两个东西呢?
那你知道full GC和young GC的区别吗?
除了synchronized,那你觉得还有什么关键字实现线程安全呢?(lock)
为什么有了synchronized,还要有lock这个关键字呢?
反问环节
(面试官说是二面的,但是的确是我的第三次面,所以阿里面试很迷,气场感觉不是很合)
(面了好几次阿里了,真的感觉自己跟阿里的气场不是很合,算辽算辽,平常心!!!)
1.1 计算机为什么是二进制呢?
① 从电路实现层面来讲,更容易实现。当计算机工作的时候,电路通电工作,于是每个输出端就有了电压。电压的高低通过模数转换即转换为二进制:高电平由1表示,低电平由0表示。也就是说将模拟电路换成为数字电路,这样电路只要能识别高低即可,实现起来更为简单;
② 二进制在物理上容易实现存储。可以通过磁极的取向、表面的凹凸、光照的有无来记录;
③ 便于进行加、减运算和计数编码。两个二进制数和、积运算组合各有三种,运算规则简单,有利于简化计算机内部结构,提高运算速度。
④ 便于逻辑判断(是或非)。适合逻辑运算:逻辑代数是逻辑运算的理论依据,二进制只有两个数码,正好与逻辑代数中的“真”和“假”相吻合。二进制的两个数码正好与逻辑命 题中的“真(Ture)”、“假(False)或称为”是(Yes)、“否(No)相对应。
⑤ 用二进制表示数据具有抗干扰能力强,可靠性高等优点。因为每位数据只有高低两个状态,当受到一定程度的干扰时,仍能可靠地分辨出它是高还是低。
1.2 计算机怎么去做加法运算,减法运算呢?
1.3 http,tcp,ip这三者之间的关系
TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
“IP”代表网际协议,TCP和UDP使用该协议从一个网络传送数据包到另一个网络。把IP想像成一种高速公路,它允许其它协议在上面行驶并找到到其它电脑的出口。TCP和UDP是高速公路上的“卡车”,它们携带的货物就是像HTTP,文件传输协议FTP这样的协议等。
1.4 bean之间有依赖关系,如果注入的时候A依赖B,B依赖C,C依赖A,这种情况Spring会考虑这种循环关系吗?假设我们自己来设计,我们怎么阻止这种死循环呢?(根据上一个问题提出来的)
其实,这就是一个Spring循环依赖的问题。
Spring如何解决循环依赖的呢?
Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。
当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个
对象工厂
,添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后
的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取:
第一步:先获取到三级缓存中的工厂;
第二步:调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。
第三步:当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!
1.5 为什么需要 JVM 这样的东西呢?
因为JVM能够实现一次编译,到处运行的效果,可以跨平台运行。Java编译时,并不直接翻译为相依于某平台的0101指令,而是翻译为中介格式的位元码(byte code)。Java 的原始码文件格式名为***.java**,经过编译器翻译过后,会变成***.class**的格式文件位元码。如果想要执行这个位元码档案,目标平台上必须安装有JVM(Java Virtual Machine)。JVM会将位元码翻译为相应平台支持的语言。
而C/C++在不同的平台上必须使用不同的编译器来编译你的代码,在Windows平台上编译好的程序,也不能直接拿到Linux等其它平台上执行,而必须经过重新编译的动作,让编译器将你的程式编译为该平台可以执行的指令。
总结:
Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。 JVM 有针对不同系统的特定实现
(Windows, Linux, macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语⾔**“⼀次编译,随处可以运⾏”**的关键所在。
1.6 为什么JVM中要有s0和s1这两个东西呢?
Survivor的存在意义:就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,为什么一个Survivor区不行?假设现在只有一个survivor区:刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。碎片化带来的风险是极大的,堆中没有足够大的连续内存空间,无法为一个内存需求很大的对象分配内存。所以应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
1.7 那你知道full GC和young GC的区别吗?
参考博客:young GC和Full GC的区别、什么时候触发young gc和Full GC、如何优化GC
young GC(新生代GC):指发生在新生代的垃圾收集动作,新生代中的对象朝生夕死,所以 Minor GC 非常频繁,回收速度也比较快。当新生代中的eden区分配满的时候触发。
Full GC(老年代GC):指发生在老年代的GC,速度一般比 Minor GC 慢十倍以上。Full GC 会 Stop-The-World。Full GC是对收集整堆(新生代、老年代)和方法区的垃圾收集。当老年代满时会引发Full GC,Full GC将会同时回收新生代、年老代 ;当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载 。
拓展:
参考博客:JVM 垃圾回收之Minor GC、Major GC和Full GC之间的区别
- Minor GC 是 清理 新生代中的Eden区,Survivor区满时不会触发 ;当年轻代(Eden区)满时就会触发 Minor GC,这里的年轻代满指的是 Eden区满。
- Major GC 是 **清理 老年代 **;CMS收集器中,当老年代满时会触发 Major GC。目前,只有CMS收集器会有单独收集老年代的行为。其他收集器均无此行为。
1.8 为什么有了synchronized,还要有lock这个关键字呢?
我们使用synchronized加锁是无法主动释放锁的,这就会涉及到死锁的问题。
如果我们的程序使用synchronized关键字发生了死锁时,synchronized关键字是是无法破坏**“不可剥夺”这个死锁的条件的。这是因为synchronized**申请资源的时候, 如果申请不到, 线程直接进入阻塞状态了, 而线程进入阻塞状态, 啥都干不了, 也释放不了线程已经占有的资源。
然而,在大部分场景下,我们都是希望**“不可剥夺”这个条件能够被破坏。也就是说对于“不可剥夺”**这个条件,占用部分资源的线程进一步申请其他资源时, 如果申请不到, 可以主动释放它占有的资源, 这样不可剥夺这个条件就破坏掉了。
synchronized关键字的局限性:
有时候用synchronized修饰的代码,访问它需要很长时间,下一个要访问同一代码块的线程就要等待阻塞很长的时间。如果我想要下一个线程在等待一段时间后,如果还没有得到锁的话,就放弃等待,这就可以使用Lock锁,来设置等待时间。synchronized 是互斥锁,同一时间只能有一个线程可以访问被它修饰的代码块。而Lock锁可以实现互斥锁,也可以实现共享锁(同一时间支持多条线程访问)。
如果我们的程序使用synchronized关键字发生了死锁时,synchronized关键是是无法破坏**“不可剥夺”这个死锁的条件的。这是因为synchronized**申请资源的时候, 如果申请不到, 线程直接进入阻塞状态了, 而线程进入阻塞状态, 啥都干不了, 也释放不了线程已经占有的资源。
手撕算法题:
/***
评测题目一: 已知一只贪吃蛇长度为 n*n,每截以数字从小到大代表。将这只贪吃蛇摆成一个盘旋的正方形。例如:
给定 n = 2,输出:
1 2
4 3
给定 n = 3,输出:
1 2 3
8 9 4
7 6 5
给定 n =4,输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
输入 n,输出贪吃蛇图形
***/
public class Print {
public static void main(String[] args) {
int n = 3;
int[][] res = Print(3);
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
System.out.print(res[i][j]);
}
System.out.println();
}
}
public static int[][] Print(int n){
int[][] arr = new int[n][n];
int l = 0, r = n - 1;
int t = 0, b = n - 1;
int count = 0;
while(true){
for(int i = l; i <= r; i++){
arr[t][i] = count + 1;
count += 1;
}
t++;
if(t > b) break;
for(int i = t; i <= b; i++){
arr[i][r] = count + 1;
count += 1;
}
r--;
if(r < l) break;
for(int i = r; i >= l; i--){
arr[b][i] = count + 1;
count += 1;
}
b--;
if(b < t) break;
for(int i = b; i >= t; i--){
arr[i][l] = count + 1;
count += 1;
}
l++;
if(l > r) break;
}
return arr;
}
}