后台开发的过程中积累的关于java的杂记
架构
SSH框架
为什么要分层?
因为分层使代码变得清晰,容易写也容易阅读,更重要的是让代码扩展性更好,层与层之间的改动不会互相影响
各层的分工
- dao——与数据库交互
- service——处理业务逻辑,调用dao层方法
- action——用来控制转发,接到请求交给service处理
dao是用于操作数据用的,service是为页面功能服务的,在service中对数据进行处理计算,然后返回数据结果到ACTION,而action则再对数据进一步处理,比如把list转成json,把两个service数据进行合并等,并发送到jsp页面显示。
并发相关
ReentrantLock
- lock之后要自己unlock
- lock相比synchronized更加灵活,可以通过trylock判断锁是不是被占用了,在被占用的情况下可以忙其他事,而不是直接就阻塞了
- lock持有的是对象监视器,也就是类似于syncronized(this){} ,但是需要注意这两者持有的对象监视器是不同的
- lock配置Condition的signal和await可以实现syncronized的wait和notify来实现等待/通知模型,相比之下Condition更灵活:一个lock实例可以创建多个Condition实例,实现多路通知和有选择性的通知,而不是像notify一样是由jvm随机选择的
BlockingQueue
概念
阻塞队列是一种支持当获取元素时会阻塞直到队列不为空,当插入元素时阻塞直到队列有空间。
方法
操作阻塞队列有四种形式的方法,add/remove/element抛出异常,offer/poll/peek返回具体值,put/take阻塞,offer(e, time, unit)、poll(time, unit)指定等待的最长时间
不同实现
- ArrayBlockingQueue 只有一个锁,通过两个condition来实现阻塞、通知,添加和删除数据时只允许一个被执行
- LinkedBlockingQueue 有两个锁,putLock和takeLock,各自维护一个condition,添加和删除数据可以允许并行,当然删除和添加最多各自有一个线程在执行。
- LinkedBlockingQueue 不仅在消费数据的时候进行唤醒插入阻塞的线程,而且在插入如果容量还没满,也会唤醒插入阻塞的线程
jvm原理
垃圾回收
参考深入理解java垃圾回收机制
垃圾回收就是java中的一个亮点(有利有弊),通过一定的算法自动管理对象的生命周期,防止内存泄漏(内存对象的生命周期超过了程序需要它的时长)
垃圾回收的算法有:
- 引用计数:早期的算法,通过给堆中的每个对象内置一个引用计数器来实现(缺点是:无法检测循环引用)
- 标志、清洗算法
- 分代收集:频繁收集新生代,比较少的收集老生代,基本不收集持久代(分代回收的GC分为 minor gc 和 full/major gc,以下为两种gc的日志格式)
- GC:
- FULL GC:
- 新的对象都在eden区上创建,当eden区的大小达到阈值就会发生GC,eden区中存活的对象会复制到survivor区,并清除eden中无效的对象,如果survivor区中的对象达到年龄限制或者大小达到阈值,就会将存活的对象复制到old区,如果这时old区空间不足就会发生full gc,full gc之后old区的空间仍然无法承载young区要晋升的对象大小,就会发生OOM
内存分配
内存分为 heap、stack、method
heap:
- 堆存放的都是对象,空间大但是访问慢(时空守恒)
- 为所有线程所共享
- java heap主要分成三种:
- young:主要用来存放新生的对象
- old: 主要用来存放生命周期长的内存对象
- permanent:主要用来存放类和方法的元数据信息和常量池 ,类被加载后就放入这个区域。GC不会对持久层进行清理,
- metaspace:在java8中持久代已经被移除了,因为持久代的大小是固定的,所以在类加载很多的情况下,容易出现OOM:PermGen space错误,类和方法的元数据被移入元数据区。(存在于本地内存中,所以大小只受物理内存的影响)元数据区是自动增长的,通过-XX:MaxMetaSpaceSize来限制Metaspace的大小,以前的Perm参数失效,超过最大值将会在metaspace发生full gc收集dead class或者classloader
stack:
- 栈区放的都是基础数据类型和对象的引用
- 保存函数调用的现场
method:
- 方法区也为所有线程所共享(另一种说法也就是permanent区)
- 方法区存放的都是在整个程序中永远唯一的元素,包括class、static变量
- 常量池也是方法区的一部分,存放程序中的字面量如”hello“ 以及常量
java 集合框架
哈希结构
哈希表的数组长度为什么总是习惯用2^n?
hash 的时候总是需要对对象的hashCode取哈希表长度的模,对于2^n 取模,可以简化为 hash & (2^n - 1),提高效率
jdk1.8中的hashmap中的 hash算法是什么?有什么优点?
hash算法是(h = key.hashCode()) ^ (h >>> 16) ,通过这样hash,高位的变化反映到低位里,这样我们取模的时候hash & (length - 1) 就不会只取低位相关,防止有些hashCode只和高位相关造成的冲突过多
hashtable、hashmap、concurrenthashmap 哈希家族的异同点
- 都是继承于map接口,用于存储键值对。hashtable是同步的,如果不需要线程安全,推荐使用hashmap代替hashtable,如果需要高并发线程安全的实现,使用ConcurrentHashMap代替hashtable
- 集合方法返回的iterators是“fail-fast”的,也就是说当hash结果被修改,除了通过iterator的remove方法外的改动,都会造成iterator抛出ConcurrentModificationException异常。因此,在并发修改的情况下,iterator很快失败并清除,而不是冒险在未来不确定的时间做不确定的事。hashtable的方法返回的emurations却不是fail-fast的
- hashmap可以接受null值,hashtable则不行
- HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);
ConcurrentHashMap(面试必考)
concurrentHashMap是一个并发的hash表的实现,它支持完全的并发读取,支持大数量的并发更新操作。
高性能的原因:
- 用分离锁实现多个线程间的更深层次的共享访问,不再是只有一个线程能同时持有容器的锁了。
- 利用hashEntry的不变性(final hash,key,next)来降低读操作对加锁的需求
- 用volatile变量协调读写线程间的内存可见性
缺点
- 返回的迭代器是弱一致性的,fail-safe并且不会抛出ConcurrentModificationException异常
源码分析(基于jdk1.7)
concurrentHashMap是由segment数组和hashEntry数组组成的,segment是一种可重入锁ReentranLock,在CHM中扮演锁的角色,HashEntry用于存储键值对数据。一个CHM中包含一个segment数组,segment的结构和hashmap类似,一个segment中包含一个hashEntry数组,每个HashEntry都是一个链表的结构,每个segment守护着自己的hashEntry数组,要往这一段hashEntry数组中修改,必须先获得相应的锁
arrayList、linklist、vector
- arrayList 内部用数组实现,随机访问和遍历快,插入删除慢
- linklist 内部用链表实现,适合数据的动态插入和删除,随机访问和遍历慢
- vector 跟 arraylist差不多,除了以下几点
- vector是线程安全的,因此访问速度也较慢
- arraylist在内存不够时扩展50% + 1个,vector默认扩展一倍
java 代码执行顺序
JAVA类首次装入时,会对静态成员变量或方法进行一次初始化,但方法不被调用是不会执行的,静态成员变量和静态初始化块级别相同,非静态成员变量和非静态初始化块级别相同。
初始化顺序:先初始化父类的静态代码--->初始化子类的静态代码-->(创建实例时,如果不创建实例,则后面的不执行)初始化父类的非静态代码(变量定义等)--->初始化父类构造函数--->初始化子类非静态代码(变量定义等)--->初始化子类构造函数
tips:
若子类没有显示调用父类的构造函数,则默认调用父类的无参构造函数,如果父类没有则编译错误
java 命令行参数
-classpath
java 通过指定-classpath 来指定虚拟机搜索的你要运行的类的目录、jar文件名、zip文件名,之间用;(linux 用:)隔开。否则java查不到你的class文件就会报java.lang.NoClassDefFoundError异常,在运行时可以通过System.getProperty(“java.class.path”)得到jvm查找类的路径
也可以通过CLASSPATH环境变量来指定类搜索路径,建议用-cp-DpropertyName=value
系统属性,可以通过System.getProperty(propertyName)获取value的值,用来设置全局变量值,如配置文件路径-Xms -Xmx 堆的最大最小值
-Xss 线程堆的最大值
--XX:+HeapDumpOnOutOfMemoryError
当JVM不断地抛出OutOfMemory错误的时候,该命令会通知JVM拍摄一个“堆转储快照”, 并通过-XX:HeapDumpPath 指定该文件的保存路径,可以方便调试问题-XX:+UseParNewGC 使用多线程并发处理新生代GC
-XX:+UseConcMarkSweepGC 使用CMS并发处理GC
-Djava.awt.headless=true 无头模式,系统的配置模式,在该模式下,系统缺少了显示、键盘或鼠标。据说
在Java服务器程序需要进行部分图像处理功能时,建议将程序运行模式设置为headless,这样有助于服务器端有效控制程序运行状态和内存使用(可防止在处理大图片时发生内存溢出)
泛型
- 上界:表示对泛型的限制,传进来的对象必须是class 或者 class 的子类
- 通配符
- java5之后添加了通配符 和 泛型(泛型我们是了解的),通配符的基本用法
GenericType<?>
GenericType<? extends upperBoundType> // 设置上界
GenericType<? super lowerBoundType> // 设置下界
- 看了下 在 [Java 的泛型类型中使用通配符 - 博客频道 - CSDN.NET](https://app.yinxiang.com/shard/s15/nl/2659954/653fb6e2-ea9d-48f2-b4cf-5d5f749923fb/),觉得通配符主要是方便了**泛型对象作为方法参数可以引用泛型子类**,代码如下:
List<? extends Number> nums = new ArrayList<>();
List<Integer> ints = Arrays.asList(1, 2);
List<Double> doubles = Arrays.asList(1.1, 2.2);
nums.addAll(ints);
nums.addAll(dbls); // addAll 方法使用了通配符作为参数
}
// print all Number 或 Number 子类的list, 如果没有通配符的话, 估计得一个一个子类都去实现以下。。。
public void print(List<? extends Number> list) {
list.forEach(x -> System.out.println(x));
}
- 通配符的限制:
- 不能用来直接创建变量对象
- 不能进行修改操作,例如下面的代码,编译器可能觉得,鬼知道你引用了哪个子类呢
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(1); // 编译错误
java 基础概念
- 守护线程: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作。当最后一个守护线程结束时,守护线程随同JVM一起结束工作。最典型的例子就是GC
-
volatile:
- 用在多线程中,同步变量。一般情况下线程为了提高效率,会缓存主内存中的变量在自己的线程栈中,volatile声明的变量则不能缓存,保证了线程之间的变量一致性[虽然自己测不出这个效果。。。]
- 不能保证线程安全。引用例子如下
假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6;线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6;导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
- 创建对象:
创建对象有哪些方法?
最普通的new(给对象分配空间,调用构造函数初始化,返回引用)
-
调用对象的clone方法
- clone给对象分配空间后,直接在内存上对已有对象影印,不需要构造函数
- 需要实现CloneAble接口才能调用对象的clone方法,clone是一种浅复制,例如对象中包含一个String,那么新的对象中的String 跟原来的指向同一个字符串。
- 要实现深复制,需要实现clone方法,不仅clone本身,还需要包含的引用对象
运用反序列化手段,调用java.io.ObjectInputStream对象的 readObject()方法。【没玩过】
访问修饰符: 只有private在同一个包内不能访问,其他包的只能public能访问
日志操作
log4j
log4j 使用写日志变得很简单,支持多个输出和格式化
log4j 主要由三个部分组成 logger 、appender、 layout
- logger 负责日志的收集,主要由rootlogger 和 其他各个类的logger组成,通过logger.info等方法来记录消息,子logger中可以自定义各种配置,如果没有设置就会向上级的logger查找相应的配置,最上层为rootlogger
- appender负责日志的输出,可以输出到文件、控制台、数据库、kafka等等
- layout绑定到相应的的appender来格式化它的日志输出格式,丰满多姿