Java基础
Java 基础
1、动态代理
java动态代理好文章
1.1、设计模式-代理模式
代理类与委托类有相同的接口

1.2、静态代理
在编译是就已经将接口、代理类、被代理类确定下来。在程序运行之前,代理类的.class文件就已经生成。
1.3、动态代理
代理类根据我们在Java代码中的指示动态生成。动态代理的优势 在于很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。 使用反射的原理,使用Proxy类传入classloader与代理类接口,创建的代理类会使用IvocationHandler的invoke接口调用各个代理对象的接口。
2、Java值传递
Java中只有值传递,没有引用传递。
3、Java Collection 集合
3.1、Collection的整体结构
- Collection 有三种子类集合 Set、List、Queue, 具体的实现类有ArrayList, LinkedList, HashSet, LinkedHashSet, ArrayBlockingQueue等。
3.2、Iterator、ListIterator
- Collection继承Iterator, ListIterator继承Iterator
- Iterator中含有hasNext next等接口方法,在ArrayList当中,Iterator的实现相当于用游标的形式返回元素,所以可以在用iterarot遍历数组的过程中删除对应的元素,游标由内部进行管控,不会出错。
- 在iterator当中没有currentElement, 它的cursor永远停留在调用previous()返回的前一个元素与next()返回的后一个元素中间,官方解释为:
An iterator for lists that allows the programmer to traverse the list in either direction, modify the list during iteration, and obtain the iterator's current position in the list. A ListIterator has no current element; its cursor position always lies between the element that would be returned by a call to previous() and the element that would be returned by a call to next(). An iterator for a list of length n has n+1 possible cursor positions, as illustrated by the carets (^) below: Element(0) Element(1) Element(2) ... Element(n-1) cursor positions: ^ ^ ^ ^ ^Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().
- 为什么集合不直接实现iterator接口而直接实现iterable接口呢?
因为迭代器中关键方法是next与hasnext,里面包含了集合的迭代位置,如果由集合本身来实现,那么集合的类成员变量中则会包含迭代位置,当集合在不同的方法当中被传递时,会出错,这是程序设计的一个考虑。
3.3、List集合
- List常用子类:ArrayList、LinkedList、Vector
- Set 集合中常用的子类:HashSet、TreeSet、LinkedHashSet
4、Java运行时数据区域
4.1、程序计数器
- 一块较小的内存空间,可以看做是当前线程执行Java字节码的行号指示器。
- 线程私有,每个线程都系要独立的一个程序计数器
- 执行Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,执行native方法计数器的值是空。
4.2、Java虚拟机栈
- 线程私有
- 每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表,方法出口、操作数栈,动态链接等信息。每个方法从调用直至执行完成,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
- 我们所说的栈,一般也指虚拟机栈中的局部变量表,局部变量表中存储了编译期可知的基本变量类型,对象引用等。其中long 和 double 是64位长度的,占两个局部变量空间(slot), 其他的基本数据类型占一个slot。当一个方法进入时,需要用到多大的局部变量空间是完全确定的,在方法运行时不会改变。
*该空间定义的异常:StackOverFlowError OutOfMemoryError
4.3、本地方法栈
- Java虚拟机栈执行Java方法,本地方法栈执行Native方法
- 该空间定义的异常:StackOverFlowError OutOfMemoryError
4.4、Java堆
- 线程共享
- 存放对象实例
- 定义的异常:OutOfMemoryError
4.5、方法区
- 线程共享
- 存储已被虚拟机加载的类信息,常量、静态变量、即使编译后的代码等数据。
- 定义异常:OutOfMemoryError
4.6、运行时常量池(方法区的一部分)
- class文件中除了类的版本、字段、方法、接口等木奥术信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。
5、虚拟机对象探秘
5.1 对象的创建
- 类加载检查:虚拟机遇到new指令,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用对应的类是否已被加载、解析和初始化过。如果没有,则先执行类的加载过程。
- 为新生对象分配内存
- 对象初始化设置,例如对对象头的设置(那个类的实例、对象的哈希码,GC分代年龄等)
5.2、对象的内存布局:
- 对象头:共有两部分信息,一部分用于存储对象自身的运行时数据如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称为Mark Word, 在32位虚拟机中为32位大小。 另一部分为类型指针,指向类元数据,通过该指针可以确认是哪个类的实例。

- 实例数据:对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。
- 对齐填充:占位符作用,对象大小必须是8字节的整数倍(因为HotSpot VM的自动内存管理系统对对象的起始地址必须是8字节的整数倍)
5.3、对象的访问定位
- 句柄访问: Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的具体地址信息。优势:对象被移动时reference本身不需要修改,只需改变句柄中的实例数据指针。

