文章目录
0 语法
final关键字的作用?
-
1、用来修饰一个引用
-
如果引用为基本数据类型,则该引用为常量,该值无法修改;
-
如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
-
如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
-
-
2、 当使用final修饰方法时
- 这个方法将成为最终方法,无法被重写,可以被继承。
-
3、用来修饰类
-
当用final修饰类时,该类成为最终类,无法被继承,该类就不能被其他类所继承;简称为“断子绝孙类”。
-
final类中所有的成员方法都会隐式的定义为final方法。
-
1 常用类
Object类
toString方法
equals方法和hashcode方法
Objects类
Date类
- public Date() :构造方法,分配Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。
- public Date(long date) :构造方法,分配Date对象并初始化此对象,以表示自从标准基准时间以来的指定毫秒数。
DateFormat类
- public SimpleDateFormat(String pattern) :构造,用给定的模式和默认语言环境的日期格式符号构造impleDateFormat。参数pattern字符串,自定义格式。
- public String format(Date date):将Date对象格式化为字符串。
- public Date parse(String source):将字符串解析为Date对象。
Calendar类
- Calendar.getInstance() 抽象类的静态方法构建,默认时区和语言环境获得一个日历。
- get/set方法
- add方法
- getTime方法:不是获取毫秒时刻,而是拿到对应的Date对象
System类
- currentTimeMillis方法
获取当前系统时间与1970年01月01日00:00点之间的毫秒差值。 - arraycopy方法
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
数组的拷贝动作是系统级的,性能很高。
StringBuilder类
- public StringBuilder()
构造一个空的StringBuilder容器。 - public StringBuilder(String str)
构造一个StringBuilder容器,并将字符串添加进去。 - public StringBuilder append(…)
添加任意类型数据的字符串形式,并返回当前对象自身。 - public String toString()
将当前StringBuilder对象转换为不可变String对象。
String、StringBuilder、StringBuffer的区别⭐⭐⭐
详解
可变:StringBuilder、StringBuffer 不可变:String
效率高低:StringBuilder > StringBuffer (加synchronized的原因)
线程安全:StringBuilder < StringBuffer
- String每次修改都需要产生一个新的对象,而对于StringBuffer,每次操作都是对StringBuffer对象本身,特别适用于字符串内容经常改变的情况下。
- String字符串的拼接会被JVM解析成StringBuilder对象append
包装类
基本类型 | 对应的包装类(位于java.lang包中) |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
- 自动装箱与自动拆箱
- 基本类型和字符串之间的转换
除了Character类之外(String获取单字符可以调用charAt方法),其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型。
boolean a = Boolean.parseBoolean("true");
System.out.println(a); //true
int i = Integer.parseInt("123");
System.out.println(i); //123
byte abc = Byte.parseByte("46");
System.out.println(abc); //46
long l = Long.parseLong("1515661515");
==和equals的使用比较⭐
==:基本类型比较的是值,包装类型比较的是地址
equals:比较的是有该方法的同类型的值
- 值不同
使用==和equals比较都返回false
- 值相同
- 使用==比较:
(1)基本类型-基本类型、基本类型-包装对象返回true(遇到基本类型要自动拆箱)
(2)包装对象-包装对象返回false(都是new出来的)
(3)缓存中去的包装对象比较返回true(JVM 缓存部分基本类型常用的包装类对象,如 Integer 缓存 -128 ~ 127 ) - 使用equals比较:
(1)包装对象-基本类型返回true
(2)包装对象-包装对象返回true - 不同类型的对象对比,返回 false
————————————————————————————————————————————
包装类型声明的细节问题
==比的是地址,new出来的对象地址不同
Integer i1 = new Integer(127);
Integer i2 = new Integer(127);
i1!=i2 //
直接赋值等同于调用valueOf方法,即等价于Integer i = Integer.valueOf(127),缓冲池中-128和127的直接赋值,无需堆中开辟空间
Integer i3 = 127;
Integer i4 = 127;
i3==i4 //
128不在范围内,则需开辟新空间存放变量,看成new,地址不同了
Integer i5 = 128;
Integer i6 = 128;
i5!=i6
2 Collection集合、迭代与泛型
集合框架⭐⭐⭐
-
Collection集合(单列集合)
- List
- ArrayList
- LinkedList
- Vector
- Set
- HashSet
1.LinkedHashSet - TreeSet
- HashSet
- List
-
Map集合(双列集合)
- HashMap
- LinkedHashMap
- HashMap
常用方法
- Collection : add(E e) 、clear() 、remove(E e) 、contains(E e)、isEmpty() 、size()、toArray()
- Map: put(K key, V value)、remove(Object key) 、 get(Object key)、keySet()、entrySet()
实现
ArrayList是动态数组(如何扩容?1.5)非线程安全->vector(扩容2,线程安全,效率不高)
LinkedList是链表
HashTable是HashMap,多了锁,线程安全(Hashtable则不允许null作为key,初始容量和hashcode计算不同)
HashSet是简化版的HashMap,不能重复
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)
TreeMap 是红黑树
Properties是 Hashtable
array和arrayList,数组和列表的区别⭐
-
一 空间大小
1、Array的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。
2、ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大0.5倍的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。 -
二 存储内容
1、Array数组可以包含基本类型和对象类型。
2、ArrayList却只能包含对象类型。
需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。 -
三 方法
ArrayList作为Array的增强版,当然是在方法上比Array多样化。比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。
ArrayList和LinkedList的大致区别:
- ArrayList基于动态数组实现的非线程安全的集合;LinkedList基于链表实现的非线程安全的集合。
- 对于随机index访问的get和set方法,一般ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
- 新增和删除元素,一般LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。
- LinkedList集合不支持 高效的随机随机访问(RandomAccess)
- ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
HashMap的实现⭐
- 数组+链表
- 存储位置 = f(关键字),f为哈希函数,找到数组的存储位置
- 相同存储位置的时候会发生哈希冲突,hashmap使用链地址法来解决
- 发生hash冲突,且size大于阈值时候数组扩容两倍,数组是连续的全复制过去
- jdk1.8 优化:当链表长度大于8,数组长度大于等于64时候,链表转化成红黑树。
ConcurrentHashMap如何高效实现线程安全⭐⭐
- hashmap多线程下不安全容易成环,hashtable安全但是Synchronized修饰的,阻塞效率低,同时竞争一把锁
- concurrentHashMap 使用分离锁,将内部进行分段segment,里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。
- HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。
相比于JDK1.7版本,JDK1.8主要有两个改进。
详情2
-
1.取消了segment分段设计,直接使用Node数组来保存数据。并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率。
-
2.将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的数据结构。
方法具体流程
- get:不锁定表
- put:
- 空数组插入未加锁:volatile读->为空->cas封装成node插入
- 数组内node的链表插入加锁:头结点进行加锁,插入链表尾部数据
- size:锁定整个表
工具类collections
常用功能
- addAll(Collection c, T… elements) :往集合中添加一些元素。
- shuffle(List<?> list) 打乱顺序 :打乱集合顺序。
- sort(List list) :将集合中元素按照默认规则排序。
- sort(List list,Comparator<? super T> ) :将集合中元素按照指定规则排序
- public static void sort(List list,Comparator<? super T> ) :将集合中元素按照指定规则排序。
Comparable和Comparator俩接口的区别
JDK9 对集合添加的优化
of()方法只是Map,List,Set这三个接口的静态方法,创建集合的不可变实例。
数据结构
- 栈:stack
- 队列:queue
- 数组:Array
- 链表:linkedlist
- 红黑树:一颗二叉查找树BST+约束
迭代器
-
Iterator接口
要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,方法public Iterator iterator() -
常用方法:
public E next() :返回迭代的下一个元素。
public boolean hasNext() :如果仍有元素可以迭代,则返回 true。 -
原理
-
增强for
Map的遍历
-
增强for
-
(Entry)对象
for (Entry<String, String> entry : map.entrySet()) { // 解析 String key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"的CP是:"+value); }
泛型
概念和好处
使用含有泛型的类、泛型的方法、泛型的接口
泛型通配符
受限泛型
3 异常
详情
Throwable体系:
- Error:严重错误Error,无法通过处理的错误。
- Exception:可以通过代码的方式纠正。
异常处理
Java异常处理的五个关键字:try、catch、finally、throw、throws
4 线程⭐⭐⭐⭐⭐⭐
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
进程与线程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度:
- 分时调度 :所有线程轮流使用CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度 :优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
创建线程
- 法一 : 定义Thread类的子类,并重写该类的run()方法,创建Thread子类的实例,即创建了线程对象。
- 法二: 定义Runnable接口的实现类,并重写该接口的run()方法,创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象,调用线程对象的start()方法来启动线程。
- 法三:匿名内部类
Runnable r = new Runnable(){ public void run(){ for (int i = 0; i < 20; i++) { System.out.println("张宇:"+i); } } }; new Thread(r).start();
Thread和Runnable的区别
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
线程池⭐⭐
运行过程:
- 判断线程池的运行状态是否是RUNNING(ctl == 0?) False直接返回null
- currentPoolSize < corePoolSize ?
True —> 创建一个新的线程(其实是worker对象)放入线程池 - False —> currentWorkQueueSize < workQueueSize ?
True —> 将任务放入workQueue中 - Fasle —> 此时阻塞队列已满,判断 currentPoolSize < maxPoolSize ?
True —> 启动一个新的线程来执行新提交的任务 - False —> 执行拒绝策略
ThreadLocal⭐⭐
详解
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
- 每个Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, 在每个Thread类中有一个threadLocals 变量,是ThreadLocalMap类型的,为每一个线程来存储自身的ThreadLocal变量的, ThreadLocalMap是ThreadLocal类的一个内部类。
- 这个Map里面初始化了一个长度为16的数组table,数组的最小的存储单位是一个Entry(K,V)。
- 存储某一个ThreadLocal时,table的索引i=threadLocalHashCode进行一个位运算(取模),找到位置存放值,每个位置是一个Entry,set 这个Entry(threadlocal, Value)。调用set的时候,传入值就行,第一个参数是通过this获取的threadlocal对象。
一个线程一个table,不同threadlocal不同位置;不同线程不同table,同一threadlocal同一位置。
- 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
- 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
线程互斥同步(阻塞同步)⭐⭐
1.同步代码块。
synchronized(同步锁){
需要同步操作的代码
}
同步锁
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象可以是任意类型。
- 多个线程对象要使用同一把锁。
- 注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。 使用同步代码块解决代码:
2.同步方法。
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
同步代码块加this,和同步方法一样效果,锁属于实例
同步代码块加xxx.class,和静态同步方法一样,锁属于对象
3.Lock锁。
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
synchronized和ReentrantLock比较⭐
-
实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
-
性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
-
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
-
公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下是非公平的,但是也可以是公平的。 -
锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。
-
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
非阻塞式同步
- 乐观并发策略(不断地重试,直到成功为止)
- CAS(Compare-and-Swap,CAS) 指令需要有3个操作数,分别是内存地址V、旧的预期值A和新值 B。当执行操作时,只有当V的值等于A,才将V的值更新为B。
- AtomicInteger :J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
- ABA问题
线程状态
- NEW(新建) 创建未启动,没调用start方法。
- Runnable(可运行) :虚拟机中运行的状态,可能正在运行自己代码,也可能没有,取决于操作系统处理器。
- Blocked(锁阻塞): 一个线程试图获取一个被其他的线程持有的对象锁的状态;当该线程持有锁时Runnable状态。
- Waiting(无限等待): 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
- Timed Waiting(计时等待): 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
- Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
线程之间的协作,notify、join⭐
- join() :在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。实现也是通过wait,notify实现的,“你先wait我执行完,notify你”
- wait() notify() notifyAll():必须要由同一个锁对象调用:
- wait:线程不再活动,不再参与调度,进入 wait set 中,这时的线程状态即是 WAITING。当前线程会释放锁。
- notify:则选取所通知对象的 wait set 中的一个线程释放
- notifyAll:则释放所通知对象的 wait set 上的全部线程
- 3 await() signal() signalAll():await() 可以指定等待的条件,因此更加灵活
wait可以解决生产者消费者问题,BlockingQueue也可以解决
锁优化⭐⭐⭐⭐
这里的锁优化主要是指 JVM 对 synchronized 的优化。
- 自旋锁:让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
- 锁消除:被检测出不可能存在竞争的共享数据的锁进行消除。
- 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。第一个 append() 操作之前直至最后一个 append()操作之后(字符串拼接中的sb)
- 轻量级锁:JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
- 偏向锁:偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
volatile原理⭐
作用:
- 保证多线程可见性、不保证原子性
- 禁止指令重排序(JVM底层volatile是采用“内存屏障”)
使用它必须满足如下两个条件:
- 对变量的写操作不依赖当前值;
- 该变量没有包含在具有其他变量的不变式中。
添加链接描述
volatile 与 synchronized 比较
- volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized 要好; volatile 只能修饰变量,而 synchronized 可以修饰方法,代码块. 随着 JDK 新版本的发布,synchronized 的执行效率也有较大的提升,在开发中使用 sychronized 的比率还是很大的.
- 多线程访问 volatile 变量不会发生阻塞,而 synchronized 可能会阻塞
- volatile 能保证数据的可见性,但是不能保证原子性; 而synchronized 可以保证原子性,也可以保证可见性
- 关键字 volatile 解决的是变量在多个线程之间的可见性; synchronized 关键字解决多个线程之间访问公共资源的同步性
不参与线程竞争- 唤醒
原理:
- 缓存与主存:缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
- 工作内存与主存:当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。
硬件原子级Unsafe
- 1、通过Unsafe类可以分配内存,可以释放内存
- 2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;
- 3、挂起与恢复
- 4、CAS操作:compareAndSwapXXX方法
5 Lamada表达式
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法(函数式接口)。
- 使用Lambda必须具有上下文推断。方法的参数或局部变量类型必须为Lambda对应的接口类型。
6 File类
详情
构造:
- public File(String pathname)
- public File(String parent, String child)
- public File(File parent, String child)
常用方法:
- public String getAbsolutePath(
- public String getPath()
- public String getName()
- public long length()
- public boolean exists()
- public boolean isDirectory()
- public boolean isFile()
- public boolean createNewFile()
- public boolean delete()
- public boolean mkdir()
- public boolean mkdirs()
- public String[] list()
- public File[] listFiles()
7 IO
输入流 | 输出流 | |
---|---|---|
字节流 | 字节输入流 InputStream | 字节输出流 OutputStream |
字符流 | 字符输入流 Reader | 字符输出流 Writer |
字节流
- OutputStream
public void close()
public void flush()
public void write(byte[] b)
public void write(byte[] b, int off, int len)
public abstract void write(int b)- FileOutputStream
public FileOutputStream(File file)
public FileOutputStream(String name)
public FileOutputStream(File file, boolean append)
public FileOutputStream(String name, boolean append)
- FileOutputStream
- InputStream
public void close()
public abstract int read()
public int read(byte[] b)- FileInputStream
FileInputStream(File file)
FileInputStream(String name)
- FileInputStream
字符流
- Reader
public void close()
public int read()
public int read(char[] cbuf)- FileReader
FileReader(File file)
FileReader(String fileName)
- FileReader
- Writer
void write(int c)
void write(char[] cbuf)
abstract void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)
void flush()
void close()- FileWriter
FileWriter(File file)
FileWriter(String fileName)
- FileWriter
try-with-resource
自动close资源
8 流
缓冲流
4个基本的 FileXxx 流的增强:
- 字节缓冲流:
BufferedInputStream(InputStream in)
BufferedOutputStream(OutputStream out) - 字符缓冲流:
BufferedReader(Reader in)
BufferedWriter(Writer out)
转换流
- InputStreamReader
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, String charsetName) - OutputStreamWriter
OutputStreamWriter(OutputStream in)
OutputStreamWriter(OutputStream in, String charsetName)
序列化
- public ObjectOutputStream(OutputStream out)
序列化:类必须实现 java.io.Serializable 接口,该类的所有属性可序列化的。属性必须注明是瞬态transient 不会被序列化。
public final void writeObject (Object obj) : 将指定的对象写出。 - public ObjectInputStream(InputStream in)
public ObjectInputStream(InputStream in)
public final Object readObject ()
打印流
- print 、 println 方法、来自于java.io.PrintStream
- public PrintStream(String fileName):使用指定的文件名创建一个新的打印流。
- System.out 就是 PrintStream 类型
9 网络编程
软件结构
C/S结构 :全称为Client/Server结构。
B/S结构 :全称为Browser/Server结构。
TCP/IP五层模型和OSI网络七层协议⭐⭐
三次握手与四次挥手⭐⭐
网络编程三要素
协议、ip、端口号
TCP通信程序
- 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
- 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。
10 函数式接口
详情
有且仅有一个抽象方法的接口
@FunctionalInterface注解
函数式编程
11 Stream流
12 反射⭐
反射详解
反射的优缺点:
-
1、优点:在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
-
2、缺点:
(1)反射会消耗一定的系统资源,因此,如果不需要动态创建一个对象,那么就不需要用反射;
(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
13 注解
14 JVM
⭕详解Class类文件的结构
魔术(4字节)、副版本号(5、6) 主版本号(7、8)、常量池(9~)、访问标志(Class是类还是接口、是否定义为public、是否定义为abstract类型、类是否被声明为final)。。。
⭕Java内存模型(JMM)特性
是根据可见性、原子性以及有序性3个特性来建立的,是用来保证多线程下程序能够正确的运行的基础。
- 原子性:由Java内存模型的读写操作以及锁机制保证。
- 可见性:在单线程程序中不存在可见性问题。Java内存模型通过保证正确的多线程同步来实现可见性。
- 有序性:在Java内存模型中有内存屏障、指令重排序优化保证程序的有序性。
⭕运行时的内存区域
程序计数器
- 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
Java 虚拟机栈
- 存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应入栈和出栈的过程。
- 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为1M。
本地方法栈
- 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
堆
- 所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
- 现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:新生代(Young Generation)、老年代(Old Generation)
- 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError 异常。
- 可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
方法区
- 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
- 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
- HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
- 方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
运行时常量池
- 运行时常量池是方法区的一部分。
- Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
直接内存
- 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java堆里的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
⭕哪些对象需要回收?
-
引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。缺点:在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
-
可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
虚拟机栈中局部变量表中引用的对象 本地方法栈中 JNI 中引用的对象 方法区中类静态属性引用的对象 方法区中的常量引用的对象
引用计数法和可达性分析的在jvm具体工作流程
-
方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。主要是对常量池的回收和对类的卸载。
对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件,也不一定会被卸载:
该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。 加载该类的 ClassLoader 已经被回收。 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
-
finalize()
缓期2次回收,当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
⭕引用的类型
-
强引用
被强引用关联的对象不会被回收,
使用 new 一个新对象的方式来创建强引用。 -
软引用
被软引用关联的对象只有在内存不够的情况下才会被回收,
使用 SoftReference 类来创建软引用。 -
弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前,
使用 WeakReference 类来创建弱引用。 -
虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
唯一目的是能在这个对象被回收时收到一个系统通知,使用 PhantomReference 来创建虚引用。
⭕⭕垃圾收集算法
- 标记 - 清除
- 标记阶段,程序会检查每个对象是否为活动对象,活动对象打上标记。
- 清除阶段,会进行对象回收并取消标志位。另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
- 在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
- 不足:
标记和清除过程效率都不高;
会产生大量不连续的内存碎片,导致无法给大对象分配内存。
- 标记 - 整理
- 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 优点:
不产生内存碎片 - 不足:
需要移动大量对象,处理效率比较低。
- 复制
- 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
- 不足:只使用了内存的一半。
- 商业虚拟机都采用这种收集算法回收新生代。新生代中8份和1份复制到1份上去。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
- HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。回收有多于 10% 的对象存活,一块 Survivor 就不够用了,借用老年代的空间存储放不下的对象。
- 分代收集
-
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
-
一般将堆分为新生代和老年代。
新生代使用:复制算法
老年代使用:标记 - 清除 或者 标记 - 整理具体过程:回收时eden区->survivor0,清空eden区,当survivor0区满,则eden区+survivor0区->survivor1区,清空eden和这个survivor0区,然后将survivor0区和survivor1区交换,即保持survivor1区为空。
当survivor1区不足以存放 eden和survivor0的存活对象时,存放到老年代。老年代满了触发一次Full GC,也就是新生代、老年代都进行回收。
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
⭕常见的垃圾收集器
-
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
-
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
-
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
-
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
-
Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
-
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除:不需要停顿。在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
缺点:
1. 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
2.无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
3.标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 -
G1 收集器(标记-整理算法)
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能,使命是未来可以替换掉 CMS 收集器。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
每个 Region 都有一个 Remembered Set,记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:1-初始标记
2-并发标记
3-最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
4-筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。具备如下特点:
空间整合:
整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是 基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:
能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
⭕内存分配策略
-
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 -
大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
-
长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。-XX:MaxTenuringThreshold 用来定义年龄的阈值。
-
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 -
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象
⭕⭕JVM的垃圾回收过程与垃圾收集器详解
-
我们常说的GC一般指的是堆区的垃圾回收,堆内存空间可以进一步划分新生代和老年代,新生代会发生Minor GC,老年代会发生Full GC。
-
JVM把年轻代分成三部分:一个Eden区和两个Survivor区(即From区和To区),比例为8:1:1。
-
当Eden区没有足够的内存空间给对象分配内存时,虚拟机会发起一次Minor GC,在GC开始的时候,对象会存在Eden和From区,To区是空的。进行GC时,Eden区存活的对象会被复制到To区,From区存活的对象会根据年龄值决定去向,达到阈值(默认15)的对象会被移动到老年代中,没有达到阈值的对象会被复制到To。
(但有可能存在没有达到阈值就从Survivor区直接移动到老年代的情况:在进行GC的时候会对Survivor中的对象进行判断,Survivor空间中年龄相同的对象的总和大于等于Survivor空间一半的话,年龄大于或等于该年龄的对象就会被复制到老年代;在把Eden区的对象复制到To区的时候,To可能已经满了,这个时候Eden中的对象就会被直接复制到老年代中)。
-
这时Eden区和From区已经被清空了。接下来From区和To区交换角色,以保证To区在GC开始时是空的。Minor GC会一直重复这样的过程,直到To区被填满,To被填满之后,会将所有对象移动到老年代中。
-
如果老年代内存空间不足,则会触发一次Full GC。
⭕Full GC触发条件
-
调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 -
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 -
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 -
JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
-
Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
⭕Java虚拟机:类加载机制与双亲委派模型
-
定义: 类加载机制,就是虚拟机把类的数据从class文件加载到内存,并对数据进行校检,准备、解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。
-
生命周期七个阶段、类加载五个:加载——验证——准备——解析——初始化——使用——卸载
-
类加载器
启动类加载器(Bootstrap ClassLoader)
扩展类加载器(Extension ClassLoader)
应用类加载器(Application ClassLoader)
还可以加入自己定义的类加载器 -
双亲委派模型:
1 原因:
JVM的类加载机制,规定一个类有且只有一个类加载器对它进行加载
2 定义:
双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父类加载器。(类加载器之间的父子关系不是以继承的关系实现,而是使用组合关系来复用父加载器的代码)
3 过程:
(父亲干不了给儿子)加载过程可以看成自底向上检查类是否已经加载,然后自顶向下加载类。
4 优点:
(1)使用双亲委派模型来组织类加载器之间的关系,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
(2)避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没必要再加载一次。
(3)解决各个类加载器的基础类的统一问题,越基础的类由越上层的加载器进行加载。避免Java核心API中的类被随意替换,规避风险,防止核心API库被随意篡改。
⭕打破双亲委派
1.第一次破坏
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
2.第二次破坏
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。
如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,
它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
3.第三次破坏
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。