如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理
Throwable是Java语言中所有错误或者异常的超类。下一次为Error和Exception
Error
Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果
出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止
Exception
Exception 又 有 两 个 分 支 , 一 个 是 运 行 时 异 常 RuntimeException , 一 个 是CheckedException
RuntimeException 如 : NullPointerException 、 ClassCastException ; 一 个 是 检 查 异 常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一定是程序员的错误.检查异常
CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:
试图在文件尾部读取数据
2. 试图打开一个错误格式的 URL
3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不
try{}catch(){}finally{}
位置不同
功能不同
HashTable初始size为16,扩容newsize=oldsize*2,size一定为2的次幂
HashTbale计算index的方法:index=hash&(tab.length-1)
在jdk1.8中,resize方法在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容
每次扩容的时候,都是扩展2倍
扩展后的node对象的位置要么在原位置,要么移动到原偏移量俩倍的位置
WEB服务区
轻量级高并发服务区
稳定、开源、跨平台、不支持高并发
并发访问量大,会导致服务区消耗大量内存
操作系统对其进行进程或线程间的切换会消耗大量的CPU资源,导致HTTP请求的平均响应速度降低
mybatis优缺点、spring 与mybatis 集成
Config、Sql配置、Mapper配置、有几种注册mapper的方法,优先级如何?
mybaits的一级缓存、二级缓存、mybatis的二级缓存为什么是鸡肋?
通用mapper的实现、mybaits编写sql语句的三种方式
@MapperScan的源码分析?mapperScan如何生效的?
mybatis如何扩展spring的扫描器的、mybatis扫描完之后如何利用FactoryBean的?
mybaits底层如何把一个代理对象放到spring容器中?用到了spring的哪些知识?
mybaits和spring的核心接口ImportBeanDefinitionRegistrar之间千丝万缕的关系
从原来来说明mybaits的一级缓存为什么会失效?spring为什么把他失效?有没有办法解决?
从mybatis来分析mybatis的执行流程、mybaits的sql什么时候缓存的?缓存在哪里?
mybaits当中的方法名为什么需要和mapper当中的id一致?从源码来说明
tomat的总体概述和tomcat的启动流程源码分析
tomcat当中web请求的源码分析?一个http如何请求到tomcat的?tomcat如何处理的?
tomcat的协议分析,从源码来分析tomcat当中的各种详细配置的意义
tomcat和apache、nginx等等主流静态服务器的搭配使用
tomcat的性能调优?生成环境上如何让你的tomcat容器的性能达到最高
spring的基本应用和spring源码的编译
java 混乱的日志系统,Jul、jcl、log4j、slf4j.....
spring4和spring在日志方面的源码对比
AspectJ和springAop,aspectj的静态织入
JDK动态代理的源码分析,JDK是如何操作字节码
spring通过cglib完成AOP,cglib如果完成方法拦截
AnnotationAwareAspectJAutoProxyCreator如何完成代理织入的
BeanDefinition是什么东西,sping当中的各种BeanDefinition的作用
BeanDefinition有什么作用?如果来改变一个bean的行为
BeanDefinitionRegistry的作用,源码分析
BeanNameGenerator如何改变beanName的生成策略
BeanPostProcessor如何插手bean的实例化过程、经典的应用场景有哪些?spring内部哪里用到了这个接口
BeanFactoryPostProcessor和BeanPostProcessor的区别、经典应用场景、spring内部如何把他应用起来的
BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor的关系已经区别,spring底层如何调用他们
ConfigurationClassPostProcessor这个类如何完成bean的扫描,如何完成@Bean的扫描、如何完成对@Import的解析
@Imoprt的三种类型,普通类、配置类、ImportSelector
如何利用ImportSelector来完成对spring的扩展?
@Configuration这注解为什么可以不加?加了和不加的区别,底层为什么使用cglib
@Bean的方法是如何保证单例的?如果不需要单例需要这么配置?为什么需要这么配置
springFacoryBean和BeanFacory的区别,有哪些经典应用场景?spring的factoryMethod的经典应用场景?
ImportBeanDefinitionRegistrar这个接口的作用,其他主流框架如何利用这个类来完成和spring的结合的?
spring是什么时候来执行后置处理器的?有哪些重要的后置处理器,比如CommonAnnotationBeanPostProcessor
CommonAnnotationBeanPostProcessor如何来完成spring初始化方法的回调。spring内部的各种Procesor的作用分别是什么
spring和springBoot当中的各种@Enablexxxx的原理是什么?如何自己实现一个?比如动态开启某某些自定义功能
spring如何来完成bean的循环依赖并且实例化的,什么是spring的IOC容器,怎么通过源码来理解?
其他,比如Bean的实例化过程,源码中的两次gegetSingleton的不同和相比如SpringMvc的源码分析等等......
Spring Cloud
Eureka的源码分析服务注册和服务发现以及心跳机制和保护机制,对比eureka与zookeeper,什么是CAP原则?
Ribbon源码分析和客服端负载均衡,客户端负载均衡?服务端负载均衡? Ribbon核心组件IRule以及重写IRule
Fegin源码分析和声明式服务调用,Fegin负载均衡,Fegin如何与Hystrix结合使用? 有什么问题?
Hystrix实现服务限流、降级,大型分布式项目服务雪崩如何解决? 服务熔断到底是什么?一线公司的解决方案
HystrixDoashboard如何实现自定义接口降级、监控数据、数据聚合等等
Zuul统一网关详解、服务路由、过滤器使用等,从源头来拦截掉一些不良请求
分布式配置中心Config详解,如何与github或是其他自定义的git平台结合、比如gitlab
分布式链路跟踪详解,串联调用链,,让Bug无处可藏,如何厘清微服务之间的依赖关系?如何跟踪业务流的处理顺序?
Spring Boot
Spring Boot的源码分析和基本应用、利用springmvc的知识模拟和手写一个springboot
springmvc的零配置如何实现的?利用servelt3.0的哪些新知识?在springmvc中如何内嵌一个tomcat,如何把web.xml去掉
springboot当中的监听器和设计模式中观察者模式的关系、模拟java当中的事件驱动编程模型
springboot的启动流程分析、springboot如何初始化spring的context?如何初始化DispacterServlet的、如何启动tomcat的
springboot的配置文件类型、配置文件的语法、配置文件的加载顺序、模拟springboot的自动配置
l springboot的日志系统、springboot如何设计他的日志系统的,有什么优势?如何做到统一日志的?
Docker
什么是Docker、为什么要使用他、和开发有什么关系?能否带来便捷、Docker简介、入门,Docker的架构是怎样的?
Docker的三大核心概念:镜像(Images)、容器(Containers)、仓库服务注册器(Registry)他们分别是什么?
Docker的基础用法以及Docker镜像的基本操作
容器技术入门、Docker容器基本操作、容器虚拟化网络概述以及Docker的容器网络是怎样的?
程序员如何利用Dockerfile格式、Dockerfile命令以及docker build构建镜像
Compose和Dockerfile的区别是什么?Compose的配置文件以及使用Compose运行容器、Docker的实战应用
1)是支持【分布式处理】的软件系统
2)是在有【通信网络】互联的【多处理机制体系结构】上执行任务的系统
他包括
【分布式操作系统】
【分布式程序设计】语音及编译(解释)系统
【分布式文件系统】
具有执行远程文件存取的能力,并已透明的方式对分布式在网络上的文件进行管理和存取
【分布式数据库系统】
由分布于多个计算机节点上的若干个数据系统组成
【邮件系统】
构建自己的邮件服务器
适用于(政府机构、大型集团)有效管理(人员结构)
分布式操作系统与(集中操作系统)的区别
(资源管理、进行通信、系统结构)
程序软件设计
用于编写与【分布式计算机系统】上的【分布式程序】
一个分布式程序由若干个可以独立执行的【程序模块】组成
他们分布于一个【分布式处理系统】的多台计算机上被同事执行
特点
分布性、通信性、稳健性
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本
此时,用于简化增删改查工作量的数据访问框架(ORM)是关键
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆分成几个互不相干的应用,以提升效率。此时用于加速前端开发的WEB框架(MVC)是关键
当垂直应用越来越多,应用之间交互不可避免、将核心业务抽取出来,作为独立服务,逐渐形成稳定的服务中心,是前端应用能更快速的响应多变的市场需求,此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键
当服务越来越多,容量的评估,小服务的浪费资源等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率,此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。
Zookeeper提供了一个类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制
Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色的一种
角色与Follower类似,但是无投票权。Zookeeper需保证高可用与强一致性,为了支持更多的客户端,需要增加更多的Server;Server增多,投票阶段延迟增大,影像性能;引入Observer,Observer不参与投票;Observer接受客户端的连接,并将写请求发送给Leader节点;加入更多的Observer节点,提高伸缩性,同时不影响吞吐率。
事务编号 Zxid(事务请求计数器+ epoch)
在 ZAB ( ZooKeeper Atomic Broadcast , ZooKeeper 原子消息广播协议) 协议的事务编号 Zxid
设计中,Zxid 是一个 64 位的数字,其中低 32 位是一个简单的单调递增的计数器,针对客户端每
一个事务请求,计数器加 1;而高 32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新
的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中最大事务的 ZXID,并从中读取
epoch 值,然后加 1,以此作为新的 epoch,并将低 32 位从 0 开始计数。
Zxid(Transaction id)类似于 RDBMS 中的事务 ID,用于标识一次更新操作的 Proposal(提议)
ID。为了保证顺序性,该 zkid 必须单调递增。
epoch
epoch:可以理解为当前集群所处的年代或者周期,每个 leader 就像皇帝,都有自己的年号,所
以每次改朝换代,leader 变更之后,都会在前一个年代的基础上加 1。这样就算旧的 leader 崩溃
恢复之后,也没有人听他的了,因为 follower 只听从当前年代的 leader 的命令。
Zab 协议有两种模式-恢复模式(选主)、广播模式(同步)
Zab 协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导
者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 Server 完成了和 leader 的状
态同步以后,恢复模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态。
ZAB 协议 4 阶段
Leader election(选举阶段-选出准 Leader)
1. Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数
节点的票数,它就可以当选准 leader。只有到达 广播阶段(broadcast) 准 leader 才会成
为真正的 leader。这一阶段的目的是就是为了选出一个准 leader,然后进入下一个阶段。
13/04/2018 Page 173 of 283
Discovery(发现阶段-接受提议、生成 epoch、接受 epoch)
2. Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers
最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且
准 leader 生成新的 epoch,让 followers 接受,更新它们的 accepted Epoch
一个 follower 只会连接一个 leader,如果有一个节点 f 认为另一个 follower p 是 leader,f
在尝试连接 p 时会被拒绝,f 被拒绝之后,就会进入重新选举阶段。
Synchronization(同步阶段-同步 follower 副本)
3. Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,
同步集群中所有的副本。只有当 大多数节点都同步完成,准 leader 才会成为真正的 leader。
follower 只会接收 zxid 比自己的 lastZxid 大的提议。
Broadcast(广播阶段-leader 消息广播)
4. Broadcast(广播阶段):到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,
并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
ZAB 提交事务并不像 2PC 一样需要全部 follower 都 ACK,只需要得到超过半数的节点的 ACK 就
可以了。
ZAB 协议 JAVA 实现(FLE-发现阶段和同步合并为 Recovery Phase(恢复阶段))
协议的 Java 版本实现跟上面的定义有些不同,选举阶段使用的是 Fast Leader Election(FLE),
它包含了 选举的发现职责。因为 FLE 会选举拥有最新提议历史的节点作为 leader,这样就省去了
发现最新提议的步骤。实际的实现将 发现阶段 和 同步合并为 Recovery Phase(恢复阶段)。所
以,ZAB 的实现只有三个阶段:Fast Leader Election;Recovery Phase;Broadcast Phase。
什么是分布式系统?分布式系统有何挑战?Zookeeper快速入门&集群搭建基本使用
Zookeeper有哪些常用命令以及注意事项、zkclient客户端与curator框架有什么功能以及如何使用
手写Zookeeper常见应用场景:分布式配置中心、分布式锁、分布式定时任务
Zookeeper核心概念znode、watch机制、序列化、持久化机制讲解及其源码解析
Zookeeper怎么解决分布式中的一致性问题?领导选举流程讲解及其源码解析
手写RPC框架以及为什么要使用Dubbo? 传统应用系统如何演变成分布式系统详解
Dubbo的六大特性是什么?对企业级开发有何好处?Dubbo的作用简要说明、快速演示Dubbo调用示例
Dubbo中协议、注册中心、动态代理机制是怎么达到可扩展的?Dubbo的扩展机制源码解析
Dubbo从服务提供者到注册中心到消费者调用服务中间的流程源码解析
Dubbo的监控中心以及管理平台的使用,方便企业级开发与管理
关系型数据库瓶颈与优化、ehcache和redis的对比?nosql的使用场景
Redis基本数据类型、比如map的使用场景?有什么优缺点?什么时候用map等等
Redis高级特性、如何来理解redis的单线程但是高性能?如何理解redis和epoll
Redis持久化、什么情况下需要持久化?方案是什么?有什么优缺点?如何优雅的选择持久化方案
Redis项目中应用、reids的高级命令mget、scan?为什么有scan这条命令,如何理解redis的游标?
单机版redis的安装以及redis生产环境启动方案
redis持久化机对于生产环境中的灾难恢复的意义
redis主从架构下如何才能做到99.99%的高可用性
在项目中重新搭建一套主从复制+高可用+多master的redis cluster集群
redis在实践中的一些常见问题以及优化思路(包含linux内核参数优化)
redis的RDB持久化配置以及数据恢复实验
redis的RDB和AOF两种持久化机制的优劣势对比
分库分表场景介绍
Mycat原理解析
分库分表实战
RabbitMQ环境安装&RabbitMQ整体架构与消息流转&交换机详解
消息如何保障 100% 的投递成功方案&企业消息幂等性概念及业界主流解决方案
Confirm确认消息详解&Return返回消息详解&消费端的限流策略&消费端ACK与重回队列机制
SpringAMQP用户管理组件-RabbitAdmin应用&SpringAMQP消息模板组件-RabbitTemplate实战
SpringAMQP消息容器-SimpleMessageListenerContainer详解&SpringAMQP消息适配器-MessageListenerAdapter使用
RabbitMQ与SpringBoot2.0整合实战&RabbitMQ与Spring Cloud Stream整合实战
RabbitMQ集群架构模式&RabbitMQ集群镜像队列构建实现可靠性存储&RabbitMQ集群整合负载均衡基础组件HaProxy_
+
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。
1. 尽量在合适的场合使用单例
使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
第一,控制资源的使用,通过线程同步来控制资源的并发访问;
第二,控制实例的产生,以达到节约资源的目的;
第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
2. 尽量避免随意使用静态变量
当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如
public class A{
private static B b = new B();
}
此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。
3. 尽量避免过多过常地创建Java对象
尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。
4. 尽量使用final修饰符
带有final修饰符的类是不可派生的。在JAVA核心API中,有许多应用final的例子,例如java、lang、String,为String类指定final防止了使用者覆盖length()方法。另外,如果一个类是final的,则该类所有方法都是final的。java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关),此举能够使性能平均提高50%。
如:让访问实例内变量的getter/setter方法变成”final:
简单的getter/setter方法应该被置成final,这会告诉编译器,这个方法不会被重载,所以,可以变成”inlined”,例子:
class MAF {
public void setSize (int size) {
_size = size;
}
private int _size;
}
更正
class DAF_fixed {
final public void setSize (int size) {
_size = size;
}
private int _size;
}
5. 尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。
6. 尽量处理好包装类型和基本类型两者的使用场所
虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包装类型是对象,是在堆中产生实例。在集合类对象,有对象方面需要的处理适用包装类型,其他的处理提倡使用基本类型。
7. 慎用synchronized,尽量减小synchronize的方法
都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以,synchronize的方法尽量减小,并且应尽量使用方法同步代替代码块同步。
9. 尽量不要使用finalize方法
实际上,将资源清理放在finalize方法中完成是非常不好的选择,由于GC的工作量很大,尤其是回收Young代内存时,大都会引起应用程序暂停,所以再选择使用finalize方法进行资源清理,会导致GC负担更大,程序运行效率更差。
10. 尽量使用基本数据类型代替对象
String str = "hello";
上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;
String str = new String("hello");
此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o
11. 多线程在未发生线程安全前提下应尽量使用HashMap、ArrayList
HashTable、Vector等使用了同步机制,降低了性能。
12. 尽量合理的创建HashMap
当你要创建一个比较大的hashMap时,充分利用这个构造函数
public HashMap(int initialCapacity, float loadFactor);
避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。
13. 尽量减少对变量的重复计算
如:
for(int i=0;i<list.size();i++)
应该改为:
for(int i=0,len=list.size();i<len;i++)
并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。
14. 尽量避免不必要的创建
如:
A a = new A();
if(i==1){
list.add(a);
}
应该改为:
if(i==1){
A a = new A();
list.add(a);
}
15. 尽量在finally块中释放资源
程序中使用到的资源应当被释放,以避免资源泄漏,这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。
16. 尽量使用移位来代替'a/b'的操作
"/"是一个代价很高的操作,使用移位的操作将会更快和更有效
如:
int num = a / 4;
int num = a / 8;
应该改为:
int num = a >> 2;
int num = a >> 3;
但注意的是使用移位应添加注释,因为移位操作不直观,比较难理解。
17.尽量使用移位来代替'a*b'的操作
同样的,对于'*'操作,使用移位的操作将会更快和更有效
如:
int num = a * 4;
int num = a * 8;
应该改为:
int num = a << 2;
int num = a << 3;
18. 尽量确定StringBuffer的容量
StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。
如:
StringBuffer buffer = new StringBuffer(1000);
19. 尽量早释放无用对象的引用
大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。
例如:
Java代码
Public void test(){
Object obj = new Object();
……
Obj=null;
}
上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:
Java代码
Public void test(){
Object obj = new Object();
……
Obj=null;
//执行耗时,耗内存操作;或调用耗时,耗内存的方法
……
}
这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。
20. 尽量避免使用二维数组
二维数据占用的内存空间比一维数组多得多,大概10倍以上。
21. 尽量避免使用split
除非是必须的,否则应该避免使用split,split由于支持正则表达式,所以效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果确实需要频繁的调用split,可以考虑使用apache的StringUtils.split(string,char),频繁split的可以缓存结果。
22. ArrayList & LinkedList
一个是线性表,一个是链表,一句话,随机查询尽量使用ArrayList,ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好2者得数据结构,对症下药。
23. 尽量使用System.arraycopy ()代替通过来循环复制数组
System.arraycopy() 要比通过循环来复制数组快的多。
24. 尽量缓存经常使用的对象
尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。
25. 尽量避免非常大的内存分配
有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配的内存块都必须是连续的,而随着堆越来越满,找到较大的连续块越来越困难。
26. 慎用异常
当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说:先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。
如果您创建一个 Exception ,就得付出代价,好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,你甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常,幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。
27. 尽量重用对象
特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。
28. 不要重复初始化变量
默认情况下,调用类的构造函数时,java会把变量初始化成确定的值,所有的对象被设置成null,整数变量设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键字创建一个对象时,构造函数链中的所有构造函数都会被自动调用。
这里有个注意,给成员变量设置初始值但需要调用其他方法的时候,最好放在一个方法。比如initXXX()中,因为直接调用某方法赋值可能会因为类尚未初始化而抛空指针异常,如:public int state = this.getState()。
29. 在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。
30. 在java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。
31. 过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。
32. 在使用同步机制时,应尽量使用方法同步代替代码块同步。
33. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层
Error是获取系统错误的类,或者说是虚拟机错误的类。不是所有的错误Exception都能获取到的,虚拟机报错Exception就获取不到,必须用Error获取。
34. 通过StringBuffer的构造函数来设定它的初始化容量,可以明显提升性能
StringBuffer的默认容量为16,当StringBuffer的容量达到最大容量时,它会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达它的最大容量,它就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的!
35. 合理使用java.util.Vector
Vector与StringBuffer类似,每次扩展容量时,所有现有元素都要赋值到新的存储空间中。Vector的默认存储能力为10个元素,扩容加倍。
vector.add(index,obj) 这个方法可以将元素obj插入到index位置,但index以及之后的元素依次都要向下移动一个位置(将其索引加 1)。 除非必要,否则对性能不利。同样规则适用于remove(int index)方法,移除此向量中指定位置的元素。将所有后续元素左移(将其索引减 1)。返回此向量中移除的元素。所以删除vector最后一个元素要比删除第1个元素开销低很多。删除所有元素最好用removeAllElements()方法。
如果要删除vector里的一个元素可以使用 vector.remove(obj);而不必自己检索元素位置,再删除,如int index = indexOf(obj);vector.remove(index)。
36. 不用new关键字创建对象的实例
用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。
下面是Factory模式的一个典型实现:
public static Credit getNewCredit()
{
return new Credit();
}
改进后的代码使用clone()方法:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit()
{
return (Credit)BaseCredit.clone();
}
9. 不要将数组声明为:public static final
37. HaspMap的遍历:
Map<String, String[]> paraMap = new HashMap<String, String[]>();
for( Entry<String, String[]> entry : paraMap.entrySet() )
{
String appFieldDefId = entry.getKey();
String[] values = entry.getValue();
}
利用散列值取出相应的Entry做比较得到结果,取得entry的值之后直接取key和value。
38. array(数组)和ArrayList的使用
array 数组效率最高,但容量固定,无法动态改变,ArrayList容量可以动态增长,但牺牲了效率。
39. 单线程应尽量使用 HashMap, ArrayList,除非必要,否则不推荐使用HashTable,Vector,它们使用了同步机制,而降低了性能。
40. StringBuffer,StringBuilder的区别在于
java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。
为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。
41. 尽量使用基本数据类型代替对象。
42. 使用具体类比使用接口效率高,但结构弹性降低了,但现代IDE都可以解决这个问题。
43. 考虑使用静态方法,如果你没有必要去访问对象的外部,那么就使你的方法成为静态方法。它会被更快地调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质,调用这个方法不会改变对象的状态。
44. 应尽可能避免使用内在的GET,SET方法。
45.避免枚举,浮点数的使用。
以下举几个实用优化的例子:
一、避免在循环条件中使用复杂表达式
在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。例子:
import java.util.Vector;
class CEL {
void method (Vector vector) {
for (int i = 0; i < vector.size (); i++) // Violation
; // ...
}
}
更正:
class CEL_fixed {
void method (Vector vector) {
int size = vector.size ()
for (int i = 0; i < size; i++)
; // ...
}
}
二、为'Vectors' 和 'Hashtables'定义初始大小
JVM为Vector扩充大小的时候需要重新创建一个更大的数组,将原原先数组中的内容复制过来,最后,原先的数组再被回收。可见Vector容量的扩大是一个颇费时间的事。
通常,默认的10个元素大小是不够的。你最好能准确的估计你所需要的最佳大小。例子:
import java.util.Vector;
public class DIC {
public void addObjects (Object[] o) {
// if length > 10, Vector needs to expand
for (int i = 0; i< o.length;i++) {
v.add(o); // capacity before it can add more elements.
}
}
public Vector v = new Vector(); // no initialCapacity.
}
更正:
自己设定初始大小。
public Vector v = new Vector(20);
public Hashtable hash = new Hashtable(10);
三、在finally块中关闭Stream
程序中使用到的资源应当被释放,以避免资源泄漏。这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。
四、使用'System.arraycopy ()'代替通过来循环复制数组
例子:
public class IRB
{
void method () {
int[] array1 = new int [100];
for (int i = 0; i < array1.length; i++) {
array1 [i] = i;
}
int[] array2 = new int [100];
for (int i = 0; i < array2.length; i++) {
array2 [i] = array1 [i]; // Violation
}
}
}
更正:
public class IRB
{
void method () {
int[] array1 = new int [100];
for (int i = 0; i < array1.length; i++) {
array1 [i] = i;
}
int[] array2 = new int [100];
System.arraycopy(array1, 0, array2, 0, 100);
}
}
五、让访问实例内变量的getter/setter方法变成”final”
简单的getter/setter方法应该被置成final,这会告诉编译器,这个方法不会被重载,所以,可以变成”inlined”,例子:
class MAF {
public void setSize (int size) {
_size = size;
}
private int _size;
}
更正:
class DAF_fixed {
final public void setSize (int size) {
_size = size;
}
private int _size;
}
六、对于常量字符串,用'String' 代替 'StringBuffer'
常量字符串并不需要动态改变长度。
例子:
public class USC {
String method () {
StringBuffer s = new StringBuffer ("Hello");
String t = s + "World!";
return t;
}
}
更正:把StringBuffer换成String,如果确定这个String不会再变的话,这将会减少运行开销提高性能。
七、在字符串相加的时候,使用 ' ' 代替 " ",如果该字符串只有一个字符的话
例子:
public class STR {
public void method(String s) {
String string = s + "d" // violation.
string = "abc" + "d" // violation.
}
}
更正:
将一个字符的字符串替换成' '
public class STR {
public void method(String s) {
String string = s + 'd'
string = "abc" + 'd'
}
}
以上仅是Java方面编程时的性能优化,性能优化大部分都是在时间、效率、代码结构层次等方面的权衡,各有利弊,不要把上面内容当成教条,或许有些对我们实际工作适用,有些不适用,还望根据实际工作场景进行取舍,活学活用,变通为宜。
mysql中为什么不使用其他数据结构而就用B+树作为索引的数据结构
mysql执行计划详解&mysql查询优化器详解
mysql索引优化实战,包括普通查询、group by、order by
hash算法详解、java当中hashmap源码解析、手写一个hashmap
从源码理解hashmapJDK7和JDK8的变化、为什么有这样的变化,Java8新特性
顺序存储、双向链表、单向链表、java当中linkedList的源码分析
java当中线性结构、树形结构以及图形结构分析以及应用场景和经典使用
大数字运算和经典排序、二叉树红黑树排序、查找
java内存模型总体概述、类加载过程和classloader、运行时数据区当中的总体内容、编译原理
内存区域与内存溢出异常、虚拟机对象、程序计数器、java栈、本地方法栈、操作数、方法区、堆内存和元数据等等
Classloader的知识详细、默认全盘负责机制、从JDK源码来理解双亲委派模式、如何打破双亲委派?为什么需要打破?
虚拟机性能监控与故障处理、jvm基本命令,jinfo命令的使用jmap命令使用、jstak命令的使用、使用jvisualvm分析
垃圾收集器与内存分配策略、垃圾回收算法与基础、串型收集器、并行收集器、内存分配与回收策略。
程序编译与代码优化、运行期优化、编译期优化、JVM调优的本质是什么?什么是轻gc?什么是Full gc?如何调优
JVM执行子系统、类文件结构、类加载机制、字节码执行引擎、字节码编译模式、如何改变字节码编译模式?
Thread类本质上是实现了Runnable接口的实例,代表一个线程实例。启动线程的唯一方法就是通过Thread类的start()实例方法。Start()方法是一个native方法,它将启动一个新线程,并执行run()方法
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
如果自己的类已经extends另一个类,就无法直接继承extends Thread,此时,可以实现一个Runnable接口
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()
public void run() {
if (target != null) {
target.run();
}
}
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行
Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程
了。
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
13/04/2018 Page 57 of 283
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程会以下面三种方式结束,结束后就是死亡状态。正常结束
1. run()或 call()方法执行完成,线程正常结束。异常结束
2. 线程抛出一个未捕获的 Exception 或 Error。调用 stop
3. 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使
程序运行结束,线程自动结束。
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的
运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while
循环是否退出,代码示例:
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit
时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只
能由一个线程来修改 exit 的值。
使用 interrupt()方法来中断线程有两种情况:
1. 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,
会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让
我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实
际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法。
2. 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用
interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关
闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:
thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子
线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用
thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈
现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因
此,并不推荐使用 stop 方法来终止线程。
1)定义:守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
2) 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3)设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
4)在 Daemon 线程中产生的新线程也是 Daemon 的。
5)线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
6) example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
7) 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退
JAVA内存模型(JMM)
java当中的线程通讯和消息传递
什么是重排序和顺序一致性?Happens-Before?As-If-Serial?
Synchronized的概念和分析
同步、重量级锁以及Synchronized的原理分析
自旋锁、偏向锁、轻量级锁、重量级锁的概念、使用以及如何来优化他们
Volatile和DCL的知识
Volatile的使用场景和Volatile实现机制、内存语义、内存模型
DCL的单例模式,什么是DCL?如何来解决DCL的问题
并发基础之AQS的深度分析
AbstractAueuedSynchronizer同步器的概念、CLH同步队列是什么?
同步状态的获取和释放、线程阻塞和唤醒
Lock和并发常用工具类
java当中的Lock、ReentrantLock、ReentrantReadWriteLock、Condition
java当中的并发工具类CyclicBarrier、CountDownLatch、Semphore
java当中的并发集合类ConcurrentHashMap、ConcurrentLinkedQueue......
原子操作常用知识讲解
基本类型的原子操作比如经典的AtomicBoolean、AtomicLnteger、AtomicLong
数组类型的原子操作代表几个类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
引用类型的原子操作的典型AtomicReference、AtomicReferenceFieldUpdater......
CAS的概念和知识、Compare And Swap 以及他的缺陷
线程池和并发并行
Executor、ThreadPoolExecutor、Callable &Future、ScheduledExecutorService
ThreadLocal、Fork & Join?什么是并行?线程池如何保证核心线程不被销毁?
Maven
整体认知maven的体系结构
maven核心命令
maven的pom配置体系
搭建Nexus私服
Git
动手搭建Git客户端与服务端
Git的核心命令
Git企业应用
git的原理,git底层指针介绍
Linux
Linux原理、启动、目录介绍
Linux运维常用命令、Linux用户与权限介绍
shell脚本编写
数组
在程序设计中,为了处理方便,把具有同类型的若干变量按有序的形式组织起来。
这些按顺序排列的同类元素的集合称为数组
栈
只能在一段插入或者删除的特殊线性表,他按照新进后出的原则存储数据,先进入的压入栈底,最后的数据在栈顶,需要的数据的时候从栈顶开始弹出(最后一个数据第一个读出来)
队列
一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而表的后端(rear)进行插入操作,进行插入操作的端称为队尾,进行删除操作的端称为对头。队列是按照“先进先出”或“后进后出”的原则组织数据。
队列没有元素的时候称为,空队列
链表
是一种物理存储单元上非连续,非顺序的存储结构,它既可以表示线性结构,又可以表示非线性结构,数据元素的逻辑顺序是通过链表中的指针次顺实现的,
链表由一系列结点(链表中的每个元素称为结点)组成,结点可以再运行时动态生成
每个结点包括俩个部分:一个是存储数据的数据域,一个是存储下一个结点地址的指针域。
树(n结点、k有穷集,关系N,KO根节点)
是包含n(n>0)个结点的有穷集合k,且k中定义了关系N
N满足一下条件
大型互联网电商项目
面试题详解,offer选择
简历技术优化、项目优化
面试问题剖析
职业生涯规划