- 直接指针访问:栈中的reference中直接存储的就是对象的地址,其中对象的类型数据通过到对象类型数据的指针指示。 优势:访问速度快,节省了一次指针定位的耗时。

string.intern()是一个native方法,如果字符串常量池中已经包含一个等于该string对象的字符串,则返回常量池中这个字符串的string对象,否则将该string对象包含的字符串加到常量池中。
6、垃圾收集器与内存分配策略
6.1 对象已死吗?
- 引用计数法,给对象添加一个引用计数器,每当有一个地方引用,计数器的值就+1。无法解决循环引用问题
- 可达性分析算法:gc root对象作为起始点,从起始点向下搜索,走过的路径称为引用链,如果一个对象到gc root没有任何引用链,那么该对象不可达,可以进行回收。 GC ROOT的对象可以为:虚拟机栈中的引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象。

6.2、引用
- 强引用,只要被引用,对象就不会被回收
- 软引用,在系统将要内存溢出之前,会对这些对象进行回收
- 弱引用,在下一次GC之前被回收。
- 虚引用,不会对对象生命周期造成影响,唯一用处就是在对象回收之前收到一个系统通知。
一个对象的死亡至少要经历两次标记的过程,如果在对象进行可达性分析知识,对象没有与任何一个gc root的引用链,它会进行第一次筛选,如果重写了finalize方法并且没有被执行过,该对象会被放置到一个f-queue当中,并且用虚拟机创建的低优先级finalizer线程去执行。在finialz方法中对象可以机进行一次自我拯救。
6.3、回收方法区
方法区垃圾手机的两部分内容为:废弃常量与无用类。
无用类的判定党发:
* 该类的所有实例已被回收
* 该类的classloader已被回收
* 该类对应的java.lang.class对象没有在任何地方被医用,无法在任何地方通过反射方位该类。
6.4、垃圾收集算法
- 标记清除算法。不足:效率不高、空间问题(产生大量不连续内存碎片,后续程序给大对象分配内存时,无法找到足够连续内存而不得不提前出发一个gc)
- 复制算法。将内存容量分为相等大小两块,每次只使用一块分配内存,当这块内存用完时,存活的对象复制到另一块,并将原来这块清空。IBM公司的专门研究表明,新生代的对象98%是朝生夕死的,所以并不需要1:1分配,将内存可分为较大的一块Eden空间与两块较小的survivor空间。每次使用eden与一个survivor空间,hotspot eden:survivor = 8 :1, 每次gc将存活对象复制到另一个survivor空间。这里有朋友可能回想万一survivor空间不够怎么办,虽然研究表明98%的对象是朝生夕死的,但万一出现了例外怎么处理,当survivor空间不够时,需要依赖其他其他内存,即老年代进行分配担保。如果复制到新的survivor空间内存不够,则这些对象进入里老年代,这个后续讲解。
- 标记-整理算法。上面的复制算法对于对象存活率高的场景,要进行很多的复制,效率降低。对于老年代,标记-整理算法更实用。这里与清除算法一致,但不是直接清理掉不可达的对象,而是将存活对象都向一端移动,然后清理掉边界意外的内存。
- 分代收集算法。根据对象的存活周期的不同将内存或分为几块,一般为新生代和老年代,不同内存区域采用不同收集算法,比如新生代使用复制算法,老年代使用标记整理或者标记清除算法。
6.5、HotSpot的垃圾收集算法实现
- 新生代GC Minor GC:因为java对象大多朝生夕死,所以发生非常频繁,一般是复制算法
- 老年代GC Mjor GC、Full Gc:比minorgc 一般慢十倍。一般是标记整理或者标记清除算法。
- 对象优先在Eden分配
- 大对象直接进入老年代,避免发生大量内存复制
- 长期存活对象进入老年代。eden的对象每熬过一次minor gc就年龄增加1岁,当年龄增加到一定程度,默认15岁就晋升到老年代。
- 动态对象年龄判断。上面进入老年代的年龄不是固定的,如果survivor空间中相同年龄的所有对象的大小总和大于大小总和大于survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。
- 空间分配担保,上面提到的问题。在发生minor gc以前,虚拟机会检查老年代的最大可用空间是否大于新生代所有对象的总空间,如果大于,那么minor gc是确保安全的。如果不大于,则会检查是否允许担保失败,如果不允许,则出发一个full gc得到更多的空间,如果允许,则老年代会检查剩余空间是否大于历次从survivor中晋升到老年代的对象平均大小,如果大于,则进行一次有风险的minor gc。如果小于,就进行一次full gc。
7、类文件结构
todo 目前为止不太用得到,而且看完以后很容易忘,感觉需要哪天涉及到字节码插桩等项目时具体去深入。毕竟实践才是最佳的学习。
8、类加载机制
8.1、类加载时机
- 类的整个生命中期包括:loading -> verification -> preparation -> resolution -> initialization -> using -> unloading
其中加载 验证 准备 初始化 卸载这个五个阶段顺序时确定的。解析不一定。
- 加载阶段时机没有规定,初始化时机时确定的,遇到 new getstatic putstatic invokestatic
- 进行类反射时
- 初始化一个类,发现其父类未初始化,需要堆父类进行初始化
- 通过子类引用父类的静态字段,不会导致子类的初始化
- 通过数组来定义引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
8.2、类加载的过程
- 加载:a、通过类的全限定名来获取定义此类的二进制字节流 b、将字节流所代表的静态存储结构转化成方法区运行时的数据结构。c、在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各个数据的访问入口。 加载阶段既可以由系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。
- 验证 各种验证符号引用验证、字节码验证、元数据验证、文件格式验证
- 准备 正式未类变量分配内存病设置类变量初始值的阶段,都在方法区分配,不包括实例变量。 这里的初始值通常情况下时数据类型的零值,非通常情况如类字段中表明的final常量,则使用这个常量值。
- 解析。 将符号引用提出按称为直接引用过程。
- 初始化。 a、初始化阶段时执行类构造器()方法的过程。()方法是编译器自动收集类中的所有被变量的复制动作和静态语句块的语句合并产生的。收集顺序由语句在源文件中出现的顺序决定的。静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。b、方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证子类的()方法执行之前,父类的()方法执行完毕。因此第一个被执行()方法的类肯定是java.lang.Object
- 由于上面的原因,父类的静态语句块要优于子类的变量赋值操作。
8.3、类加载器
- 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
- 双亲委派模型 系统提供的类加载器:
1、启动类加载器,负责将存放在\lib目录中的,或者被- Xbootclasspath参数指定路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。 该加载器无法被Java程序直接引用,用户如果需要把加载请求委派个引导类加载器,直接用null替代即可。
2、扩展类加载器:负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的类库,开发者可以直接使用该加载器。
3、应用程序类加载器:也称为系统类加载器,负载加载用户类路径上锁指定的类库。如果应用程序中没有自定义的类加载器,一般情况下这就是程序中默认的类加载器。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有父类加载器反馈无法完成这个加载请求,子加载器才会尝试自己去加载。
例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
9、Java内存模型与线程
由于计算机的存储设备与处理器的运算速度由几个量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器的高速缓存来作为内存与处理器之间的缓冲。 由于缓存的存在,很好的解决了处理器与内存的速度矛盾,但也增加了系统复杂性,带来了缓存一致性问题。每个处理器都有自己的告诉缓存,而他们又共享同一主内存。为了解决一致性问题,缓存在对主内存进行读写时,要遵循一个协议。

