java 随记

后台开发的过程中积累的关于java的杂记

架构

SSH框架

为什么要分层?

因为分层使代码变得清晰,容易写也容易阅读,更重要的是让代码扩展性更好,层与层之间的改动不会互相影响

各层的分工

  1. dao——与数据库交互
  2. service——处理业务逻辑,调用dao层方法
  3. action——用来控制转发,接到请求交给service处理

dao是用于操作数据用的,service是为页面功能服务的,在service中对数据进行处理计算,然后返回数据结果到ACTION,而action则再对数据进一步处理,比如把list转成json,把两个service数据进行合并等,并发送到jsp页面显示。

并发相关

ReentrantLock

参考Java多线程11:ReentrantLock的使用和Condition

  1. lock之后要自己unlock
  2. lock相比synchronized更加灵活,可以通过trylock判断锁是不是被占用了,在被占用的情况下可以忙其他事,而不是直接就阻塞了
  3. lock持有的是对象监视器,也就是类似于syncronized(this){} ,但是需要注意这两者持有的对象监视器是不同的
  4. 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)指定等待的最长时间

不同实现

  1. ArrayBlockingQueue 只有一个锁,通过两个condition来实现阻塞、通知,添加和删除数据时只允许一个被执行
  2. LinkedBlockingQueue 有两个锁,putLock和takeLock,各自维护一个condition,添加和删除数据可以允许并行,当然删除和添加最多各自有一个线程在执行。
  3. LinkedBlockingQueue 不仅在消费数据的时候进行唤醒插入阻塞的线程,而且在插入如果容量还没满,也会唤醒插入阻塞的线程

jvm原理

垃圾回收

参考深入理解java垃圾回收机制

垃圾回收就是java中的一个亮点(有利有弊),通过一定的算法自动管理对象的生命周期,防止内存泄漏(内存对象的生命周期超过了程序需要它的时长)

垃圾回收的算法有:

  1. 引用计数:早期的算法,通过给堆中的每个对象内置一个引用计数器来实现(缺点是:无法检测循环引用)
  2. 标志、清洗算法
  3. 分代收集:频繁收集新生代,比较少的收集老生代,基本不收集持久代(分代回收的GC分为 minor gc 和 full/major gc,以下为两种gc的日志格式)
  • GC:java 随记
  • FULL GC:java 随记
  • 新的对象都在eden区上创建,当eden区的大小达到阈值就会发生GC,eden区中存活的对象会复制到survivor区,并清除eden中无效的对象,如果survivor区中的对象达到年龄限制或者大小达到阈值,就会将存活的对象复制到old区,如果这时old区空间不足就会发生full gc,full gc之后old区的空间仍然无法承载young区要晋升的对象大小,就会发生OOM

内存分配

参考Java里的堆(heap)栈(stack)和方法区(method)深入探究JVM | 探秘Metaspace

内存分为 heap、stack、method

heap:

  1. 堆存放的都是对象,空间大但是访问慢(时空守恒)
  2. 为所有线程所共享
  3. java heap主要分成三种:
  • young:主要用来存放新生的对象
  • old: 主要用来存放生命周期长的内存对象
  • permanent:主要用来存放类和方法的元数据信息和常量池 ,类被加载后就放入这个区域。GC不会对持久层进行清理,
  • metaspace:在java8中持久代已经被移除了,因为持久代的大小是固定的,所以在类加载很多的情况下,容易出现OOM:PermGen space错误,类和方法的元数据被移入元数据区。(存在于本地内存中,所以大小只受物理内存的影响)元数据区是自动增长的,通过-XX:MaxMetaSpaceSize来限制Metaspace的大小,以前的Perm参数失效,超过最大值将会在metaspace发生full gc收集dead class或者classloader

stack:

  1. 栈区放的都是基础数据类型和对象的引用
  2. 保存函数调用的现场

