双非Java的学习之旅以及秋招路程
个人信息:
趁着中秋写个帖子记录一下吧。渣渣本,无实习,无高质量证书,走了很多弯路,最后选择的Java后端。现在算是半躺平了,接了几个中小厂的offer保底,20w多的薪资,后面还有几家公司接着面。不是大佬,还有很多比我厉害的双非战神!感谢很多前辈还有网友,让我白嫖了那么多的资源,趁着中秋假期,写篇文章总结一下。
大一大二划水:
大一学Unity3D做游戏开发,大二学CTF、渗透测试挖洞,都是跟着社团一起学的。我是在大二的暑假,意识到了自己的一些问题,决定选择Java后端的方向。这个时候我Java只会用eclipse写写for循环。
大三上学期:
大三上在b站看了黑马的999集JavaSE、JavaWEB、狂神的SSM、尚硅谷的SSM、Redis等视频。然后寒假投了一波小公司的简历,面试发现知识远远不够。大三上的寒假继续学了尚硅谷的SpringBoot、Dubbo、Spring注解驱动原理等,也是这个时候学长告诉我,春招对大三学生来说也非常重要,很多大佬都是大二就出去实习了,我还一直以为大四才用找工作。也是这个时候,我才知道了一个非常友好的平台,牛客网。可以经常看看别人的面试经历,调整自己的学习重点。
大三下学期:
过完年的寒假投实习岗,阿里、腾讯、字节、京东、携程各大厂的一面,没有一家大厂能进二面的。算法不会,项目没含金量,我都替面试官尴尬。。意识到了算法的重要性,开始刷剑指offer和leetcode,看博客和视频跟着做项目。今年5月份开始,继续投简历,继续面试,开始拿到了一些offer,拿到第一份月薪过万的工作还挺激动。
但我很清楚自己的弱点,计算机网络、操作系统、jvm、juc、mysql原理,都不扎实,而这些又是大公司爱问的。就开始疯狂补这些方面知识。黑马的jvm和juc就挺好。学了后继续面试,每一场面试我都会复盘总结,记录下来,形成自己的小题库。不会就去学,学不会就去背!其实高频被问的就那么些。8月中旬开始,状态越来越好,只要能进面试,至少不会一轮游了,都能接着二面,面试过程也能侃侃而谈,面试官提出的问题都能答个七七八八。
碎碎念:
最后,秋招算是落下帷幕,有点遗憾但要保持热爱。学习Java刚好有一年的时间了,当然没有一些几个月零基础上岸大佬的学习效率高,中间也三天打鱼两天晒网过,但总体保持着一个较好的学习状态。秋招就是一个长跑的过程,投了近百家公司,最累的时候一天四场面试两场笔试。找工作焦虑很正常,经常睡不着。但一直坚持,相信会有好的结果。面包会有的,offer也会有的!
附上一点点自己经常被问到的题目,超高频(答案不一定全对):
1、ArrayList和LinkedList的区别是什么?
ArrayList的底层是数组实现,初始化的时候数据量是零,当第一次add的时候默认变成10。扩容是每次到之前的1.5倍。特性是查询速度快,增删效率低。
扩容条件:每超出数组长度就会进行扩容
扩容分为两个步骤:1.把原来的数组复制到一个更大的数组中2.把新元素添加到扩容的数组里。
LinkedList的底层是带有头节点和尾节点的双向链表,实现了Deque接口所以还可以当双向队列使用。特性是适合插入删除,查询速度慢。
线程都不安全。
如果想要线程安全,又要用List,会怎么用?
古老的Vector类,底层结构和ArrayList一样都是数组。与ArrayList的区别是,大部分方法都被synchronized关键字修饰,所以是一个线程安全的。扩容和ArrayList有所区别,每次扩容为之前的2倍。
2、hashmap的数据结构是什么?
hashmap在1.7和1.8版本底层数据结构不同:1.7是数组加链表,1.8的数据结构是数组加链表/红黑树的方式。
链表和红黑树之间的转换:当链表长度大于等于阈值8,并且数组长度大于等于64,将单链表转化为红黑树。红黑树节点数量小于等于6的时候,又会重新转换为单链表。
扩容机制:hashmap初始化时创建一个空的数组,在第一次put值时数组大小默认变成16。hashmap的负载因子是0.75,这样阈值就是16*0.75=12。
hashmap元素个数大于等于阈值时,调用resize()触发扩容。
resize():创建新的数组代替原有容量小的数组,每次扩容为原来的2倍。扩容后的对象要么放在原来位置,要么移动到原偏移量的两倍的位置。
线程不安全:jdk1.7,添加数据遇到hash碰撞,采用的是头插法,在多线程环境下会造成循环链表死循环。所以jdk1.8改用了尾插法。虽然避免了死循环,但是在多线程情况下,有数据覆盖或者多次扩容发生。
线程不安全的替代品:ConCurrentHashMap
简述从hashmap中get元素的过程?
先对key进行hash计算,得到的hash值跟数组长度-1进行与运算,得到数组下标。如果命中了桶的第一个节点,直接返回;发生hash冲突,通过key.equals()去找到对应的值。
简述从hashmap中put元素的过程?
实际调用了putval()方法:
①先调用hash()方法,对key进行hash计算,得到的hash值跟数组长度-1进行与运算,得到数组下标。
②如果桶里面为null,直接新建节点进行添加;
③如果桶里不为空(发生了hash碰撞),有两种情况:
如果桶里首个元素和key相同(equals),则直接覆盖value;
如果key不同,判断是否为treeNode红黑树,如果是则直接在树中插入键值对;否则就是链表,遍历链表判断key是否存在,存在就直接覆盖value,不存在就插入节点。链表插入节点后判断链表长度是否大于8,大于8就链表转换为红黑树。最后,++size,判断实际大小是否大于阈值,大于就要resize()扩容。
hashmap的hashcode方法如何实现?
将对象的物理地址转化为一个整数,将整数通过hash计算得到hashcode。
3、1)介绍JVM几种垃圾收集算法?
标记-清除算法Mark Sweep:
分为“标记”和“清除”两个阶段,首先标记出所有存活的对象,标记完成后统一回收所有没有被标记的对象。它是最基础的收集算法,后续的算法都
是在对其不足进行改进得到。
产生问题:1.效率问题 2.空间问题(产生大量不连续碎片)
复制算法Copy:
为了解决效率问题。将内存分为相同两块,每次使用其中一块,使用完后将还存活的对象复制到另一个内存块去,然后把原来的内存块全部清理
掉。这样每次都是对一半内存区的回收。
标记-整理算法Mark Compact:
先标记存活的对象,然后让存活对象向一端移动,然后直接清理掉存活对象区域外的内存。
分代收集算法(当前虚拟机都使用):
根据对象存活周期的不同,将内存分为新生代和老年代。比如在新生代,每次收集都会有大量对象死去,采用复制算法,只需要付出少量对象的复制成本,就可以完成每次垃圾收集。而老年代对象存活几率比较高,选择“标记-清除”或“标记-整理”算法进行垃圾收集。
2)新生代和老年代的垃圾收集器有哪些?
Serial收集器:单线程,只使用一个垃圾收集的线程,而且还会stop the world直到它收集结束。
ParNew收集器:Serial的多线程版本。
Parallel Scavenge收集器:关注的是吞吐量(高效率的利用CPU)。jdk1.8默认。
CMS收集器:关注的是用户线程的停顿时间短。四个步骤:
初始标记:暂停所有的其他线程,把直接与root相连的对象记录下来,速度很快。
并发标记:同时开启GC和用户线程,。。。
重新标记:修正并发标记期间,因为用户线程继续运行导致标记变动的记录。
并发清除:开启用户线程,同时GC线程对未标记区域进行清除。
G1收集器:面向服务器的GC。并发与并行,分代收集、空间整合、可预测的停顿。大致四个步骤:
初始标记:
并发标记:
重新标记:
筛选回收:
3)总结一下发生full GC的条件有哪些?
4)JVM的内存区域(运行时数据区)?这几个区有什么作用?为什么分区?堆的内部结构?
堆:
存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。(JDK1.7开始默认开启了逃逸分析,如果对象引用没有被外面使用,也就是未逃逸出去,那么对象可以直接在栈上分配内存)
堆还可以细分为:
JDK1.7:新生代(Eden、From Survivor、To Survivor),老年代,永生代。
JDK1.8:永久代被移除,取而代之的是元空间,元空间使用的是直接内存。(物理上永久代或元空间属于堆,逻辑上属于方法区)
方法区(1.8不同):
存储被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。为永久代或元空间的逻辑部分。
虚拟机栈:
每次方法调用的数据,都是通过虚拟机栈来传递。虚拟机栈由一个个栈帧组成,每次调用方法都有一个对应的栈帧被压入栈,方法结束栈帧被弹出。每个栈帧都有局部变量表、操作数栈、动态链接、方法出口信息。
本地方法栈:
和虚拟机栈类似,区别是,虚拟机使用Native本地方法,就会在本地方法栈创建一个栈帧。
程序计数器:
1.在字节码解释器通过改变程序计数器来依次读取指令,来实现流程控制,如:顺序执行、循环、异常处理。
2.多线程情况下,会发生上下文切换,程序计数器用于记录当前线程执行的位置,在切换回来的时候知道线程上次运行到哪儿了。
4、JAVA线程池实现原理?(不会,底层原理挺难的)
线程池作用:减少每次获取资源的消耗,提高对资源的利用率;提高响应速度;更加方便进行管理。
阿里开发手册强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式创建。
线程池四大方法:Executors.newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、ScheduledThreadPool。
Executors创建FixedThreedPool创建固定线程池和SingleThreadExecutor:LinkedBlockingQueue,允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
Executors创建CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
七大参数:
建议通过ThreadPoolExecutor 的构造方法创建。
ThreadPoolExecutor(int corePoolSize,核心线程数,线程数定义了最小可以同时运行的线程数量。 int maximumPoolSize,当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 long keepAliveTime,当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的救急线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; TimeUnit unit,keepAliveTime 参数的时间单位。 BlockingQueue<Runnable> workQueue,当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 ThreadFactory threadFactory,executor 线程工厂,创建新线程可以起名字、是否守护线程。 RejectedExecutionHandler handler)拒绝策略。
四种拒绝策略:
Redis的数据结构?
String:由多个字节组成,每个字节8个bit,也是bitmap的数据结构。
List列表:相当于Java的LinkedList,双向链表(当栈和队列使用),插入、删除快,查找(lindex)慢。
Hash:相当于Java的HashMap(数组+链表),无序字典。
Set集合:相当于Java的HashSet,无序且唯一。
Sorted set有序集合:相比于set增加了一个权重参数score,使集合中的元素可以有序排列。有点像HashMap和TreeSet的结合。zadd,zcard,zscore,zrange,zrevrange,zrem。
持久化:
快照(RDB):可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。数据体积小,从硬盘恢复到内存速度快。因为是一下子把内存中的数据存到硬盘上,比较耗时,产生阻塞,对其他业务有影响。(不适合实时去做,时候几个小时进行一次备份)
日志(AOF):每执行一个命令,把redis命令存储。可以实时存,不断追加,体积比较大。从硬盘恢复到内存速度慢。
如何解决缓存一致性问题?
对于缓存和数据库的操作,主要有两种方式。
方式一:先删除缓存,再更新数据库(较多):
有脏数据问题:线程1缓存删除后,在更新数据库前。线程2来读缓存,缓存不存在,读数据库,此时数据库读到的是旧值,然后把旧值写入缓存,所以缓存不一致。
解决方案:延时双删:先删除缓存,在更新数据库时,其他线程发现没有缓存会读数据库旧值,然后把旧值添加到缓存。所以在更新完数据库后,sleep一段时间(大于其他线程读写缓存的时间),然后再次删除缓存。
方案缺点:影响性能,sleep时间短第二次删除还是会失败。
方式二:先更新数据库,再删除缓存。保证了最终一致性。(项目)使用缓存的策略:Cache Aside Pattern(旁路缓存模式)
并发问题:更新数据库成功,删除缓存失败,其他线程从缓存读的是旧值。
解决方案:消息队列:先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
方案缺点:问题变得更复杂。而且怎么保证消息不丢失。