为了使得处理器内部的运算单元能被充分利用,处理器可能会对输入代码乱序执行优化。
- Java内存模型规定了所有的变量(静态变量,堆中的实例变量)都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了呗该线程使用的变量和主内存副本拷贝,线程堆变变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间无法访问对象工作内存中的变量。

9.1、内存间的交互操作:
- 有一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,java内存模型定义了以下八种操作来完成。
a、lock,锁定,作用于主内存变量,把变量标识未一条线程独占的状态。
b、unlock,解锁,作用于主内存变量,把一个处于锁状态的变量释放出来,释放后的变量可以被其他线程锁定。
c、read,读取,作用于主内存变量,把一个变量的值从主内存传输到线程工作的工作内存中
d、load载入,作用于工作内存变量,把read操作从主内存中得到的变量值放入工作内存的副本中。
e、use 使用 作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
f、assign 复制,作用于工作内存变量,把一个从执行引擎收到的值赋给工作内存的变量。
g、store 存储,作用工作内存变量,把工作内存的变量的值传到主内存中。
h、write 写入,作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。
执行以上操作必须满足一下规则:
a、read和load, store和write不允许单独出现
b、不允许一个线程丢弃它最近的assignn操作,即变量在工作内存中改变后必须要把该变化同步会主内存。
c、果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作
d、一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
e、一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
f、如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
g、如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量
h、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
9.2、对于volatile变量的特殊规则
- 对于volatile变量,特性之一时可见性,保证了此变量对所有线程的可见性,当一个线程修改了这个变量的值,新值对于其他线程来说时可以立即得知的,而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
- 第二个特性时禁止指令重排序优化,
- volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
9.3、Java与线程状态
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:● 没有设置Timeout参数的Object.wait()方法。● 没有设置Timeout参数的Thread.join()方法。● LockSupport.park()方法。
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

10、线程安全与锁优化
10.1、线程安全的实现方法