method:

  1. 方法区也为所有线程所共享(另一种说法也就是permanent区)
  2. 方法区存放的都是在整个程序中永远唯一的元素,包括class、static变量
  3. 常量池也是方法区的一部分,存放程序中的字面量如”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 哈希家族的异同点

  1. 都是继承于map接口,用于存储键值对。hashtable是同步的,如果不需要线程安全,推荐使用hashmap代替hashtable,如果需要高并发线程安全的实现,使用ConcurrentHashMap代替hashtable
  2. 集合方法返回的iterators是“fail-fast”的,也就是说当hash结果被修改,除了通过iterator的remove方法外的改动,都会造成iterator抛出ConcurrentModificationException异常。因此,在并发修改的情况下,iterator很快失败并清除,而不是冒险在未来不确定的时间做不确定的事。hashtable的方法返回的emurations却不是fail-fast的
  3. hashmap可以接受null值,hashtable则不行
  4. HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

ConcurrentHashMap(面试必考)

concurrentHashMap是一个并发的hash表的实现,它支持完全的并发读取,支持大数量的并发更新操作。

高性能的原因:

  1. 用分离锁实现多个线程间的更深层次的共享访问,不再是只有一个线程能同时持有容器的锁了。
  2. 利用hashEntry的不变性(final hash,key,next)来降低读操作对加锁的需求
  3. 用volatile变量协调读写线程间的内存可见性

缺点

  1. 返回的迭代器是弱一致性的,fail-safe并且不会抛出ConcurrentModificationException异常

源码分析(基于jdk1.7)

java 随记

concurrentHashMap是由segment数组和hashEntry数组组成的,segment是一种可重入锁ReentranLock,在CHM中扮演锁的角色,HashEntry用于存储键值对数据。一个CHM中包含一个segment数组,segment的结构和hashmap类似,一个segment中包含一个hashEntry数组,每个HashEntry都是一个链表的结构,每个segment守护着自己的hashEntry数组,要往这一段hashEntry数组中修改,必须先获得相应的锁

arrayList、linklist、vector

  1. arrayList 内部用数组实现,随机访问和遍历快,插入删除慢
  2. linklist 内部用链表实现,适合数据的动态插入和删除,随机访问和遍历慢
  3. vector 跟 arraylist差不多,除了以下几点
    • vector是线程安全的,因此访问速度也较慢
    • arraylist在内存不够时扩展50% + 1个,vector默认扩展一倍

java 代码执行顺序

JAVA类首次装入时,会对静态成员变量或方法进行一次初始化,但方法不被调用是不会执行的,静态成员变量和静态初始化块级别相同,非静态成员变量和非静态初始化块级别相同。

初始化顺序:先初始化父类的静态代码--->初始化子类的静态代码-->(创建实例时,如果不创建实例,则后面的不执行)初始化父类的非静态代码(变量定义等)--->初始化父类构造函数--->初始化子类非静态代码(变量定义等)--->初始化子类构造函数

tips:

若子类没有显示调用父类的构造函数,则默认调用父类的无参构造函数,如果父类没有则编译错误

java 命令行参数

  1. -classpath

    java 通过指定-classpath 来指定虚拟机搜索的你要运行的类的目录、jar文件名、zip文件名,之间用;(linux 用:)隔开。否则java查不到你的class文件就会报java.lang.NoClassDefFoundError异常,在运行时可以通过System.getProperty(“java.class.path”)得到jvm查找类的路径

    也可以通过CLASSPATH环境变量来指定类搜索路径,建议用-cp

  2. -DpropertyName=value

    系统属性,可以通过System.getProperty(propertyName)获取value的值,用来设置全局变量值,如配置文件路径

  3. -Xms -Xmx 堆的最大最小值

  4. -Xss 线程堆的最大值

  5. --XX:+HeapDumpOnOutOfMemoryError

    当JVM不断地抛出OutOfMemory错误的时候,该命令会通知JVM拍摄一个“堆转储快照”, 并通过-XX:HeapDumpPath 指定该文件的保存路径,可以方便调试问题

  6. -XX:+UseParNewGC 使用多线程并发处理新生代GC

  7. -XX:+UseConcMarkSweepGC 使用CMS并发处理GC

  8. -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来格式化它的日志输出格式,丰满多姿
上一篇:微信小程序记账本进度五


下一篇:如何使用Charles抓包-- 入门篇