Java核心机制
Java虚拟机:相当于一块将字节码转换为机器码的CPU
垃圾回收机制(GC)
Java运行原理
代码程序运行,通过编译器将Java源代码转换为.class文件,然后.class文件通过JVM转换为机器码。
JDK,JRE,JVM三者之间的关联
JDK:Java运行环境工具。JDK = JRE + Java核心工具类。
JRE:Java运行环境。JRE = JVM + Java运行类库。
JVM : Java虚拟机。包含了Java最核心的类库。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-joUvQgkx-1581551561017)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581468376029.png)]
执行过程:
Java源代码(.java)经过编译器(javac.exe)编译之后,并没有直接转换为机器码,
而是转换成一种中间格式——字节码(.class)【二进制】,字节码再经过Java虚拟机
编译,转换为机器码,然后经由操作系统达到CPU。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLZ8YCKz-1581551561020)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581468589756.png)]
Java内存区域:
JVM内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【Java堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁,每个线程都与操作系统的本地线程直接映射,
因此这部分内存区域的存/否跟随本地线程的生/死对应。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是JVM运行时数据区的一部分,但也会被频繁的使用:在JDK1.4引入的NIO提供了基于Channel与Buffer的IO方式,
它可以使用Native函数库直接分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用进行操作,这样就避免了在
Java堆和Native堆中来回复制数据,因此在一些场景中可以显著提高性能。
堆:
是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的的最重要的内存区域。
由于现代JVM采用分代收集算法,因此Java堆从GC的角度还可以细分为新生代(Eden区、From Survivor区和To Survivor
区)和老年代。
新生代:
Eden区
Survivor(From)区:设置Survivor是为了减少送到老年代的对象。
Survivor(To)区:设置两个Survivor区是为了解决碎片化的问题
老年代:
-Xms:堆的最小值
-Xmx:堆的最大值
-Xmn:新生代的大小
-XX:NewSize:新生代最小值
-XX:MaxNewSize:新生代最大值
方法区:
永久代,用于存储被JVM加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。HotSpot VM把GC分代收集扩展至方法区,
即使用Java对的永久代来实现方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门
的内存管理器(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存储
编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对Class文件的每一部分
(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规定上的要求,这样才会被虚拟机认可、装载和执行。
运行时常量池:
Class文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
存储信息:
符号引用:
符号引用包含常量:
类符号引用
方法符号引用
字段符号引用
字面量:
文本字符串
八种基本数据类型
声明为final的常量
各版本内存区域的变化:
JDK1.6:运行时常量池在方法区
JDK1.7:运行时常量池在堆
JDK1.8:运行时常量池在元数据区
静态变量
final类型常量
类信息
类的完整有效名、返回值类型、修饰符、变量名、方法名、方法代码、类的直接接口的一个有序列表
JDK1.7以及之前
-XX:PermSize
-XX:MaxPermSize
JDK1.8以后
只受本地总内存的限制
-XX:MaxMetaspaceSize=3M
栈:
是描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧:
栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,
随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
动态链接:
符号引用和直接引用在运行时进行解析和链接的过程。
前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
操作数栈:
保存着Java虚拟机执行过程中的数据
局部变量表:
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,
在方法运行期间不会改变局部变量表的大小。
存放的信息
基本数据类型
对象引用
returnAddress类型
方法返回地址:
方法被调用的位置
方法退出的过程实际上就等同于把当前栈帧出栈
方法退出可能包含的操作
恢复上层方法的局部变量表和操作数栈
把返回值(如果有的话)压入调用者栈帧的操作数栈中
把PC计数器的值以指向方法调用指令后面的一条指令
栈帧的大小缺省为1M,可用参数-Xss调整大小。例如-Xss256K
虚拟机栈和线程的生命周期相同
一个线程中,每调用一个方法创建一个栈帧。
栈帧的结构:
本地变量表 Local Variable
操作数栈
对运行时常量池的引用
异常:
线程请求的栈深度大于JVM所允许的深度:StackOverflowError
若JVM允许动态扩展,若无法申请到足够内存:OutOfMemoryError
在编译程序代码的时候,栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的
Code属性之中,因此一个栈帧需要分配多个内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9qehU7q6-1581551561023)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581470249293.png)]
程序计数器:
一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为"线程私有"的内存。
如果执行 Java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是 native 方法,则为空,undefined
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
本地方法区:
本地方法区和Java Stack类似,区别是虚拟机栈为执行Java方法服务,而本地方法栈则为Native方法服务,如果一个VM实现使用C-linkage
模型来支持Native调用,那么该栈将会是一个C栈,但HotSpot VM直接把本地放方法栈和虚拟栈合二为一。
和虚拟机栈类似,区别是本地方法栈为使用到的 Native 方法服务
直接内存:
使用Native函数库直接分配堆外内存
并不是JVM运行时数据区域的一部分,但是会被频繁使用
避免了在Java和Native堆中来回复制数据,能够提高效率
如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用DirectByteBuffer对象直接引用并操作;
这块内存不受Java堆大小限制,但受本地总内存的限制,可以通过-XX:MaxDirectMemorySize来设置
(默认与堆内存最大值一样),所以也会出现OOM异常。
GC要做的三件事:
哪些内存需要回收?(对象是否存活)
引用计数法
快,方便,实现简单,缺点:对象相互引用时,很难判断对象是否该回收。
在Java中,引用和对象是有关联的。如果要操作对象则必须要用引用进行。因此,很显然的一个简单的算法时通过引用
计数法来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都为0,则
说明对象不太可能再被用到,那么这个对象就是可回收对象。
缺陷:循环引用会导致内存泄漏
可达性分析
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。需要注意的是,不可达对象不等价于可回收对象,
不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
来判定对象是否存活的。这个算法的基本思路是就是通过一系列的称为"GC Roots"的对象作为你起始点,
从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,
则说明此对象是不可用的。
作为GC Roots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象(当前本地栈中局部变量表中的引用的对象)
判断一个对象是否可回收的过程
1.找到GC Roots不可达的对象,如果没有重写 finalize()或者调用过 finalize(),则该对象假如到F-Queue中。
2.再次进行标记,如果此时对象还未与GC Roots建立引用关系,则被回收。
什么时候回收?
怎么回收?
垃圾收集算法:
标记清除:
最基础的垃圾回收算法,分为两个阶段,标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象锁占用的空间。
过程:
1.将需要回收的对象标记起来
2.清除对象
缺陷:
效率低、内存碎片多
复制:
为了解决标记清除算法内存碎片化的缺陷而提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存
满后将尚存活的对象复制到另一块上去,把已使用的内存清除掉。
新生代使用的是复制算法
优点:
简单高效,不会出现内存碎片问题
缺陷:
1.内存利用率低
2.存活对象较多时效率明显会降低
标记整理:
结合标记清除和复制算法的缺陷而提出。标记阶段和标记清除阶段相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除
端边界外的对象。
分代收集:
分代收集算法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分
为老年代和新生代。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,
因此可以根据不同区域选择不同的算法。
新生代与复制算法:
每次垃圾收集都能发现大批对象已死,只有少量存活。因此选用复制算法,只需要付出少量存储对象的复制成本就可以完成收集。
老年代与标记整理算法:
因为对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清除"或"标记-整理"算法来进行回收,不必进行内存复制,
且直接腾出空闲内存。
根据各个年代的特点选取不同的垃圾收集算法
新生代使用复制算法
老年代使用标记-整理或标记-清除算法
分区收集:
分区算法则是将整个堆空间划分为连续的不同小空间,每个小空间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间,
根据目标停顿时间,每次合理得回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5pktRzt5-1581551561024)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581479877379.png)]
垃圾收集器:
Serial:
串行收集器:Serial收集器
一个单线程的收集器,在进行垃圾收集的时候,必须暂停其他所有的工作线程直到它收集结束。
使用在新生代。
对应的JVM参数:-XX:+UseSerialGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseSerialGC
ParNew
并行收集器:ParNew
使用多线程进行垃圾回收,在垃圾回收时,会stop-the-world暂停其他工作线程直到它收集结束。
ParNew收集器就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代CMS GC工作,其余的行为和
Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程.它是很多Java虚拟机
运行在Server模式下新生代的默认垃圾收集器。
使用在新生代。
常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代
备注:
-XX:ParallelGCThreads 限制线程数,默认开启和CPU数目相同的线程数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParNewGC
Parallel Scavenge
并行回收GC
使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。串行收集器在新生代和老年代的并行化。
使用在新生代。
重点关注:
可控制的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即程序运行100分钟,垃圾收集1分钟,吞吐量就是99%)。
高吞吐量意味着高效利用CPU时间,它多用于后台运算而不需要太多交互的任务。
自适应调节策略也是ParallelScanvenge收集与ParNew收集器的一个重要区域。
自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间
(-XX:MaxGCPauseMillis)或最大的吞吐量。
常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC是使用Parallel Scanvenge收集器
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
-XX:ParallelGCThreads=数字N 表示启动N个线程
CPU > 8 N=5/8
CPU < 8 N = 实际个数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseParallelOldGC
Serial Old
Serial Old:
是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要运行在Client默认的Java虚拟机默认
的老年代垃圾收集器。
使用在老年代。
在Server模式下,只要有两个用途:
1.在JDK1.5之前版本中与新生代的Parallel Scanvenge收集器搭配使用。
2.作为老年代版中使用CMS收集器的后背垃圾收集方案。
Parallel Old:
是Parallel Scanvenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
在JDK1.6之后,Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量的要求较高,
JDK1.8之后可以优先考虑新生代ParallelScanvenge和老年代Parallel Old收集器的搭配策略。
JVM常用参数:
-XX:+UseParallelOldGC使用Parallel Old收集器,设置参数后,新生代Parallel+老年代Parallel Old.
CMS-Concurrent Mark Sweep
CMS收集器:Concurrent Mark Sweep,并发标记清除:是一种以获取最短时间回收停顿时间为目标的收集器。
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
并发收集低停顿,并发指的是与用户线程一起使用
使用在老年代
开启该收集器的JVM参数:-XX:+UseConcurrentMarkSweepGC 开启该参数后会自动将-XX:+UseParNewGC打开
开启该参数后,使用ParNew(新生代使用)+CMS(老年代使用)+Serial Old的收集器组合,Serial Old作为CMS的
后备收集器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
过程:
1.初始标记:只是标记一下GC Roots能直接关联到的对象,速度很快,仍然需要暂停所有的工作线程
2.并发标记和用户线程一起:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
主要标记过程,标记全部对象。
3.为了修正在并发标记期间,因用户继续运行而导致标记变动的那一部分对象的标记记录,仍然需要暂停
所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,在做修正。
4.并发清除和用户线程一起:
消除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上
来看CMS收集器的内存回收和用户线程是一起并发的执行。
优点:
并发收集低停顿
缺点:
1.并发执行,对CPU资源压力大
2.采用的标记清除算法会导致大量内存碎片
G1收集器
G1收集器,是一款面向服务端应用的收集器。
应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。
G1垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比于CMS收集器,G1收集器两个最突出的改进是:
1.基于标记-整理算法,不产生内存碎片
2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
Xms
Xmx
Xmn
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=xxx
-XX:MaxTenuringThreshold
-XX:-HandlePromotionFailure
Java四种引用类型
强引用:
垃圾回收期不会回收它,当内存不足时宁愿抛出OOM异常,使得程序异常终止。
在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,
它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java
内存泄漏的主要原因之一。
Object obj = new Object();
软引用:SoftReference
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它。
软引用非常适合于创建缓存。当系统内存不足时,缓存中的内容是可以被释放的。
弱引用:WeakReference
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。
虚引用:PhantomReference
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
类的生命周期:
加载:
1.会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。
2.这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中获取(比如从jar包和war包中读取),
也可以在运行时计算生成(动态代理),也可以由其他文件生成(比如将JSP文件转换为对应的Class类)
验证:
确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:
正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
解析:
虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用就是Class文件中的类常量,属性常量,方法常量等类型的常量
符号引用:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,
但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化:
1.初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器之外,
其他操作都是由JVM主导。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
2.类构造器<cinit>:
初始化阶段是执行类构造器<cinit>方法的过程。<cinit>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中
的语句合并而成的。虚拟机会保证子<cinit>方法执行之前,父类的<cinit>方法已经执行完毕,如果一个类中没有对静态变量
赋值也没有静态语句块,那么编译器可以不为这个类生成<cinit>()方法。
3.以下几种情况不会执行类初始化:
1.通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2.定义对象数组,不会触发该类的初始化。
3.常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发 定义常量所在的类。
4.通过类名获取 Class 对象,不会触发类的初始化
5.通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个 参数是告诉虚拟机,是否要对类进行初始化。
6.通过ClassLoade默认的loadClass方法,也不会触发初始化动作。
使用
卸载
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4H2dZ9L-1581551561026)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581483119220.png)]
类加载器:
启动类加载器
C++实现,是虚拟机自身的一部分
负责将存放在<JRE_HOME>\lib目录中的类库加载到虚拟机内存中
负责加载 JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
其他类加载器
由Java实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader.
分类:
自定义类加载器:
用户根据需求自定义的,需要继承自ClassLoader
应用程序类加载器
负责加载用户类路径(Classpath)上所指定的类库
负责加载用户路径(classpath)上的类库。JVM通过双亲委派模型进行类的加载,当然也可以通过继承java.lang.ClassLoader
实现自定义的类加载器。
扩展类加载器
负责将<JAVA_HOME>/lib/ext或者被java.ext.dir系统变量所指定路径中的所有类库加载到内存。
负责加载 JAVA_HOME\lib\ext目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
双亲委派模型:
内容:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,
只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
实现:
首先检查类是否被加载;
若未加载,则调用父类加载器的loadClass方法;
若该方法抛出 ClassNotFoundException 异常,则表示父类加载器无法加载,则当前类加载器调用 findClass 加载类;
若父类加载器可以加载,则直接返回 Class 对象。
好处:
保证Java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题。
破坏:
基础类需要调用用户的代码
解决方式
线程上下文类加载器
也就是父类加载器请求子类加载器去完成类加载的动作,这种行为
实际上就是打通了双亲委派模型层次结构来逆向使用累加载器,实际上已经违背了双亲委派模型的一般性原则
实现方法
重写 ClassLoader 类的 loadClass()
示例:
JDBC
原生的JDBC 中的类是放在 rt.jar 包的,是由启动类加载进行类加载的,JDBC中的 Driver 类中需要动态区加载不同的数据库类型的 Driver 类
JNDI 服务需要调用独立厂商并部署在引用程序的 ClassPath 下的 JNDI 接口提供者的代码
重写 loadClass() 方法
双亲委派模型的具体实现就在 loadClass() 方法中
用户对程序的动态性的追求
例如OSGi(面向Java的动态模型系统)的出现。在OSGi环境下,类加载器不再是 双亲委派模型中的树状结构,而是进一步发展为网状结构。
代码热替换、模块热部署
OSGI(动态模型系统):
是面向Java的动态模型系统,是Java动态化模块化系统的统一系列规范。
动态改变构造
OSGi服务平台提供在多种网络设备上无需启动的动态改变构造功能。为了最小化耦合度和
促使这些耦合度可管理,OSGi技术提供了一种面向 服务的架构,它能使这些组件动态的发现的对方。
模块化编程与热插拨
OSGi旨在为实现Java程序的模块化编程提供基础条件,基于OSGi 的程序很可能可以实现模块化级的热插拨功能,当程序升级更新时,
可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序 可开发来说是非常具有诱惑力的特性。
OSGi描绘了一个很美好的模块化开发目标,而是定义了实现这个目标 的所需服务与架构,同时也有成熟的框架进行实现支持。
但并非所有 的应用都适合采用OSGi作为基础架构,它在提供强大功能同时,也引入 了额外的复杂度,因为它不遵守了类加载的双亲委派模型。
类加载过程:
加载
将编译后的.class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到。
验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
解析
将 class 文件的常量池的符号引用替换为直接引用的过程(是静态连接)。
可能发生在初始化阶段之前,也可能发生在初始化阶段之后,后者是为了支持Java的动态绑定
初始化
为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序(执行<cinit>()方法).
类加载方式:
1.命令行启动应用程序的时候由 JVM 初始化加载。
2.通过 Class.forName() 方法动态加载
3.通过 Class.loadClass() 方法动态加载
类加载时机:
遇到new,getStatic,putStatic,invokeStatic这四条指令
调用一个类的静态方法
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区
的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,
并且向Java程序员提供了访问方法区内的数据结构的接口。
加载过程详解:
* 概述
> 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)
于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
* 注意
> 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
常量HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值“hello world”存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际都被转化为NotInitialization类对自身常量池的引用了。
也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
* 加载阶段
> 虚拟机需要完成以下3件事情:
1. 通过一个类的全限定名来获取定义此类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
* 验证
> 是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
* 准备阶段
> 是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。表7-1列出了Java中所有基本数据类型的零值。
假设上面类变量value的定义变为:public static final int value=123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
* 解析阶段
> 是虚拟机将常量池内的符号引用替换为直接引用的过程
* 类初始化阶段
> 是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。
Tomcat本身也是一个Java项目,因此其他也需要被JDK的类加载机制加载,也就必然存在引导类加载、扩展类加载器和应用(系统)类加载器。
Common ClassLoader作为Catalina ClassLoader和Shared ClassLoader的parent,而Shared ClassLoader又可能存在多个
children类加载器WebApp ClassLoader,一个WebApp ClassLoader实际上就对应一个Web应用,那Web应用就有可能存在Jsp
页面,这些Jsp页面最终会转成class类被加载,因此也需要一个Jsp的类加载器。
需要注意的是,在代码层面Catalina ClassLoader、Shared ClassLoader、Common ClassLoader对应的实体类实际上都是
URLClassLoader或者SecureClassLoader,一般我们只是根据加载内容的不同和加载父子顺序的关系,在逻辑上划分为这三
个类加载器;而WebApp ClassLoader和JasperLoader都是存在对应的类加载器类的。
当tomcat启动时,会创建几种类加载器:
1.BootStrap引导类加载器:加载JVM启动所需要的类,以及标准扩展类(位于jre/lib/ext下)
2.System系统类加载:加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定,
位于CATALINA_HOME/bin下。
3.Common通用类加载器:加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
4.webapp应用类加载器:每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于WEB-INF/lib下的jar
文件中的 class 和 WEB-INF/classes 下的class文件。
魔数
唯一作用是用于确定这个文件是否为一个能被虚拟机接受的 class 文件。
版本号
常量池
字面量
符号引用
访问标志
用于识别一些类或 接口层次的访问信息
是否为final
否public,否则是private
是否是接口
是否可用 invokespecial字节码指令
是否是abstract
是否是注解
是否是枚举
类索引,父类索引,接口索引集合
这三项数据主要用于 确定这个类的继承关系。
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类父类的全限定名
接口索引
描述这个类实现了哪些接口
字段表集合
表结构
访问标志
名称索引
描述符索引
属性表集合
字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
简单来说,字段表集合存储字段的修饰符+名称
变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
方法表集合
访问标志
名称索引
描述符索引
属性表集合
Java 代码经过编译器编译为字节码之后,存储在方法属性表集合中一个名叫"code"的属性中
属性表集合
在 class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
class 类文件本质
1. 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)
是构成平台无关性的基石,也是语言无关性的基础。Java虚拟机不和包括Java在内
的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文
件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
2. 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流。
class文件格式
各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,
这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有
两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、
4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。
表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张
格式详解
#### Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在其中的数据项,
无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。按顺序包括:
* 魔数与Class文件的版本
> 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是
否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全
方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,
只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(MinorVersion),
第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的
每个JDK大版本发布主版本号向上加1高版本的JDK能向下兼容以前版本的Class文件,但不能运行以
后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
* 常量池
> 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,
代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
* 访问标志
> 用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;
是否定义为abstract类型;如果是类的话,是否被声明为final等
* 类索引、父类索引与接口索引集合
> 这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的
父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,
所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集
合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接
口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中
* 字段表集合
> 描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。
而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,
譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
* 方法表集合
> 描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合
中的方法属性表集合中一个名为“Code”的属性里面。
与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出
现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”
* 属性表集合
> 存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。
字节码指令
* 悉知
> Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)
以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。
由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。
大多数的指令都包含了其操作所对应的数据类型信息。例如:
iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。
大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型
阅读字节码作为了解Java虚拟机的基础技能,请熟练掌握。请熟悉并掌握常见指令即可。
* 加载和存储指令
> 用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。
将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、
dload、dload_<n>、aload、aload_<n>。
将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、
fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、
iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
扩充局部变量表的访问索引的指令:wide。
* 运算或算术指令
> 用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul等等
* 类型转换指令
> 可以将两种不同的数值类型进行相互转换,
Java虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换):
int类型到long、float或者double类型。
long类型到float、double类型。
float类型到double类型。
处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,
这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
* 创建类实例的指令
> new
* 创建数组的指令
> newarray、anewarray、multianewarray
* 访问字段指令
> getfield、putfield、getstatic、putstatic
* 数组存取相关指令
> 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
* 检查类实例类型的指令
> instanceof、checkcast
*操作数栈管理指令
> 如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
将栈最顶端的两个数值互换:swap
* 控制转移指令
> 控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继
续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令如下。
条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、
if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
复合条件分支:tableswitch、lookupswitch。
无条件分支:goto、goto_w、jsr、jsr_w、ret。
* 方法调用指令
> invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。
invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,
前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关。
* 方法返回指令
> 是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、
short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外
还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
* 异常处理指令
> 在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现
* 同步指令
> 有monitorenter和monitorexit两条指令来支持synchronized关键字的语义
对象的创建:
根据new的参数是否能在常量池 中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出 ClassNotFoudException;
检查符号引用对应的类是否加载过
如果没有,则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
从堆中划分一块对应大小的内存空间给该对象
指针碰撞
java堆内存空间规整的情况下使用
Java堆是否规整又由所采用的垃圾收集器 是否带有压缩整理功能决定。
空闲列表
java 堆空间不规整的情况下使用
对象中的成员变量附上初始值
设置对象头信息
调用对象的构造函数进行初始化
对象的布局:
对象头
Mark Word
对象的 hashCode
GC 年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC 标志
Class Metadata Address
指向对象实例的指针
对象的内存布局
对象的访问方式
指针
reference 中存储的直接就是对象地址
句柄
java 堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址, 而句柄中包含了对象数据与类型数据各自的具体地址信息。
两种方式比较
使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对 象被移动时只会改变句柄的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位 的时间开销。HotSpot虚拟机使用的是直接指针访问的方式。
对象优先在 Eden 区分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机发起一次 MinorGC.
大对象直接进入老年代
最典型的大对象是那种很长的字符串以及数组。
避免在 Eden 区和 Survivor 区之间的大量内存复制。
长期存活的对象进入老年代
如果对象在Eden出生并经过第一次MinorGC胡仍然活着,并且能被Survivor容纳的话,将被移动 到Survivor空间中,
并将对象年龄设为1,对象在Survivor区中每经过 一次MinorGC,年龄就增加1,当它的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中。
对象年龄动态判定
如果在 Survivor 空间中相同年龄所有对象大小的综合大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,
那么MinorGC可以 确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
如果允许,那么会继续检查老年代最大 可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试
着进行一次MinorGC,尽管这次MinorGC是有风险的,如果 担保失败则会进行一次Full GC;如果小于,或者
HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
内存回收
Minor GC
特点
发生在新生代上,发生的比较频繁,执行速度较快
触发条件
Eden区空间不足
空间分配担保
Full GC
特点
发生在老年代上,较少发生,执行速度较慢
触发条件
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK1.7及以前的永久代(方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则会触发 Full GC
内存溢出
实实在在的内存空间不足导致
程序在申请内存时,没有足够的内存空间
内存溢出的构造方式
堆溢出
OutOfMemoryError:不断创建对象
栈溢出
StackOverflowError;增大本地变量表,例如不合理的递归
OutOfMemoryError : 不断建立线程
方法区和运行时常量池溢出
OutOffMemoryError : 通过String.intern()方法不断向常量池中添加常量,例如String.valueOf(i++).intern()
本机内存溢出
内存泄漏
该释放的对象没有释放,多见于自己使用容器保存元素的情况下
程序在申请内存后,无法释放已申请的内存空间
原因
长生命周期的对象持有短生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接,网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时把对象设置为 null
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此
内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏。
解决方法
将内部类定义为 static
用 static 的变量引用匿名内部类的实例
或将匿名内部类的实例化操作放到外部类的静态方法中
Hash值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄漏
阻塞 IO 模型
最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出IO请求之后,内核会去查
看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。
当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才会解除 block 状态。
典型的阻塞 IO 模型的例子:data=socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。
非阻塞 IO 模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,
它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次
收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以,在非阻塞IO模型中,
用户线程需要不断的询问内核数据是否就绪,也就是说非阻塞IO不会交出CPU,而会一直占用CPU。
对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断的去询问内核数据是否就绪,这样
会导致 CPU 占用率非常高,因此一般情况下,很少使用 while 循环这种方式来读取数据。
多路复用 IO 模型
多路复用IO模型是目前使用得比较多的模型。JavaNIO实际上就是多路复用IO。在多路复用IO模型中,
会有一个线程不断区轮询多个 Socket的状态,只有当Socket真正有读写事件时,才真正调用实际的IO
读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以 管理多个Socket,系统不需要建立新
的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用 IO
资源,所以他大大减少了资源占用。在JavaNIO中,是通过selector.select()去查询每个通道是否
有到达事件,如果没有事件,则一直阻 塞在那里,因此这种方式会导致用户线程的阻塞。多路复用IO模式,
通过一个线程就可以管理多个socket,只有当socket真正有读写事件 发生才会占用资源来进行实际的读
写操作。因此,多路复用 IO 比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用
户线程去执行的,而在多复用IO中, 轮询每个socket状态是内核在进行的,这个效率要比用户线程要高得多。
多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对事件逐一响应。因此对于多路复用IO模型
来说,一旦事件响应体很大,那么 就会导致后续的事件迟迟得不到响应,并且会影响新的事件轮询。
信号驱动 IO 模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户
线程会继续执行,当内核数据就绪时 会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
异步 IO 模型
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立即就可以开始
去做其他的事。而另一方面,从内核的角度, 当它受到一个asyncronousread之后,它会立刻返回,
说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准 备
完成,然后就将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它
read操作完成了。也就是说用户线程完全 不需要实际的整个IO操作是如何进行的,只需要先发起一
个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成的,然后
发送一个信号告知用户线程操作已经完成。用户 线程中不需要再次调用IO函数进行具体的读写。
这点是和信号驱动有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后
需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号已经表示IO操作已经
完成,不需要再在用户线程中调用IO函数进行实际的 读写操作。
注意:异步IO是需要操作系统的底层支持,在JDK1.7中,提供了Asynchronous IO.
Java IO 包:
字节流:
InputStream
ByteArrayInputStream
FileInputStream
FilterInputStream
BufferedInputStream
DataInputStream
LineNumberInputStream
PushbackInputStream
ObjectInputStream
PipedInputStream
SequenceInputStream
StringBufferInputStream
OutputStream
ByteArrayOutputStream
FileOutputStream
FilterOutputStream
BufferedOutputStream
DataOutputStream
PrintStream
ObjectOutputStream
PipedOutputStream
字符流:
Reader
BufferedReader
LineNumberReader
CharArrayReader
FilterReader
PushbackReader
InputStreamReader
FileReader
PipedReader
StringReader
Writer
BufferedWriter
CharArrayWriter
FilterWriter
OutputStreamWriter
FileWriter
PipedWriter
PrintWriter
StringWriter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQb4wo2Q-1581551561027)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581489108137.png)]
NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector.传统IO基于字节流和字符流进行
操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓存区中,或者从缓存区写入通道
中.Selector(选择区)用于监听多个通道的事件(比如:链接打开,数据到达)。因此,单个线程可以监听多个数据
通道。
NIO和传统的IO之间的第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
NIO 的缓冲区
JavaIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,
它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓
冲导向方向不同。数 据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过
程中灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入
缓冲区时,不要覆盖缓冲区里尚未处理的数据。
NIO 的非阻塞
IO的各种流是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被
读取,或数据完全 写入。该线程在此期间不能干任何事情了。NIO的非阻塞模式,使一个线程从某通道发送请
求读取数据,但是它仅能得到目前 可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程
阻塞,所以直至数据变得可以读取之前,该线程 可以继续做其他的事情,非阻塞也是如此。一个线程请求写入一
些数据到某通道,但是不需要等待它完全写入,这个线程同时 可以去做别的事情。线程通常将阻塞IO的空闲时间
用于在其他通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道。
Channel
Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream,
OutputStream,而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO中的Channel的主要实现有
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
分别可以对应文件IO、UDP和TCP(Server和Client).
Buffer
缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、
网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer.
客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。
服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:
ByteBuffer,IntBuffer,CharBuffer,LongBuffer,DoubleBuffer,FloatBuffer,ShortBuffer.
Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,
如果有事件发生, 便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是
用一个单线程就可以管理多个通道, 也就是管理多个连接。这样使得只有在连接真正有
读写事件发生时,才会调用函数来进行读写,就大大 减少了系统开销,并且不必为每个
连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
java.nio 包:
channels 包
Channels
DatagramChannel
FileChannel
FileLock
Pipe
SelectionKey
Selector
ServerSocketChannel
SocketChannel
charser 包
Charset
CharsetDecoder
CharsetEncoder
CoderResult
CodingErrorAction
Buffer
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
ByteOrder
MappedByteBuffer
功能
1.以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(
int,short,long,byte,float, double,boolean,char等)以及对象的引
用变量,其内存分配在栈上,变量出了作用域就会自动释放
2.而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,
它们指向的对象都存储在堆内存中
线程独享还是共享
1.栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
2.堆内存中的对象对所有线程可见。堆内存中的对象可以被所有的线程访问。
空间大小
栈的内存要远远小于堆内存
栈帧
一个方法的调用,就会在栈上分配一个栈帧
栈上分配
是虚拟机提供的一种优化技术,其基本思想是,对于线程私有的对象,将它打散分配在栈上,
而不分配在堆上。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能。
栈上分配需要的技术基础:逃逸分析。逃逸分析的目的是判断对象的作用域是否会逃逸出方
法体。 注意,任何可以在多线程之间共享的对象,一定都属于逃逸对象。
GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数
* jps
> 列出当前机器上正在运行的虚拟机进程
-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.
-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整package名称或jar完整名称.
-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数
* jstat
> 是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:jstat-gc 2764 250 20
常用参数:
-class (类加载器)
-compiler (JIT)
-gc (GC堆状态)
-gccapacity (各区大小)
-gccause (最近一次GC统计和原因)
-gcnew (新区统计)
-gcnewcapacity (新区大小)
-gcold (老区统计)
-gcoldcapacity (老区大小)
-gcpermcapacity (永久区大小)
-gcutil (GC统计汇总)
-printcompilation (HotSpot编译统计)
* jinfo
> 查看和修改虚拟机的参数
jinfo –sysprops 可以查看由System.getProperties()取得的参数
jinfo –flag 未被显式指定的参数的系统默认值
jinfo –flags(注意s)显示虚拟机的参数
jinfo –flag +[参数] 可以增加参数,但是仅限于由java -XX:+PrintFlagsFinal –version查询出来且
为manageable的参数
jinfo –flag -[参数] 可以去除参数
* jmap
> 用于生成堆转储快照(一般称为heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和jinfo命令一样,jmap有不少功能在Windows平台下都是受限的,除了生成dump文件的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris下使用。
jmap -dump:live,format=b,file=heap.bin <pid>
Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。
* jhat
> jhat dump文件名
后屏幕显示“Server is ready.”的提示后,用户在浏览器中键入http://localhost:7000/就可以访问详情
* jstack
> (Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
管理远程进程需要在远程程序的启动参数中增加:
-Djava.rmi.server.hostname=…..
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8888
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
* 浅堆和深堆
> __浅堆__ :(Shallow Heap)是指一个对象所消耗的内存。
例如,在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。
__深堆__ :这个对象被GC回收后,可以真实释放的内存大小,
也就是只能通过对象被直接或间接访问到的所有对象的集合。
通俗地说,就是指仅被对象所持有的对象的集合。深堆是指
对象的保留集中所有的对象的浅堆大小之和。
__举例__:对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,
不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,
由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内
> ZGC通过技术手段把stw的情况控制在仅有一次,就是第一次的初始标记才会发生,这样也就不难理解为什么GC停顿时间不随着堆增大而上升了,再大我也是通过并发的时间去回收了
关键技术
1. 有色指针(Colored Pointers)
2. 加载屏障(Load Barrier)
常用的性能评价/测试指标
常用的性能优化手段
总原则
* 避免过早优化
> 不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源。
所以,我们应该编写清晰,直接,易读和易理解的代码,真正的优化应该留到以后,
等到性能分析表明优化措施有巨大的收益时再进行。但是过早优化,不表示我们应该编写已经知道的对性能不好的的代码结构。
* 进行系统性能测试
> 所有的性能调优,都有应该建立在性能测试的基础上,直觉很重要,但是要用数据说话,可以推测,但是要通过测试求证。
* 寻找系统瓶颈,分而治之,逐步优化
> 性能测试后,对整个请求经历的各个环节进行分析,排查出现性能瓶颈的地方,定位问题,
分析影响性能的的主要因素是什么?内存、磁盘IO、网络、CPU,还是代码问题?架构设计不足?或者确实是系统资源不足?
前端优化手段
### 浏览器/App
* 减少请求数;
> 合并CSS,Js,图片
* 使用客户端缓冲;
> 静态资源文件缓存在浏览器中,有关的属性Cache-Control和Expires
如果文件发生了变化,需要更新,则通过改变文件名来解决。
* 启用压缩
> 减少网络传输量,但会给浏览器和服务器带来性能的压力,需要权衡使用。
* 资源文件加载顺序
> css放在页面最上面,js放在最下面
* 减少Cookie传输
> cookie包含在每次的请求和响应中,因此哪些数据写入cookie需要慎重考虑
* 给用户一个提示
> 有时候在前端给用户一个提示,就能收到良好的效果。毕竟用户需要的是不要不理他。
### CDN加速
> CDN,又称内容分发网络,本质仍然是一个缓存,而且是将数据缓存在用户最近的地方。无法自行实现CDN的时候,可以考虑商用CDN服务。
### 反向代理缓存
> 将静态资源文件缓存在反向代理服务器上,一般是Nginx。
### WEB组件分离
> 将js,css和图片文件放在不同的域名下。可以提高浏览器在下载web组件的并发数。因为浏览器在下载同一个域名的的数据存在并发数限制。
应用服务性能优化
存储性能优化
* 选择合适的数据结构
> 选择ArrayList和LinkedList对我们的程序性能影响很大,为什么?因为ArrayList内部是数组实现,存在着不停的扩容和数据复制。
* 选择更优的算法
> 举个例子,最大子列和问题:
给定一个整数序列,a0, a1, a2, …… , an(项可以为负数),求其中最大的子序列和。
如果所有整数都是负数,那么最大子序列和为0;
例如(a[1],a[2],a[3],a[4],a[5],a[6])=(-2,11,-4,13,-5,-2)时,
最大子段和为20,子段为a[2],a[3],a[4]。
最坏的算法:穷举法,所需要的的计算时间是O(n^3).
一般的算法:分治法的计算时间复杂度为O(nlogn).
最好的算法:最大子段和的动态规划算法,计算时间复杂度为O(n)
n越大,时间就相差越大,比如10000个元素,最坏的算法和最好的算法之间的差距绝非多线程或者集群化能轻松解决的。
* 编写更少的代码
> 同样正确的程序,小程序比大程序要快,这点无关乎编程语言。
详细了解应用服务性能优化
缓存
缓存的基本原理和本质
缓存是将数据存在访问速度较高的介质中。
可减少数据访问的时间,同时避免重复计算。
合理利用缓存的准则
> 频繁修改的数据,尽量不要缓存,读写比2:1以上才有缓存的价值。
缓存一定是热点数据。
应用需要容忍一定时间的数据不一致。
缓存可用性问题,一般通过热备或者集群来解决。
缓存预热,新启动的缓存系统没有任何数据,可以考虑将一些热点数据提前加载到缓存系统。
解决缓存击穿:
1、布隆过滤器,或者2、把不存在的数据也缓存起来 ,比如有请求总是访问key = 23的数据,
但是这个key = 23的数据在系统中不存在,可以考虑在缓存中构建一个( key=23 value = null)的数据。
分布式缓存与一致性哈希
* 以集群的方式提供缓存服务,有两种实现;
> 1. 需要更新同步的分布式缓存,所有的服务器保存相同的缓存数据,带来的问题就是,
缓存的数据量受限制,其次,数据要在所有的机器上同步,代价很大。
2. 每台机器只缓存一部分数据,然后通过一定的算法选择缓存服务器。常见的余数hash
算法存在当有服务器上下线的时候,大量缓存数据重建的问题。所以提出了一致性哈希算法。
* 一致性哈希
> 1. 首先求出服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
2. 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台服务器上。
一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题,此时必然造成大量数据
集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了
虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后面增加编号来实现。例如,可以为每台服务器计算三个虚拟节点,
于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”
的哈希值,于是形成六个虚拟节点:同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,
例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。
这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
集群
异步
同步和异步,阻塞和非阻塞
### 同步和异步关注的是结果消息的通信机制
> * 同步
>> 同步的意思就是调用方需要主动等待结果的返回
> * 异步
>> 异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
### 阻塞和非阻塞主要关注的是等待结果返回调用方的状态
> * 阻塞
>> 是指结果返回之前,当前线程被挂起,不做任何事
> * 非阻塞
>> 是指结果在返回之前,线程可以做一些其他事,不会被挂起。
> * 同步阻塞
>> 同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,
你去了之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包括看手机),
等着商家进货,直到有货为止,这个效率很低。jdk里的BIO就属于 同步阻塞
> * 同步非阻塞
>> 同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣服卖完了,
这个时候不需要傻傻的等着,你可以去其他地方比如奶茶店,买杯水,但是你还是
需要时不时的去商店问老板新衣服到了吗。jdk里的NIO就属于 同步非阻塞
> * 异步阻塞
>> 异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit然后马上future.get(),
这样线程其实还是挂起的。有点像你去商店买衣服,这个时候发现衣服没有了,这个时候你就
给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一直等着他响什么事也不做。这样感觉的确有点傻,所以这个模式用得比较少。
> * 异步非阻塞
>> 好比你去商店买衣服,衣服没了,你只需要给老板说这是我的电话,衣服到了就打。然后你就
随心所欲的去玩,也不用操心衣服什么时候到,衣服一到,电话一响就可以去买衣服了。jdk里的AIO就属于异步
常见的异步的手段
* Servlet异步
> servlet3中才有,支持的web容器在tomcat7和jetty8以后。
* 多线程
* 消息队列
* 集群
> 可以很好的将用户的请求分配到多个机器处理,对总体性能有很大的提升
* 程序代码级别
> 一个应用的性能归根结底取决于代码是如何编写的。
应用相关
代码级别
### 一个应用的性能归根结底取决于代码是如何编写的。
> * 选择合适的数据结构
>> 选择ArrayList和LinkedList对我们的程序性能影响很大,为什么?因为ArrayList内部是数组实现,存在着不停的扩容和数据复制。
> * 选择更优的算法
>> 举个例子,最大子列和问题:
给定一个整数序列,a0, a1, a2, …… , an(项可以为负数),求其中最大的子序列和。
如果所有整数都是负数,那么最大子序列和为0;
例如(a[1],a[2],a[3],a[4],a[5],a[6])=(-2,11,-4,13,-5,-2)时,
最大子段和为20,子段为a[2],a[3],a[4]。
最坏的算法:穷举法,所需要的的计算时间是O(n^3).
一般的算法:分治法的计算时间复杂度为O(nlogn).
最好的算法:最大子段和的动态规划算法,计算时间复杂度为O(n)
n越大,时间就相差越大,比如10000个元素,最坏的算法和最好的算法之间的差距绝非多线程或者集群化能轻松解决的。
> * 编写更少的代码
>> 同样正确的程序,小程序比大程序要快,这点无关乎编程语言。
并发编程
1.充分利用 CPU多核
2.实现线程安全的类,避免线程安全问题
3.同步下减少锁的竞争
资源的复用
目的是减少开销很大的系统资源的创建和销毁,比如 数据库连接,网络通信连接,线程资源等
JVM
与 JIT 编译器相关的优化
* 热点编译的概念
> 对于程序来说,通常只有一部分代码被经常执行,这些关键代码被称为应用的热点,
执行的越多就认为是越热。将这些代码编译为本地机器特定的二进制码,可以有效提高应用性能。
* 选择编译器类型
> -server,更晚编译,但是编译后的优化更多,性能更高
-client,很早就开始编译
* 代码缓存相关
> 在编译后,会有一个代码缓存保存编译后的代码,一旦这个缓存满了,jvm将无法继续编译代码。
当jvm提示: CodeCache is full,就表示需要增加代码缓存大小。
–XX:ReservedCodeCacheSize=N可以用来调整这个大小。
* 编译阈值
> 代码是否进行编译,取决于代码执行的频度,是否到达编译阈值。
计数器有两种:方法调用计数器和方法里的循环回边计数器
一个方法是否达到编译阈值取决于方法中的两种计数器之和。编译阈值调整的参数为:-XX:CompileThreshold=N
方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。
当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就
会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计
的半衰周期(Counter Half Life Time)。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,
可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,
只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。
与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
* 编译线程
> 进行代码编译的时候,是采用多线程进行编译的。
* 方法内联
> 内联默认开启,-XX:-Inline,可以关闭,但是不要关闭,一旦关闭对性能有巨大影响。
方法是否内联取决于方法有多热和方法的大小,
很热的方法如果方法字节码小于325字节才会内联,这个大小由参数 -XX:MaxFreqInlinesSzie=N
调整,但是这个很热与热点编译不同,没有任何参数可以调整热度。
方法小于35个字节码,一定会内联,这个大小可以通过参数-XX:MaxInlinesSzie=N 调整。
* 逃逸分析
> 是JVM所做的最激进的优化,最好不要调整相关的参数。
GC 调优
* 目的
> GC的时间够小
GC的次数够少
发生Full GC的周期足够的长,时间合理,最好是不发生。
* 调优的原则和步骤
> 1. 大多数的java应用不需要GC调优
2. 大部分需要GC调优的的,不是参数问题,是代码问题
3. 在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
4. GC调优是最后的手段
GC调优的最重要的三个选项:
第一位:选择合适的GC回收器
第二位:选择合适的堆大小
第三位:选择年轻代在堆中的比重
###步骤
1.监控GC的状态
> 使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照
和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;
2.分析结果,判断是否需要优化
> 如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没
有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;
注:如果满足下面的指标,则一般不需要进行GC:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次;
3.调整GC类型和内存分配
> 如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,
并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;
4.不断的分析和调整
> 通过不断的试验和试错,分析并找到最合适的参数
5.全面应用参数
> 如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
JVM 调优实战
###推荐策略
* 年轻代大小选择
> 1. 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).
在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
2. 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
3. 避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
* 年老代大小选择
> 1. 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会
话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用
暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
2. 并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
3. 吞吐量优先的应用一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样
可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象
存储性能优化
1.尽量使用 SSD
2.定时清理数据或者按数据的性质分开存放
动态绑定
指的是在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
编译阶段,根据引用本身的类型(Father)在方法表中查找匹配的方法,如果存在则编译通过
运行阶段,根据 实例变量的类型(Son)在方法表中查找匹配的方法,如果实例变量重写了方法,则调用重写的方法,否则调用父类方法
以 Father ft = new Son();ft.say();为例
表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,
则对应表项指向新的代码处。从父类继承来的方法位于子类定义的方法的前面。
参数传递
值传递
引用传递
Java在参数传递的时候,实际上是传递的当前引用的一个拷贝
如果参数是基本类型,传递的是基本类型的字面量值的拷贝
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
分派
静态分派
动态分派
单分派
多分派
集合类存放于java.util包中,主要有3种:Set(集)、List(列表包含Queue)和Map(映射)
Collection:Collection是集合List,Set,Queue的最基本的接口
Iterator:迭代器,可以通过迭代器遍历集合中的数据
Map:是映射表的基础接口
Collection :
List :
AbstractList:
AbstractSequentialList
LinkedList :
ArrayList :
Vector :
Stack
Set :
AbstractSet:
HashSet :
TreeSet :
SortedSet :
NavigableSet :
Queue :
DQueue
Map
SortedMap
NavigableMap
TreeMap
AbstractMap
TreeMap
HashMap
WeakHashMap
Hashtable
Iterator:
ListIterator
Collection:
List:
ArrayList
排列有序,可重复
底层使用数组
查询快,增删慢
线程不安全
当容量不够时,容量是当前的1.5倍
Vector:
排列有序,可重复
底层使用数组
查询快,增删慢
线程安全
当容量不够时,容量是当前的1倍
LinkedList:
排列有序,可重复
底层使用双向链表数据结构
查询慢,增删快
线程不安全
Set:
HashSet:
排列无序,不可重复
底层使用hash表实现
存取速度快
线程不安全
内部是HashMap
TreeSet:
排列无序,不可重复
底层使用二叉树实现
排列有序
内部是TreeMap的SortedSet
LinkedHashSet:
采用hash表实现,并用双向链表记录插入顺序
内部是LinkedHashMap
Queue: 在两端出入的List,所以也可以用数组或链表来实现
Map:
HashMap:
键不可重复,值不可重复
底层哈希表
线程不安全
允许key为null,value也可以为null
Hashtable
键不可重复,值不可重复
底层哈希表
线程安全
key,value都不允许为null
TreeMap
键不可重复,值不可重复
底层二叉树
是有序的Collection.Java List 一共有三个实现类:ArrayList、LinkedList和Vector.
适合随机查询和遍历,不适合插入和删除。
线程不安全
排列有序,可重复
容量不够时,扩容为原来的1.5倍
源码分析:
底层是数组。
默认情况下,扩容为原来的1.5倍。
将原来的数据复制到新数组中。
Java7情况下:
1.底层创建了长度是10的Object[]数组elementData
2.然后往数组中添加元素。
3.若添加导致底层elementData(对象数组)容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数组赋值到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity);
Java8情况下:
1.底层将 Object[] elementData的初始化为{}。
2.第一次add()时,底层才创建了长度为10的数组,并将数据添加到了elementData数组。
3.若导致底层elementData容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数组赋值到新的数组中。
底层是双向循环链表
适合动态插入和删除,随机访问和遍历速度比较慢。
线程不安全
排列有序,可重复
适合查询和遍历,不适合插入和删除
线程安全
排列有序,可重复
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能
重复。对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,
如果想要让这两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
若想让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方法。
哈希表便存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和
List显然不同)而是按照哈希值来存的,所以取数据也是按照哈希值取得。元素的
哈希值是通过元素的 hashCode 方法来获取的,HashSet 首先判断两个元素的
哈希值,如果哈希值一样,接着会比较 equlas 方法,如果 equals 结果为 true,
HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储的呢,就是在同样的哈希值下
顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
排列无序,不可重复
底层使用哈希表
存取速度快
内部是HashMap
源码分析:
添加元素:
首先调用元素所在类的hashCode()方法,计算元素的哈希值,此哈希值接着通过某种算法【哈希值&(数组长度-1)】(映射函数id%16)
计算出在HashSet底层数组中的位置(即为索引位置),判断数组此位置是否已经有元素,如果此位置上没有其他元素,则元素直接添加成
功了,如果此位置上有其他元素,则比较元素的hash值,如果hash值不相同,则元素添加成功;如果hash相同,调用元素所在类的equlas()
方法,如果equals()返回true,则元素添加失败;反之,成功。
底层是哈希值(数组+链表),初始容量为16,如果使用率超过0.75(负载因子),就会扩容为原来的2倍。
排列无序,不可重复
底层使用二叉树
排序存储
内部是HashMap的SortedSet
1.TreeSet是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),
每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
2.Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可
以的,自定义 的类必须实现Comparable接口,并且赋写相应的compareTo()函数,才可可以正常使用。
3.在赋写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序。
4.比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回 负整数、零或正整数。
标准要求:
1.向TreeSet中添加的数据,要求是相同类的对象
2.可以按照添加对象的指定属性进行排序。
排序方式:
自然排序:实现Comparable接口,覆写compareTo()
定制排序:Comparator
采用hash表实现,并用双向链表记录插入顺序
内部是LinkedHashMap
对于 LinkedHashSet 而言,它继承于 HashSet、又基于LinkedHashMap 来实现的。
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承于 HashSet,其
所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只
提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个
LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类
HashSet 的方法即可。
HashMap,ConcurrentHashMap,Hashtable,TreeMap,LinkedHashMap
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,
但遍历顺序却是不确定的。HashMap 最多只允许一条记录的键为null,允许多条记录的值为null。HashMap
非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,
可以用 Collections 的synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JKgEJly-1581551561029)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581502849145.png)]
大方向上,HashMap里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类
Entry 的实例,Entry 包含四个属性:key,value,hash 值和用于单向链表的 next。
1.capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
2.loadFactor: 负载因子,默认为 0.75.
3.threshold : 扩容的阈值,等于 capacity * loadFactor
Java7 HashMap的底层实现原理:
数组+链表 (算法:索引位置的计算: hash值&(数组长度-1)计算)
1.实例化以后,底层创建了长度是16的一位数组Entry[] table.
2.然后put()时,调用key1所在的hashCode()计算key哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置
如果此位置上的数据为空,此时的key-value添加成功
如果此位置上的数据不为空,比较key和已经存在的一个或多个数据的哈希值:
如果key的哈希值与已经存在的数据的哈希值都不相同,此时key-value添加成功
如果key的哈希值和已经存在的某一数据的哈希值相同,继续比较:调用key所在类的equals(key)
如果equals()返回false:此时key-value添加成功
如果equals()返回true:使用value1替换value2
在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来的2倍,并将原有的数据复制过来。
Java8对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
根据 Java7 HashMap 的介绍,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,
但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。
为了降低这部分的开销。在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,
在这些位置进行查找的时候可以降低时间复杂度为O(logN)。
Java8 HashMap的底层实现原理:
数组+链表+红黑树
1.首次put()方法时,底层创建长度为16的Node[]数组。
2.当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,
此时此索引位置上的所有数据改为使用红黑存储。
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。
整个 ConcurrentHashMap 由一个个Segment 组成,Segment代表"部分"或"一段"的意思,所以很多
地方都会将其描述为分段锁。注意,行文中,很多地方用了"槽"来代表一个 segment。
ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,
所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,
也就实现了全局的线程安全。
concurrencyLevel:并行级别、并发数、Segment数。默认是16,也就是说ConcurrentHashMap有16个Segments,
所以理论上,这个时候,最多可以同时支持16个线程并发写,只要它们的操作分别分布在不同的Segment上。
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个Segment内
部,其实每个Segment很像之前介绍的HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
Java8对ConcurrentHashMap进行了比较大的改动,Java8也引入了红黑树。
HashTable是遗留类,很多映射的常用功能与HashMap类似,不同的是它乘自Directionary类,
并且是线程安全的,任一时间只有一个线程能写HashTable,并发性不如ConcurrentHashMap,
因为ConcurrentHashMap引入了分段锁。HashTable不建议在新代码中使用,不需要线程安
全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
Properties:
常用来处理配置文件
key和value都是String类型
存取数据:
建议使用setProperty(String key, String value)方法和
getProperty(String key)方法
TreeMap实现了SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,
也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
如果使用排序的映射,建议使用 TreeMap。
在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,
否则会在运行时抛出java.lang.ClassCastException 类型的异常。
保证按照添加的key-value对数据进行排序,实现排序遍历。 此时考虑key的自然排序或定制排序。
底层使用的是红黑树。二叉树<---排序二叉树
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用Iterator遍历LInkedHashMap时,
先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
保证在遍历map元素时,可以按照添加的顺序实现遍历。
对于频繁的遍历操作,此类执行效率高于HashMap.
是一个操作Set、List和Map等集合的工具类.
提供了一系列静态方法对集合元素进行排序、查询和修改等操作,还提供了对集合设置不可变、
对集合对象实现同步控制等方法。
排序操作 (均为static方法)
reverse(List):反转List中元素的顺序
shuffle(List):对List集合元素进行随机排序
sort(List):根据元素的自然顺序对指定List集合元素按升序排序
sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
swap(List, int, int):将指定List集合中的i处元素和j处元素进行交换
查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection, Comparator)
int frequency(Collection, Object):返回指定集合中指定元素的出现的次数
void copy(List dest, List src):将src中的内容复制到dest中
boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值
同步控制
Collections 类中提供了多个 synchronizedXxx()方法, 该方法可使将指定集合包装成线程同步的集合,从而解决 多线程并发访问集合时线程安全问题。
static <T> Collection<T> synchronizedCollection(Collection<T> c)
static <T> List<T> synchronizedList(List<T> list)
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
static<T> Set<T> synchronizedSet(Set<T> s)
static <K,V> SortedMap<K,V> synchronizedSortedMap<Sorted<K,V> m)
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
顺序结构
分支结构
条件判断
if
if..else..
if...else...if
选择结构
switch...case...
循环结构
for
while
do..while
Java源代码--编译器编译-->字节码--解释器JVM-->机器码
if
if...else..
if..else..if
1.switch(表达式)
1.四种基本数据类型
byte
short
char
int
2.包装数据类型
Byte
Short
Character
Integer
3.枚举类型
Enum
4.字符串类型(jdk7以后)
String
2.case
1.case 里面必须跟break关键字,直到 最后一个break的case或者default出现
2.case条件里面只能是常量或者字面量
3.default语句可有可无,最多只能有一个
调用过程
1.编译器查看对象的声明类型和方法名
2.编译器将查看调用方法是提供方的参数类型
3.如果是private,static,final方法或者构造器, 编译器会准确的
知道应该调用哪个方法,这 种调用方式称为静态方法,与此对应的是,
调用方法依赖于隐式参数的实际类型,并 且在运行时实现动态绑定。
4.当程序运行时,并且采用动态绑定的调用方法时, 虚拟机一定会调用
与x所引用的对象的实际类型最 合适的那个类的方法。
知识点
1.解析
调用目标在程序代码写好,编译器进行编译的时候就必须确定下来,这个过程叫做解析
在Java语言中符合"编译器可知,运行期不变"这个要求的方法,有两种
静态方法
与类型直接关联
私有方法
外部不可访问
这两种方式,不能被继承或者以其他方式重写其版本,所以适合在类加载阶段进行解析。
2.静态分派
1.静态分派:所有依赖静态类型来定位执行方法的分派动作。
2.典型应用:方法的重载
3.静态分派是发生在编译期的,因此,静态分派的动作有编译器完成。
3.动态分派
4.基于栈的字节码解析执行引擎
参数的值传递
1.基本数据类型保存的是原始值
2.引用数据类型保存的是引用值, "引用值"指向内存空间地址,代表 了某个对象的引用,
而不是对象本身,对象本身存放在这个引用值所 表示的地址的位置。
类,接口和数组
1.对于基本数据类型参数,在方法体内对参 数进行重新赋值,不会改变原有变量的值。
2.对于引用类型参数,在方法体内对参数进行重 新赋予引用,不会改变原有变量所持有的引用。
3.方法体内对参数进行运算,不会改变原有变量的值。。
4.对于引用类型参数,方法体内对参数所指向对象的 属性进行操作,将改变原有变量所指向对象的属性值。
基本数据类型传递的是值,引用类型传递的是引用的副本。
参数的传递只有值传递
方法的重载 Overload
定义
如果同一个类中包含了两个或两个以上的方法名相同,方 法参数的个数,顺序或类型不同的方法,称为方法的重载。
发生条件
1.方法在同一个类里面
2.方法名相同,参数列表不同(个数,顺序,类型)
3.方法重载和方法修饰符列表无关
4.方法重载和方法的返回值类型无关
1.修饰类
static只能用于修饰内部类,普通类不允许是静态的。
2.修饰方法
静态方法,是没有this的,因为不依附于任何对象, 在静态方法中不能访问类的非
静态成员变量和非 静态方法。非静态成员变量和非静态方法都必须 依赖于具体的对象才能被调用。
3.修饰变量
静态变量
1.静态变量被所有对象共享,在内存中只有一个副本, 在类初次加载的时候才会初始化。
2.非静态变量是对象所拥有的,在创建对象的时候被初 始化,存在多个副本,各个对象拥有的副本互不影响。
一般需要实现以下 两个功能时使用
在对象之间共享值时
方便访问变量时
4.static块
构造方法用于对象的初始化。 静态初始化块,用于类的初始化操作。
在静态初始快中不能直接访问非static 成员。
作用
提升程序性能
5.static final 用来修饰成员变量和成员方法,可理解为"全局常量"。
1.对于变量,表示一旦给就不可修改,并且通过类名可以直接访问。
2.对于方法,表示不可覆盖,并且可以通过类名直接访问
1.修饰类
表示被修饰的类不能被继承
2.修饰方法
被final修饰的方法不能被重写
一个类的private 方法会隐式的被指定为final方法
如果父类中有final 修饰的方法,那么子类不能去重写
3.修饰变量
必须要赋初始值,而且只能被初始化一次。
赋值方式
1.直接赋值
2.全部在构造方法中赋初值
不同类型
基本数据类型,表示这个变量的值不能改变
引用类型,说明这个引用的地址的值不能修改,但是 这个引用所指向的对象里面的内容还是可以改变的。
总结
1.final类不能被继承,没有子类,final类中的方法默认是final的,但是final类中的成员变量不是final的。
2.final方法不能被子类覆盖,但可以被继承。
3.final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
4.final不能用于修饰构造方法。
jar -?
非法选项: ?
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:
-c 创建新档案
-t 列出档案目录
-x 从档案中提取指定的 (或所有) 文件
-u 更新现有档案
-v 在标准输出中生成详细输出
-f 指定档案文件名
-m 包含指定清单文件中的清单信息
-n 创建新档案后执行 Pack200 规范化
-e 为捆绑到可执行 jar 文件的独立应用程序
指定应用程序入口点
-0 仅存储; 不使用任何 ZIP 压缩
-P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
-M 不创建条目的清单文件
-i 为指定的 jar 文件生成索引信息
-C 更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
与 'm', 'f' 和 'e' 标记的指定顺序相同。
示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 'mymanifest' 并
将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
jar cvfm classes.jar mymanifest -C foo/ .
Java 的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。
栈内存特点
栈内存中数据用完就会释放掉。是脱离了变量的作用域后会释放掉。
排序:Arrays.sort()
复制:Arrays.copyOf()
反转
操作数组的工具类 Arrays
Arrays中包含了用来操作数组的各种方法,提供的方法都是静态的。
public static void sort(Object[] a)
按自然顺序排序
public static int binarySearch(Object[] a, Object key)
用二分法在给定数组中搜索给定值的对象。数组必须是排好序的。 许多binarySearch的重载方法。
public static String toString(形参)
返回数组的字符串表示形式
public static <T> List<T> asList(T,,,a)
返回固定大小的List
public static void fill(int[] a, int val)
public static int[] copyOf(int[] original, int newLength)
将原始数组的元素,复制到新的数组中,可以 设置复制的长度(即需要被赋值的元素个数)
public int[] copyOfRange(int[] original, int from, int to)
将某个范围内的元素复制到新的素组中。
public static boolean equals(long[] a, long[] a2)
如果两个指定的long型2数组彼此相等,则返回true。 如果两个数组包含相同数量的元素,并且两个数组 中的所有相应元素对都是相等的,则认为这两个数 组是相等的。换句话说,如果两个数组以相同顺序 包含相同的元素,则两个数组是相等的。同样的方 法适用于所有的其他基本数据类型的数组。
面向对象
是把构成问题事务分解成各个对象, 建立对象的目的不是为了完成一个 步骤,而是为了描述某个事务在整个解决问题的步骤中的行为。
面向过程
分析出解决问题所需要的步骤,然后 用函数把这些步骤一步一步实现,使 用的时候一个一个依次调用就可以了
类
具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。
是抽象的概念集合,表示的是一个共性的产物,类中定义 的是属性和行为(方法)。
对象
对象是人们要进行研究的任何事物,它不 仅表示具体的事物,还能表示抽象的规则 、计划或事件。
对象是一种个性的表示,表示一个独立的个体,每个对 象拥有自己独立的属性,依靠属性来区分不同对象。
类和对象的关系
对象的抽象是类,类的具体化就是对象。
类是对象的模板,对象是类的实例。
this关键字指向的是当前对象的引用
作用
1.this.属性名称
指的是访问类中的成员变量,用来区分成员变量和局部变量(重名问题)
2.this.方法名称
用来访问本类的成员变量
3.this()
访问本类的构造方法
()中可以有参数,如果有参数,就是调用指定的有参构造
this()不能使用在普通方法中,只能写在构造方法中
必须是构造方法中的第一条语句
将对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
继承
子类继承父类的特征和行为,使得子类对象(实例) 具有父类的实例域和方法,还可以在此基础上添加 新方法和域来满足需求。
提升代码的可复用性、扩展性。
super 关键字
指代变量,用于在子类中指代父类对象。
用法
1.在子类中调用父类的属性 或方法
2.在子类中指代父类构造器
总结
super关键字指代父类对象, 主要用于在子类中指定父 类的方法和属性,也用于在 子类中初始化父类。子类 的静态方法中不能使用 super关键字。
重写
子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。
目的:为了解决父类方法在子类中不适用,而让子类重写父类方法的方式。
规则
返回值不变或者为子类、形参列表不能改变并且访问控制权限不能严于父类。
子类可以重写父类的除了构造器的任何方法。构造器和类名相同, 不能被子类继承,因此也不能被重写。
重写方法不能抛出新的检查异常或者比被重写方法声明更加宽泛的异常。
子类不能重写父类中访问控制权限为private的方法。
重写和重载的区别
1.方法重写:子类和父类中方法相同,两个类之间的关系,函数的返回值类型、函数名、参数列表都一样
2.方法重载:在同一个类中,多个方法名相同,但参数列列表不同(个数不同,数据类型不同)
多态
理解
1.多态是同一个行为具有多个不同表现形式或形态的能力。
2.多态就是同一个接口,使用不同的实例而执行不同操作
3.多态性是对象多种表现形式的体现。
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。这就是多态。
存在的条件
1.继承
2.重写
3.父类引用指向子类对象
多态的实 现方式
1.重写
2.接口
3.抽象和抽象方法
体现为父类引用变量可以指向子类对象
前提条件
必须有子父类关系
在使用多态后,父类引用变量调用方法时,会调用子类重写后的方法
多态的定义 与使用格式
格式:父类类型 变量名 = new 子类类型();
转型
向上转型
多态本身就是向上转型的过程
使用格式
父类类型 变量名 = new 子类类型();
适用场景
当不需要面对子类类型时,通过提高扩展性, 或者使用父类的功能就能完成相应的操作。
向下转型
一个已经向上转型的子类对象可使用强制类型转换的格式, 将父类引用类型转换为子类引用各类型。
使用格式
子类类型 变量名 = (子类类型)父类类型的变量;
适用场景
当要使用子类特有功能时
多态中成 员的特点
成员变量
编译运行看左边
成员方法
编译看左边,运行看右边
类的成员之内部类
内部类是一个类当中的一个类。
内部类当中可以调用外部类当中的属性 和方法,而外部类却不能调用内部类当中的。
匿名内部类
匿名内部类的具体名字不会被我们在程序中当众编写, 因为它已经在主方法中被实例化了。
继承
抽象类
接口
适合创建那种只需要一次使用的类
规则
1.匿名内部类不能是抽象类
2.匿名内部类不能定义构造器。
Enum 枚举
概念
1.是JDK1.5中引入的新特性
2.被enum关键字修饰的类型
3.如果枚举不添加任何方法,枚举值默认从0开始的有序数值。
本质
enum 是一种受限制的类,并且具有自己的方法
好处
将常量组织起来,统一进行管理
应用场景
错误码、状态机等
方法
1.values():返回enum实例的数组,而且该 数组中的元素严格保持在enum中声明的顺序。
2.name():返回实例名
3.ordinal():返回实例声明时的次序,从0开始
4.getDeclaringClass():返回实例所属的enum类型
5.equals():判断是否同一个对象
6.=:可以用来比较enum实例
特性
除了不能继承,基本上可以将enum看做是一个常规的类
Java 不允许使用 = 为枚举常量赋值
枚举可以添加普通方法、静态方法、抽象方法、构造方法
为enum添加方法来间接实现显示赋值
如果要为 enum 定义方法,那么必须在enum 的最后一个实例尾部添加一个分号。此外,
在enum中,必须先定义实例,不能将字段或 方法定义在实例前面。否则,编译器会报错。
枚举可以像一般类一样实现接口
枚举不可以继承
Annotation 注解
Java注解
注解本质上是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
基本语法
Annotation 组成部分
public interface Annotation{}
每1个Annotation接口对象,都会有唯一的 RetentionPolicy属性,1-n个ElementType属性
public enum ElementType{}
ElementType是Enum枚举类型,用来指定 Annotation 的类型。
值
TYPE
标注类、接口(包括注释类型)或枚举声明
FIELD
标注字段声明
METHOD
标注方法声明
PARAMETER
标注参数声明
CONSTRUCTOR
标注构造器声明
LOCAL_VARIABLE
标注局部变量声明
ANNOTATION_TYPE
注释类型声明
PACKAGE
包声明
TYPE_PARAMETER
TYPE_USE
public enum RetentionPolicy{}
RetentionPolicy 是 Enum枚举类型, 它用来指定Annotation的策略。通俗 点说,就是不同RentitionPolicy类型的 Annotation 的作用域不同。
值
SOURCE
仅存在于编译器处理期间,编译处理完之后,就没有该Annotation信息了
源码级别
CLASS
编译器将Annotation存储于对应的.class文件中。默认行为
字节码级别
RUNTIME
编译器将Annotation存储于class文件中,并且可由JVM读入
运行时级别
Annotation 通用定义
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation1{}
说明
1.定义了一个名叫MyAnnotation1的Annotation。我们可以在代码中可以通过@MyAnnotation1来使用。
2.其中@Documented,@Target,@Retention,@interface都是用来修饰MyAnnotation1的。
@interface
使用@interface定义注解时,意味着它实现了 java.lang.annotation.Annotation接口,即 该注解就是一个Annotation。
定义Annotation时,@interface是必须的。
Annotation接口的实现细节都由编译器完成。通过@interface 定义注解后,该注解不能继承其他的注解或接口
@Documented
类和方法的Annotation在缺省情况下是不出现在javadoc中的。如果 使用@Documented修饰该Annotation,则表示它可以出现在javadoc中。
定义Annotation时,@Documented可有可无;若没有定义, 则Annotation不会出现在javadoc中。
@Target(ElementType.TYPE)
用来指定 Annotation 的类型属性
指定该Annotation 的类型是 ElementType.TYPE。意味着,MyAnnotation1 是来修饰"类、接口(包括注释类型)或枚举声明"的注解.
定义Annotation时,@Target可有可无。 若有@Target,则该Annotation只能用于它指定的地方; 若没有@Target,则该Annotation可以用于任何地方。
@Retention(RetentionPolicy.RUNTIME)
指定Annotation的策略属性。
指该Annotation的策略是RetentionPolicy.RUNTIME。意味着, 编译器会将该Annotation信息保存在.class 文件中,并且能被虚拟机读取。
定义Annotation 时,@Retention可有可无。 若没有@Retention,则默认是RetentionPolicy.CLASS.
简单理解
@interface 用来声明 Annotation
@Documented用来表示该Annotation是否会出现在javadoc中
@Target用来指定Annotation的类型
@Retention 用来指定Annotation 的策略
注解的内容格式
数据类型 属性名() default 默认值
JDK 内置注解
@Override
声明重写父类方法的注解,要求编译器帮我们检查是否成功的覆盖,如果没有父类方法,编译器将会进行报错提示。
@Deprecated
声明方法过时了,不再建议使用。要求编译器在编译的过程中对于这样的方法发出警告,提示方法过时。
@SuppressWarnings
压制警告,提示编译器,在编译的过程中对指定类型的警告不再提示。
自定义注解
Java 的自定义注解和创建一个接口类似,自定义注解的格式是以 @interface 为标志的。
格式
public @interface 注解名 {定义体}
使用场景
1.自定义注解+拦截器 实现登录校验
2.自定义注解+AOP 实现日志打印
注意事项
1.只能用public或默认(default)这两个访问权修饰.
2.参数成员只能用基本类型byte,short,int,char,long,float, double,boolean八种基本数据类型
和String,Enum,Class, annotations等数据类型,以及这一些类型的数组。
3.如果只有一个参数成员,最好把参数名设置为"value", 后加小括号。
元注解
目的
让开发者自定义注解,元注解负责注解自定义注解。
@Target
用来说明自定义注解可以用在什么地方。
@Retention
用来描述自定义注解的生命周期
@Documented
用于表示自定义注解可以被javadoc之类的工具文档化,没有成员。
@Inherited
是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
标记这个注解时继承于哪个注解类(默认注解并没有继承于任何子类)
被标注的类型是被继承的
J.U.C
collections
Queue
ConcurrentLinkedQueue
BlockingQueue
ArrayBlockingQueue
DelayQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue
Deque
ArrayDeque
IdentityLinkedList
LinkedList
BlockingDeque
LinkedBlockingDeque
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentSkipListSet
ConcurrentMap
ConcurrentHashMap
ConcurrentavigableMap
ConcurrentSkipListMap
executor
Future
RunnableFuture
RunnableScheduledFuture
FutureTask
ScheduledFuture
Callable
Executor
ExecutorService
ScheduleExecutorService
ScheduledThreadPoolExecutor
ThreadPoolExecutor
CompletionService
ExecutorCompletionService
RejectExecutionHandler
ThreadPoolExecutor.DiscardPolicy
ThreadPoolExecutor.DiscardOldesPolicy
ThreadPoolExecutor.CallerRunsPolicy
ThreadPoolExecutor.AbortPolicy
TimeUnit
aotmic
AtomicBoolean
AtomicInteger
AtomicInterArray
AtomicIntegerFailedUpdater
AtomicLong
AtomicLongArray
AtomicLongFailedUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFailedUpdater
AtomicStampledReference
locks
Lock
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
Condition
ReadWriteLock
ReentrantReadWriteLock
LockSupport
tools
CountDownLatch
CyclicBarrier
Semaphore
Executors
Exchanger
Java线程实现/创建方式
继承 Thread 类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。
启动线程的唯一方法就是 通过Thread类的start()实例方法。start()方
法是一个native方法,它将启动一个新线程,并执行run()方法。
实现 Runnable 接口
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口。
ExecutorService、Callable<Class>、Future有返回值线程
有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须 实现Runnable接口。
执行Callable任务后,可以获取一个Future的对象, 在该对象上调用get就可以获取Callable
任务返回的Object了,再结合 线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
基于线程池的方式
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,
是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
4种线程池
newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用
execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则
创建一个新线程 并添加到池中。终止并从缓存中移除那些已有60秒未被使用的线程。因此,长时间保持 空闲的线程池不会使用任何资源。
newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,
在大多数 nThreads线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提
交附加任务,则在有 可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间
由于失败而导致任何线程终止, 那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式的关闭之前,池中线程一直存在。
newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行。
newSingleThreadExecutor
Excutors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),
这个线程池可以在线程死后(或者发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
线程生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中,
它要经过新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked) 和死亡(Dead)5种状态。
尤其当线程启动以后,他不可能一直"霸占"着CPU独自运行,所以CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
新建状态(NEW)
当程序使用new关键字创建一个线程之后,该线程就处于新建状态, 此时仅由JVM为其分配内存,并初始化其成员变量的值。
就绪状态(RUNNABLE)
当线程对象调用了start()方法之后,该线程处于就绪状态。 Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行状态(RUNNING)
如果处于就绪状态的线程获得了CPU,开始执行run() 方法的线程执行体,该线程处于运行状态。
阻塞状态(BLOCKED)
阻塞状态是指线程因为某种原因放弃了CPU使用权,也即让出了CPUTIMESLICE,暂时停止运行。
直到线程进入可运行状态(runnable)状态,才有机会再次获得CPUTIMESLICE转到运行(running)状态。 阻塞的情况分为三种:
等待阻塞(o.wait—>等待队列)
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列中。
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被 别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,
JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、 或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程死亡(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
run()或call()方法执行完毕,线程正常结束。
异常结束
线程抛出一个未捕获的Exception或Error。
调用 stop
直接调用该线程的 stop() 方法来结束该线程——该方法通常容易导致死锁,不推荐使用。
正常运行结束
程序运行结束,线程自动结束
使用退出标志退出线程
Interrupt方法结束线程
stop方法终止线程(线程不安全)
1.对于 sleep() 方法,该方法属于 Thread 类中的。而 wait() 方法,则是属于 Object 类中的。
2.sleep()方法导致了程序暂停执行指定的时间,让出CPU该其他线程,但是它的监控状态依然保持者,
当指定的时间到了又会自动恢复运行状态。
3.在调用 sleep() 方法的过程中,线程不会释放对象锁。
4.当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用
notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run()方法体代码 执行完毕,可以直接继续执行下面的代码。
2.通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
3.方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态, 开始运行run函数当中的代码。Run 方法运行结束,此线程终止。然后CPU再调度其他线程。
Java后台线程
1.定义:守护线程-也称"服务线程",它是后台线程,它有一个特性,即为 用户线程提供公共服务,在没有用户线程可服务时会自动离开。
2.优先级:守护线程的优先级比较低,用于为系统中的其他对象和线程提供服务。
3.设置:通过 setDaemon(true)来设置线程为"守护线程";将一个用户线程设置为守护线程的方式 是在线程对象创建之前用线程对象的 setDaemon 方法。
4.在Daemon 线程中产生的新线程也是Daemon的。
5.线程是 JVM 级别的,以Tomcat为例,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和 web 应用程序保持同步。也就是说,即使你停止了web应用,这个线程依旧是活跃的。
6.example:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再 产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。 它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收字眼。
7.生命周期:守护进程是运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理 某些发生的事件。也就是说守护继承不依赖于终端,但是依赖于系统,与系统"同生共死"。当JVM中所有的线程 都是守护进程的时候,JVM就可以退出了;如果还有一个或以上的非守护进程则JVM不会退出。
Java锁
乐观锁
悲观锁
自旋锁
原理比较简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的
线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那线程
也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁
的线程最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
优缺点
尽可能的减少线程阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能
大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步快,这时候就不适合使用自旋锁了,
因为自旋锁在获取锁前一直都是占用CPU做无用功,同时有 大量线程在竞争一个锁,会导致获取锁的时间很长,线
程自旋的消耗大于线程阻塞挂起 操作的消耗,其他需要cpu的线程又不能获取到cpu,造成cpu的浪费所以这种情况下要 关闭自旋锁。
时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取锁立即进行处理。但是如何去选择自旋 锁的执行时间呢?
如果自旋锁时间太长,会有大量的线程处于自旋锁状态占用CPU资源,进而 会影响整体系统的性能。因此自旋
的周期选择额外重要。 JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在JDK1.6引入了适应性
自旋锁,适 应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一锁上的自旋时间以及锁的拥有
者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对 当前CPU的负荷情况
做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2) 个线程正在自旋,则后来线程直接
阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋 时间(自旋计数)或进入阻塞,如果CPU处于节电模式
则停止自旋,自选时间的最坏情况是CPU的存储 延迟(CPU A存储了一个数据,到CPU B得知这个数直接的时间差),
自旋时会适当放弃线程优先级之间的差异。
开启
JDK1.6
-XX:UseSpinning 开启
-XX:PreBlockSpin=10 自旋次数
jdk1.7
去掉此参数,由 JVM 控制
Synchronized 同步锁
ReentrantLock
公平锁与非公平锁
非公平锁
JVM按随机、就近原则分配所的机制被称为不公平锁,ReentrantLock在构造函数中提供了
是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,
ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
ReentrantLock 与 Synchronized
1.ReentrantLock 通过方法 lock() 与 unlock() 来进行加锁与解锁操作,与 Synchronized
会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异
常而无法正常解锁的情况,使用 ReentrantLock 必须在finally控制块中进行解锁操作。
2.ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。
Condition类和Object类锁方法区别
1.Condition类的await方法和Object类的wait方法等效
2.Condition类的signal方法和Object类的notify方法等效
3.Condition类的signalAll方法和Object类的notifyAll方法等效
4.ReentrantLock类可以唤醒指定条件的线程,而Object的唤醒是随机的。
tryLock和lock和lockInterruptibly的区别
1.tryLock能获得锁就返回 true,不能就立即返回false,tryLock(long timout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false.
2.lock能获得锁就返回true,不能的话一直等待获得锁。
3.lock和lockInterruptibly,如果两个线程分别执行这两个类,但此时中断这两个线程,lock不会抛出 异常,而lockInterruptibly会抛出异常
Semaphore信号量
实现互斥锁(计数器为1)
创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,也叫做二元信号量, 表示两种互斥状态。
代码实现
Semaphore与ReentrantLock
AtomicInteger
可重入锁(递归锁)
基本概念
程序
是为完成特定任务、用某种语言编写的一组指令的集合。 即指一段静态的代码,静态对象。
进程
是程序的一次执行的过程,或是正在运行的一个程序。 是一个动态的过程:有它自身的产生、存在和消亡 的过程。 —— 生命周期
程序是静态的,进程是动态的。
进程作为资源分配的单位,系统在运行 时会为每个线程分配不同的内存区域
线程
进程可细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
线程作为调度和执行的单位,每个线程拥有独立的运行栈 和程序计数器(PC),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间 —>它们从同一堆中分配对象,可以访问相同的变量和对象。
这使得线程间通信更简便、高效。但多个线程操作共享的 系统资源可能会带来安全隐患(线程不同步)
单核CPU
是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
多核CPU
多核CPU才能更好的发挥多线程的效率。(现在的服务器都是多核的)
并行
多个CPU同时执行多个任务。
并发
一个CPU(采用时间片)同时执行多个任务。
多线程的优点
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
使用场景
1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务 时,如用户输入、文件读写操作、网 络操作、搜索等
3.需要一些后台运行的程序时。
线程的创建与启动
JVM允许程序运行多个线程,通过java.lang.Thread类来体现。
Thread类
特性
每个线程都是通过某个特定Thread对象的run()方法 来完成操作的,经常把run()方法的主体称为线程体。
构造器
Thread()
创建新的Thread对象
Thread(String threadname)
创建线程并指定线程实例名
Thread(Runnable target)
指定创建线程的目标对象,它实 现了Runnable接口的run方法
Thread(Runnable target, String name)
创建新的Thread对象
有关方法
void start()
启动线程,并执行对象的run()方法
run()
线程在被调度时执行的操作
String getName()
返回线程的名称
void setName(String name)
设置线程的名称
static Thread currentThread()
返回当前线程。在Thread子类中就是this, 通常用于主线程和Runnable实现类。
static void yield()
线程让步
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。
若队列中没有同优先级的线程,忽略此方法
join()
当某个程序执行流中调用其他线程的join()方法时,调用 线程将被阻塞,直到join()方法加入的join线程执行完为止。
低优先级的线程也可以获得执行。
static void sleep(long millis)
令当前活动线程在执行时间段内放弃 对该CPU控制,使其他线程有机会被执 行,时间到后重排队。
抛出InterruptedException异常
stop()
强制线程生命期结束,不推荐使用
boolean isAlive()
返回boolean型,判断线程是否还活着
创建线程的几种方式对比
API中创建线 程的两种方式
继承Thread类
步骤
1.定义子类继承Thread类
2.子类中重写Thread类的run方法
3.创建Thread子类对象,即创建了线程对象
4.调用线程对象的start方法:启动线程,调用run方法。
注意点
1.如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
2.run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
3.想要启动多线程,必须调用start方法
4.一个线程对象只能调用一次start()方法启动,如果重复调用了, 则将抛出以上的异常"IllegalThreadStateException"
实现Runnable接口
步骤
1.定义子类,实现Runnable接口。
2.子类中重写Runnable接口中的run方法。
3.通过Thread类含构造器创建线程对象
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
5.调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
联系与区别
public class Thread extends Object implements Runnable
区别
继承Thread
线程代码存放Thread子类run方法中
实现Runnable
线程代码存放在接口的子类的run方法
实现方式的好处
1.避免了单线程的局限性
2.多个线程可以共享同一个接口实现类的对象, 非常适合多个相同线程来处理同一份资源。
线程的父类
Java的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start() 方法前调用thread.setDaemon(true)可以把一个 用户线程变成一个守护线程。
Java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出。
线程的调度
调度策略
时间片
抢占式
高优先级的线程抢占CPU
Java的调度方法
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略
线程的优先级
线程的优先级等级
MAX_PRIOTIRY:10
MIN_PRORITY:1
NORM_PRIORITY:5
涉及的方法
getPriority()
返回线程优先值
setPriority(int newPriority)
改变线程的优先级
说明
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
线程的生命周期
JDK中用Thread.State 类定义了线程的几种 状态
要想实现多线程,必须在主线程中创建新的线程对象。 Java语言使用Thread类及其子类的对象来表示线程, 在它的一个完整的生命周期中通常要经历5种状态:
新建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
就绪
处于新建状态的线程被start()后,将进入线程队列等待CPU时间 片,此时它已经具备了运行的条件,只是没分配到CPU资源。
运行
当就绪的线程被调度并获得CPU资源时,便进入 运行状态,run()方法定义了线程的操作和功能。
阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作 时,让出CPU并临时中止自己的执行,进入阻塞状态。
死亡
线程完成了它的全部工作或线程被提前 强制性的中止或出现异常导致结束。
JDK5.0新增线程创建方式
方式一
实现Callbable接口
与使用Runnable相比, Callable功能更强大些
1.相比run()方法,可以有返回值
2.方法可以抛出异常
3.支持泛型的返回值
4.需要借助FutureTask类, 比如获取返回结果
Future接口
1.可以对具体Runnable、Callable任务的执行 结果进行取消、查询是否完成、获取结果等。
2.FutureTask是Future接口的唯一的实现类
3.FutureTask同时实现了Runnable,Future接口。 它既可以作为Runnable被线程执行,又可以作 为Future得到Callable的返回值
方式二
使用线程池
背景
经常创建和销毁、使用量特别大的资源, 比如并发情况下的线程,对性能影响很大。
思路
提前创建好多个线程,放入线程池中,使用时直接 获取,使用完放回池中。可以避免频繁创建销毁、 实现重复利用。类似生活的公共交通工具。
好处
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize;核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务 时最多保持多长时间后会终止。
线程相关API
ExecutorService
真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task)
执行任务,有返回值,一般又来执行Callable
void shutdown
关闭连接池
Executors
工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
Executors.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池
Exceutors.newFixedThreadPool(n)
创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor()
创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n)
创建一个线程池,它可安排在给定 延迟后运行命令或者定期的执行。
线程的同步
1.同步代码块
synchronized(对象){ //需要被同步的代码 }
2.同步方法
public synchronized void show(String name){}
同步机制中的锁
对于并发工作,需要某种方式来防止两个任务访问相同的 资源。防止这种冲突的方法就是当资源
被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这 项资源,使其他任
务在其被解锁之前,就无法访问它了,而 在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class),非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意
必须确保使用同一资源的多个线程共用一把锁, 否则就无法保证共享资源的安全。
一个线程类中的所有静态方法共用一把锁(类.class), 所有非静态方法共用一把锁(this),同步代码块(谨慎)
同步范围
1.如何找问题,即代 码是否存在线程安全?
1.明确哪些代码是多线程运行的代码
2.明确多个线程是否有共享数据
3.明确多线程运行代码中是否有多条语句操作共享数据
2.如何解决呢?
对多条操作共享数据的语句,只能让 一个线程都执行完,在执行过程中,其 他线程不可以参与执行。
即所有操作共享数据的这些语句都 要放在同步范围中
释放锁的操作
1.当前线程的同步方法、同步代码块执行结束。
2.当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的继续执行。
3.当前线程在同步代码块、同步方法中出现了 未处理的Error或Exception,导致异常结束。
4.当前线程在同步代码块、同步方法中执行了 线程对象的wait()方法,当前线程暂停,并释放锁
不会释放锁的操作
1.线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
2.线程执行同步代码块时,其他线程调用了该线程的suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)
应尽量避免使用suspend()和resume()来控制线程
Lock(锁)
1.从JDK5.0开始,Java提供了更强大的线 程同步机制—通过显示定义同步锁对象 来实现同步。同步锁使用Lock对象充当
2.java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行 访问的工具。锁提供了对共享资源的独占访,
每次只能有一个线程对Lock对 象加锁,线程开始访问共享资源之前应 先获得Lock对象。
3.ReentrantLock类实现了Lock,它拥有 与synchronized相同的并发性和内存 语义,在实现线程安全的控制中,
比较常用的是ReentrantLock,可以显示加 锁、释放锁。
synchronized与Lock对比
1.Lock是显示锁(手动开启和关闭锁,别忘记关闭 锁),synchronized是隐式锁,出了作用域自动释放
2.Lock只有代码块锁,synchronized有代码块和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度 线程,性能更好。并且具有更好的扩展性(提 供更多的子类)
优先使 用顺序
Lock —> 同步代码块(已经进入了方法体,分配 了相应资源)—> 同步方法(在方法体之外)
线程的死锁问题
死锁
1.不同的线程分别占用对方需要的同步资源不放弃,都在 等待对方放弃自己需要的同步资源,就形成了线程的死锁。
2.出现死锁后,不会出现异常,不会出现提示,只是所有 的线程都处于阻塞状态,无法继续。
解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
线程通信
wait()
令当前线程挂起并放弃CPU、同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队
其他线程调用notify()或notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所 有权后才能继续执行。
wait()方法
1.在当前线程中调用方法:对象名.wait()
2.使当前线程进入等待(某对象)状态, 直到另一线程对该对象发出notity( 或notifyAll)为止。
3.调用此方法的必要条件:当前线程必须 具有该对象的监控权(加锁)
4.调用此方法后,当前线程将释放对象监 控权,然后进入等待。
5.在当前线程被notify后,要重新获得监 控权,然后从断点处继续代码的执行。
notify()
唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notify()/notifyAll()
1.在当前线程中调用方法:对象名.notify()
2.功能:唤醒等待该对象监控权的一个/所有线程
3.调用方法的必要条件:当前线程必须具 有对该对象的监控权(加锁)
notifyAll()
唤醒正在排队等待资源的所有线程结束等待。
以上这三个方法只有在synchronized方法或synchronzized代码块中 才能使用,否则会报java.lang.IllegalMonitorStateException异常
因为上述这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized的同步锁,因此这三个方法只能在Object类中声明
Wrapper 包装类
出现原因
1.Java的8种基本数据类型不支持面向对象编程机制
2.8种基本数据类型不具备"对象"的特性: 没有成员变量、方法可供调用
3.将8种基本数据类型不能当成Object类型使用的问题.
包装类与基本 数据类型的对应
byte -- Byte
short -- Short
int -- Integer
long -- Long
char -- Character
float -- Float
double -- Double
boolean -- Boolean
基本类型与包装 类之间的转换
JDK1.5之前
1.通过包装类的构造器实现
2.通过传入一些字符串参数,来构建包装类对象
3.如果想获得包装类对象中包装的基本数据类型, 可以用包装类提供的.xxxValue()方法
JDK1.5之后
1.自动装箱
把一个基本类型的变量,直接赋值给对应的包装类变量, 或者赋值给Object变量(Object是所有类的父类,子类对象 可以直接赋给父类变量--Java的向上自动转型特性)
2.自动拆箱
把一个包装类变量,直接赋值给一个基本类型的变量。
装箱与拆箱
装箱
将基本数据类型转换为包装类,向上转型
拆箱
将包装类转换为基本数据类型,向下转型
总结
1.会创建对象,频繁的装箱操作会消耗许多内存,影响性能.
2.equals(Object o)因为原equals方法中的参数类型 是封装类型,所传入的参数类型(a)是原始数据类型, 所以会自动对其装箱,反之,会对其拆箱
3.当两种不同类型用==比较时,包装器类的需要拆箱,当 同种类型用==比较时,会自动拆箱或者装箱
包装类的缓存分析
具有缓存 的包装类
Integer(-128=<i<=127)
Short(-128=<s<=127)
Character(c=<127)
Long(-128=<l<=127)
Byte(-128=<l<=127)
特点
在缓存范围内拿出的数据是相同的(==)
不具有..
Double
new Double(d);
Float
new Float(f)
Boolean
没有创建对象,内部提前已经创建好了。
特点
具有缓存的包装类,在缓存范围内拿出的数据是相同的(==)
不具有缓存的包装类,每次都会创建对象(Boolean除外)
字符串处理器
1.字符串的反转
1.使用StringBuilder的append方法
2.使用StringBuilder()中的reverse方法
3.使用字符串中函数toCharArray(); 方法,反向将字符串连接
2.字符串分割 成字符串组
split
3.字符串中的替换
replace
replaceAll
4.字符串中截取
substring()
...
String 类的使用与内存原理
Java 中内存分析
栈
存放基本类型的变量数据和对象的引用, 但对象本身不存放在栈中,而是存放在堆 (new 出来的对象)或者常量池中(字符串 常量对象存放在常量池中)
堆
存放所有new出来的对象。堆内存用 来存放由new创建的对象和数组,在堆 中分配的内存,由Java虚拟机的自动垃 圾回收其来管理。
在堆中产生了一个数组或者对象之后,还可以在栈中定义 一个特殊的变量,让栈中的这个变量的取值等于数组或者 对象在堆内存中的首地址,栈中的这个变量就成了数组或 对象的引用变量,以后就可以在程序中使用栈中的引用变 量来访问堆中的数组或者对象,引用变量相当于为数组或 者对象起的一个名称。引用变量是普通的变量,定义在栈 中分配,引用变量在程序运行到其作用域之外后被释放。
即使运行到使用new产生数组或者对象的语句所在的代码 块外,数组和对象本身占据的内存不会被释放,数组和对象 在没有引用变量指向它的时候,才会变成垃圾,不能再被使 用,但仍然占据内存空间不放,在随后的一个不确定的时间 被垃圾回收器收走(释放掉)。这也是Java比较占内存的原因。
常量池
在编译期被确定,在堆中分配出来的一块内存区域, 并被保存在已编译的.class文件中的一些数据。 它包括了关于类、方法、接口等中的常量,String 常量和基本类型常量,可以存储不经常改变的东 西(public static final)。常量池中的数据可以共享。
Class 文件中的常量池(编译器生成的各种字面量 和符号引用)会在类加载后被放入这个区域。
创建实例分析
1.使用new,如:String s1 = new String("myString")
在程序编译期,编译程序先去方法区的字符串常量池检查, 是否存在字符串"myString",如果不存在,则在常量池中开辟 一个内存空间存放"myString";如果存在的话,则不用重新开 辟空间,保证常量池中只有一个,节省空间。然后在堆内存 中开辟一块空间存放new出来的String实例,指向常量池中的 "myString",在栈中开辟一个 空间,命名为"s1",存放的值为堆中String实例的内存地址。这个 过程就是将引用 s1 指向 new 出来的 String 实例。
2.直接定义:如:String s1 = "myString"
在程序编译期,编译程序先去字符串常量池检查,是否存在"myString", 如果不存在,则在常量池中共开辟一个内存空间存放"myString";如果 存在,则不用开辟空间。然后在栈中开辟一块空间,命名为"s1",存放 的值为常量池中"myString"的内存地址。
3.串联生成:如String s1 = my+"String";
程序运行时,动态分配并将连接后的新地址赋给s1
StringBuffer 与 StringBuilder
String,StringBuffer,StringBuilder
String
String的值不可变,导致每次对String的操作都会生成新的 String对象,不仅效率低下,而且浪费大量优先的内存空间
StringBuffer
StringBuffer是可变类和线程安全的字符串操作类, 任何对它指向的字符串的操作都不会产生新的对象。
每次StringBuffer对象都有一定的缓冲区容量,当字符 串大小没有超过容量时,不会分配新的容量,当字符串 大小超过容量时,会自动增加容量。适合多线程。
StringBuilder
可变类,线程不安全,效率高,速度更快,适合单线程操作。
小结
1.如果要操作少量的数据用 String;
2.多线程操作字符串缓冲区下操作大量数据StringBuffer。
3.单线程操作字符串缓冲区下操作大量数据StringBuilder。
StringBuffer和 StringBuilder源码
1.StringBuffer和StringBuilder都继承自抽象类AbstractStringBuilder
2.存储数据的字符数组也没有被final修饰, 说明值可以改变,且构造出来的字符串还
有空余位置拼接字符串,但是拼接下去肯 定有不够用的时候,这时候他们内部提供了一
个自动扩容机制,当发现长度不够的 时候(默认长度是16),会自动进行扩容工作, 扩展
为原数组长度的2倍加2,创建一个新的 数组,并将数组的数据复制到新数组,所以对于
拼接字符串效率要比String高。自动扩容 机制是在抽象类中实现的。
3.线程安全性:StringBuffer线程安全,因为StringBuffer 中很多方法都被synchronized
修饰了,多线程访问时,线 程安全,但是效率低下,因为它有加锁和释放锁的过程。 StringBuilder效率高,但是线程不安全。
异常的原理
在Java语言中,将程序执行中发生的不正常的情况称为"异常"。
分类
Error
Java虚拟机无法解决的严重问题.如JVM系统 内部错误、资源耗尽等严重问题。
StackOverflowError和OOM
Exception
其他因编程错误或偶然的外在因素导致的 一致性问题,可以使用针对性的代码进行处理。
空指针访问、试图读取不存在的文件、 网络连接中断、数组角标越界等。
对于错误,一般有两种解决方法:一是遇到错误就终止程序的运行。 另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误 消息的提示,以及错误的处理。
捕获错误最理想的是在编译期间, 但有的错误只有在运行时才会发生。
编译时异常
指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。
编译器要求Java程序必须捕获或声明是所有编译时异常。
对于这类异常,如果程序不处理,可能会带来意向不到的结果。
运行时异常
编译器不要求强制处置的异常。一般是指编程时的逻辑错误。
java.lang.RuntimeException 类及它的子类都是运行时异常。
对于这类异常,可以不做处理,若全处理可能会 对程序的可读性和运行效率产生影响。
受检与非受检异常
受检异常
Throwable
Exception
IOException
ClassNotFoundException
CloneNotSupportedException
FileNotFoundException
UnknownHostException
非受检异常
Error
RuntimeException
AirthmeticException
ClassCastException
IllegalArgumentException
IllegalStateException
IndexOutOfBoundsException
NoSuchElementException
NullPointerException
异常的处理
异常处理机制一
try-catch-finally
Java 异常处理
Java采用异常处理机制,是将异常处理的程序代码集中集中在一起, 与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
Java 提供的是异常处理的抓抛模型。
Java 程序的执行过程中如出现异常, 会生成一个异常类对象,该异常对象 将提交给Java运行时系统,这个过程 称为抛出异常。
异常对象 的生成
由虚拟机自动生成
由开发人员手动创建
方式
try-catch-finally
try
捕获异常的第一步是用 try{}语句块选定 捕获异常的范围,将可能出现异常的代码 放在try语句块中
catch
finally
捕获异常的最后一步是通过finally语句为 异常处理提供一个统一的出口,使得在控制 流转到程序的其他部分以前,能够对程序的 状态做统一的管理。
不论在try代码块中是否发生了异常处理, catch语句是否执行,catch语句是否有异常, catch语句中是否有return,finally块中的语 句都会被执行。
throws+异常类型
异常处理机制二
throws(声明抛出异常)
如果一个方法可能生成某种异常,但是并不能确定如何处理 这种异常,则此方法应显示的声明抛出异常,表明该方法将不 对这些异常进行处理,而由该方法的调用者负责处理。
在方法声明中用throws语句可以声明抛出异常的 列表,throws后面的异常类型可以是方法中产生的 异常类型,也可以是它的父类。
重写方法声明 抛出异常的原则
重写方法不能抛出比重写方法范围更大的异常类型。 在多态的情况下,对方法的调用-异常的捕获按父类 声明的异常处理。
手动抛出异常
Java异常类对象除在程序执行过 程中出现异常时由系统自动生成 并抛出,也可根据需要使用人工创 建并抛出。
首先要生成异常类对象,然后通过throw 语句实现抛出操作(提交给Java运行环境)
可以抛出的异常必须是Throwable或其子类的实例。
使用finally回收资源
finally
捕获异常的最后一步是通过finally语句为 异常处理提供一个统一的出口,使得在控制 流转到程序的其他部分以前,能够对程序的 状态做统一的管理。
不论在try代码块中是否发生了异常处理, catch语句是否执行,catch语句是否有异常, catch语句中是否有return,finally块中的语 句都会被执行。
throw 制造异常
手动抛出异常
Java异常类对象除在程序执行过 程中出现异常时由系统自动生成 并抛出,也可根据需要使用人工创 建并抛出。
首先要生成异常类对象,然后通过throw 语句实现抛出操作(提交给Java运行环境)
可以抛出的异常必须是Throwable或其子类的实例。
异常的处理方式之throws
throws(声明抛出异常)
如果一个方法可能生成某种异常,但是并不能确定如何处理 这种异常,则此方法应显示的声明抛出异常,表明该方法将不 对这些异常进行处理,而由该方法的调用者负责处理。
在方法声明中用throws语句可以声明抛出异常的 列表,throws后面的异常类型可以是方法中产生的 异常类型,也可以是它的父类。
重写方法声明 抛出异常的原则
重写方法不能抛出比重写方法范围更大的异常类型。 在多态的情况下,对方法的调用-异常的捕获按父类 声明的异常处理。
自定义异常
一般的,用户自定义异常类都是RuntimeException的子类。
自定义异常类通常需要编写几个重载的构造器
自定义异常需要提供serialVersionUID
自定义的异常通过throw抛出
自定义异常最重要的是异常类的名字,当异常 出现时,可以根据名字判断异常类型。
必须继承现有的异常类
IO流原理分析
用于处理设备之间的数据传输。如读/写文件,网络通讯等。
Java程序中,对于数据的输入/输出操作以"流"的方式进行。
输入
读取外部数据(磁盘、光盘等存 储设备的数据)到程序(内存)中。
输出
将程序(内存)数据输出到磁盘、 光盘等存储设备中
IO流的分类
1.按操作数据 单位不同分为
字节流(8bit)
字符流(16bit)
2.按数据流的 流向不同分为
输入流
输出流
3.按流的角色 的不同分为
节点流
直接从数据源或目的地读写数据
处理流
不直接连接到数据源或目的地,而是"连接" 在已存在的流之上,通过对数据的处理为 程序提供更为强大的读写功能。
IO流与文件操作
InputStream
int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
Reader
int read()
读取单个字符。作为整数读取的字符,范围在0到65535之间,如果已经到达流的末尾,则返回-1.
int read(char[] buf)
将字符读入数组。如果已经达到流的末尾,则返回-1。否则返回本次读取的字符数。
int read(char[] cbuf, int off, int len)
将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。 如果已经到达流的末尾,则返回-1。否则返回本次读取的字符数。
OutputStream
void write(int b)
将指定的字节写入此输出流。write的常规协定: 向输出流写入一个字节。要写入的字节是参数 b的八个低位。b的24个高位被忽略。即写入 0~255范围内的。
void write(byte[] b)
将b.length个字节从指定的byte数组写入此输出流。write(b)的 常规协定是:应该与调用write(b,0,b.length)的效果完全相同。
void write(byte[] b, int off, int len)
将指定byte数组中从偏移量off开始的len个字节写入此输出流。
flush()
刷新此输出流并强制写出所有缓冲的输出字节,调用 此方法指示应将这些字节立即写入它们预期的目标。
close()
关闭此输出流并释放与该流关联的所有系统资源
Writer
void write(int c)
写入单个字符。要写入的字符包含在给定整数值的16个低 位中,16高位被忽略。即写入0到65535之间的Unicode码
void write(char[] cbuf)
写入字符数组
void write(char[] cbuf, int off ,int len)
void write(String str)
写入字符串
void write(String str, int off, int len)
写入字符串的某一部分
void flush()
刷新该流的缓冲,则立即将它们写入预期目标
close()
关闭此输出流并释放与该流关联的所有系统资源
读取文件
1.建立一个流对象,将已存在的一个文件加载进流。
FileReader fr = new FileReader(new File("Test.txt"));
2.创建一个临时存放数据的数组
char[] ch = new char[1024];
3.调用流对象的读取方法将流中的数据读入到数组中
fr.read(ch)
4.关闭资源
fr.close();
写入文件
1.创建流对象,建立数据存放文件
FileWriter fw = new FileWriter(new File("Test.txt"));
2.调用流对象的写入方法,将数据写入流
fw.write("....");
3.关闭流资源,并将流中的数据清空到文件中
fw.close()
缓冲流
目的
为了提高数据读写的速度,JavaAPI提供了 带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组,缺省使用8192 个字节(8kb)的缓冲区。
public class BufferedInputStream extends FilterInputStream{ private static int DEFAUL_BUFFER_SIZE = 8192; }
缓冲流要"套接"在相应的节点流之上, 根据数据操作单位可以把缓冲流分为
BufferedInputStream 和 BufferedOutputStream
BufferedReader 和 BufferedWriter
特点/用法
1.当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。
2.当使用BufferedInputStream读取字节文件时,BuffedInputStream 会一次性从文件中读取8192个(8kb),
存在缓冲区中,直到缓冲区装满 了,才重新从文件中读取下一个8192个字节数组。
3.向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区 写满,BufferedOutputStream才会把缓冲区
中的数据一次性写到文件里。 使用方法flush()可以强制将缓冲区的内容全部写入输出流
4.关闭流的顺序和打开流的顺序相反。只要关闭最外层流 即可,关闭最外层流也会相应关闭内层节点流。
5.flush()方法的使用:手动将 buffer 中内容写入文件
6.如果是带缓冲区的流对象的close()方法,不但会关闭流, 还会在关闭流之前刷新缓冲区,关闭后不能再写出。
对象流
ObjectInputStream和ObjectInputStream
用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源 中,也能把对象从数据源中还原回来。
序列化
用ObjectOutputStream类保存基本数据类型或对象的机制
反序列化
用ObjectInputStream类读取基本数据类型或对象的机制
ObjectInputStrem和ObjectOutputStream不能序列化static和transient修饰的成员变量
对象的序列化与反序列化
对象序列化
1.对象序列化机制允许把内存中的Java对象转化成平台无关的二进制流, 从而允许把这种二进制流持久
的保存在磁盘上,或通过网络将这种二进制 流传输到另一个网络节点。//当其他程序获取了这种二进制流,就可以恢 复成原来的Java对象。
好处
可将任何实现了Serializable接口的对象转化为字节数据, 使其保存和传输时可被还原。
序列化是RMI(Remote Method Invoke—远程方法调用) 过程的参数和返回值都必须实现的机制,
而RMI是 JavaEE的基础。因此序列化机制是JavaEE平台的基础
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,
为了让某个类是可序列化的,该类 必须实现如下两个接口之一。否则,会抛出 NotSerializableException 异常
Serializable
Externalizable
凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态常量:
private static final long serialVersionUID;
serialVersionUID用来表明类的不同版本间的兼容性。简言之,目的是以序列化对象进行版本控制,有关各 版本返学裂化时是否兼容
如果类没有显示定义这个静态常量,它的值是 Java运行时环境根据类的内部细节自动生成的。
若类的实例变量做了修改,serialVersionUID可 能发生变化。建议显示声明。
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类
的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就 会序列化版本不一致的异常。(InvalidCastException)
使用对象流序列化对象
1.若某个类实现了Serializable 接口,该类的对象就是可序列化的:
1.创建一个 ObjectOutputSream
2.调用ObjectOutputStream对象的 writerObject(对象)方法输出可序列化对象
3.注意写出一次,操作flush()一次
反序列化
1.创建一个 ObjectInputStream
2.调用 readObject() 方法读取流中的对象
强调
如果某个类的属性不是基本数据类型或是String类型, 而是另一个引用类型,那么
这个引用类型必须是可序 列化的,否则拥有该类型的Field的类也不能序列化
序列化
将对象写入到磁盘或者进行网络传输
反序列化
将磁盘中的对象数据源读出。
对java.io.Serializable 接口的理解
实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完
全恢复回原来的样子。这一过程可通过网络进行。这意味着序列化机制 能自动
补偿操作系统间的差异。换句话说,可以先在windows机器上创建 一个对象,对
其序列化,然后通过网络发送给一台Unix机器,然后在那里准确 无误的重新"装配"。
不必关心数据在不同机器上如何表示,也不必关心字 节的顺序或者其他任何细节。
由于大部分作为参数的类如String,Integer等都实现了java.io.Serializable 的接口,也可以利用多态的性质,作为参数使接口更灵活。
标准输入流与标准输出流
System.in和System.out分别代表了系统标准的输入和输出设备
默认输入设备是:键盘,输出设备是:显示器
System.in的类型是InputStream
System.out的类型是PrintStream,其是OutputStream的子类,FilterOutputStream的子类
重定向
通过System类的setIn,setOut方法对默认设备进行改变。
public static void setIn(InputStream in)
public static void setOut(PrintStream out)
打印流
实现将基本数据类型的数据格式转换为字符串的输出
分类
PrintStream
PrintWriter
提供了一系列重载的print()和println()方法,用于多种数据类型的输出
PrintStream和PrintWirter的输出不会抛出IOException异常
PrintWirter和PrintStream有自动flush功能
PrintStream打印的所有字符都是用平台的默认字符编码转换为字节。 在需要写入字符而不是写入字节的情况下,应该是用PrintWriter类。
System.out返回的是PrintStream的实例
转换流
转换流提供了在字节流和字符流之间的转换
两个转换流
InputStreamReader
将InputStream转换为Reader
实现将字节的输入流按指定字符集转换为字符的输入流。
需要和InputStream“套接”
OutputStreamWriter
将Writer转换为OutputStream
实现将字符的输出流按指定字符集转换为字节的输出流。
需要和OutputStream“套接”
字节流中的数据都是字符时,转成字符流操作更高效
很多时候使用转换流来处理文件乱码问题。 实现编码和解码的功能。
应用
将字符按指定编码格式存储
对文本数据按指定编码格式来解读
指定编码表的动作由构造器完成
字符编码与解码
编码
字符串—>字节数组
解码
字节数组—>字符串
RandomAccessFile 类
随机存取文件流
实现了DataInput、DataOutput两个接口, 意味着这个类既可以读也可以写。
支持"随机访问"的方式,程序可以 直接跳到文件的任意地方来读、写文件
支持只访问文件的部分内容
可以向已存在的文件后追加内容
RandomAccessFile对象包含一个 记录指针,用以标示读写处的位置。
RandomAccessFile类对象可以自由移动记录指针
long getFilePointer():获取文件记录指针的当前位置。
void seek(long pos):将文件记录指针定位到pos位置
构造器
public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)
创建RandomAccessFile类 实例需要指定一个mode 参数,该参数指定 RandomAccessFile 的访问模式
r
以只读方式打开
rw
打开以便读取和写入
rwd
打开以便读取和写入;同步文件内容的更新
rws
打开以便读取和写入;同步文件内容和元数据的更新
如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的 文件,如果读取的文件不存在则会出现异常。
如果模式为rw读写,如 果文件不存在,则会去创建文件,如果存在则不会创建。
可以用 RandomAccessFile类来实现一个多线程断点下载的功能, 下载前都会建立两个临时文件,
一个是与被下载文件大小相同的 空文件,另一个是记录文件指针的位置文件,每次暂停的时候,
都会 保存上一次的指针,然后断点下载的时候,会继续从上一次的地方 下载,从而实现断点下载或上传的功能。
总结
1.流是用来处理数据的。
2.处理数据时,一定要先明确数据源,与数据目的地。
数据源可以是文件,可以是键盘。
数据目的地可以是文件、显示器或者其他设备
3.流只是在帮助数据进行传输,并对传输的数据 进行处理,比如过滤处理、转换处理等。
Java反射概述
反射机制允许程序在执行期间借助于API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象), 这个
对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。透过这个对象 可以看到类的结构,所以,我们称之为"反射"
动态语言
在运行时可以改变其结构的语言,通俗点讲,在运行时代码可以根据某些条件改变自身结构
静态语言
运行时结构不可变的语言就是静态语言。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPDuKpRX-1581551561033)(C:\Users\10540\AppData\Roaming\Typora\typora-user-images\1581514180261.png)]
Java 反射机制的研究与应用
Java反射机制提供的功能
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时获取泛型信息
在运行时调用任意一个对象的成员变量和方法
在运行时处理注解
生成动态代理
相关API
java.lang.Class:代表一个类
java.lang.reflect.Method:代表类的方法
java.lang.reflect.Field:代表类的成员变量
java.lang.reflect.Constructor:代表类的构造器
....
理解Class类并获取Class的实例
Class类
在Object类中定义了以下的方 法,此方法将被所有子类继承:
public final Class getClass()
返回值的类型是一个Class类,此类是Java反射的源头, 实际上所谓反射从程序的运行结果来看很好理解,即: 可以通过对象反射求出类的名称。
通过反射可以得到的信息
类的属性
方法
类实现了 哪些接口
对于每个类而言,JRE都为其保留一个不变的Class类型的对象。
一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息
Class本身也是一个类
Class对象只能由系统建立对象
一个加载的类在JVM中只会有一个Class实例
一个Class对象对应的是一个加载到JVM中的一个.class文件
每个类的实例都会记得自己是由哪个Class实例所生成
通过Class可以完整地得到一个类中的所有被加载的结构
Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象。
Class类的常用方法
获取Class类的实例(4种方法)
若已知具体的类,通过类的class属性获取, 该方法最为安全可靠,程序性能最高。
Class clazz = String.class
已知某个类的实例,调用该实例 的getClass()方法获取Class对象
Class clazz = "string".getClass()
已知一个类的全类名,且该类在类路径下, 可通过Class类的静态方法forName()获取, 可能抛出ClassNotFoundException
Class clazz = Class.forName("java.lang.String");
不做要求
ClassLoader cl = this.getClass().getClassLoader();
Class clazz = cl.loadClass("类的全类名");
哪些类型可以有Class对象?
1.class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
2.interface:接口
3.[]:数组
4.enum:枚举
5.annotation:注解@interface
6.primitive type:基本数据类型
7.void
类加载器 ClassLoader简介
类的加载过程
当程序主动使用某个类时,如果该类还 未被加载到内存中,则系统会通过如下 三个步骤来对该类进行初始化
类的加载(Load)
将类的class文件读入内存,并为之创建一个 java.lang.Class对象。此过程由类加器完成。
类的链接(Link)
将类的二进制数据合并到JRE中
类的初始化(Initialize)
JVM负责将类进行初始化
加载
将class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区运行
时数据结构,然后生成一个代表这个类 的java.lang.Class对象,作为方
法区中类 数据的访问入口(即引用地址)。所有需 要访问和使用类数据只能通过这个Class 对象。这个加载的过程需要类加载器参与。
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程。
验证
确保加载的类信息符合JVM规范,如以cafe开头,没有安全方面的问题
准备
正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
解析
虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化
执行类构造器<clinit>()方法的过程。类构造器<clinit>() 方法是由编译器自动
收集类中所有类变量的赋值动作 和静态代码块中的语句合并产生的。(类构造器是构造 类信息的,不是构造类对象的构造器)
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发器父类的初始化。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
什么时候会发 生类初始化?
类的主动引用(一定 会发生类的初始化)
当虚拟机启动,先初始化main方法所在的类
new 一个类的对象
调用类的静态成员(除了final常量)和静态方法
使用java.lang.reflect包的方法对类进行反射调用
当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类
类的被动引用(不会 发生类的初始化)
当访问一个静态域时,只有真正声明这个域的类才会被初始化
当通过子类引用父类的静态变量,不会导致子类初始化
通过数组定义类引用,不会触发此类的初始化
引用常量不会触发此类的初始化(常量在连接阶段就存入调用类的常量池中了)
类加载器 的作用
类加载 的作用
1.将class文件字节码内容加载到内存中,并
2.将这些静态数据转换成方法区的运行时数据结构,然后
3.在堆中生成一个代表这个类的java.lang.Class对象, 作为方法区中类数据的访问入口。
类缓存
标准的JavaSE类加载器可以按要求查找类, 但一旦某个类被加载到类加载器中,它将维 持加载(缓存)一段时间。不过JVM垃圾回收 机制可以回收这些Class对象。
用来把类(class)装载进内存的
JVM规范定义了如 下类型的类加载器
系统类加载器
负责java -classpath 或 -D java.class.path 所指的目录下的类与jar包装入工作库,是最常用的加载器
扩展类加载器
负责jre/lib/ext目录下的jar包或-D java.ext.dirs 指定目录下的jar包装入工作库
引导类加载器
用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取。
检查类是否已装载
自底向上检查类是否已装载
自定义类加载器—>系统类加载器—>扩展类加载器—>引导类加载器
加载类
自顶向下尝试加载类
引导类加载器—>扩展类加载器—>系统类加载器—>自定义类加载器
操作
1.获取一个系统类加载器
2.获取系统类加载器的父类加载器,即扩展类加载器
3.获取扩展类加载器的父类加载器,即引导类加载器
4.测试当前类由哪个类加载器进行加载
5.测试JDK提供的Object类由哪个类加载器加载
6.关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流
ClassLoader classloader = ClassLoader.getSystemClassLoader();
classloader = classloader.getParent();
classloader = classloader.getParent();
classloader = Class.forName("exer2.ClassloaderDemo).getClassLoader();
classloader = Class.forName("java.lang.Object").getClassLoader();
InputStream in = null; in = this.getClass()
.getClassLoader().getResourceAsStream("exer2\\test.properties); System.out.println(in);
类加载机制
加载
将class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区运行 时数据结构,
然后生成一个代表这个类 的java.lang.Class对象,作为方法区中类数据的访问入
口(即引用地址)。所有需 要访问和使用类数据只能通过这个Class 对象。这个加载的过程需要类加载器参与。
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程。
验证
确保加载的类信息符合JVM规范,如以cafe开头,没有安全方面的问题
准备
正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
解析
虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化
执行类构造器<clinit>()方法的过程。类构造器<clinit>() 方法是由编译器自动收集类中所有类
变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造 类信息的,不是构造类对象的构造器)
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发器父类的初始化。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
获取 Class 实例的四种方式
Class clazz = 类.class
Class clazz = 实例名.getClass();
Class clazz = Class.forName("全类名");
ClassLoader cl = this.getClass().getClassLoader(); Class clazz = cl.loadClass("类的全类名");
创建运行时类的对象
创建类的对象
调用Class对象的 newInstance()方法
1.类必须有一个无参的构造器
2.类的构造器的访问权限需要足够
获取运行时类的完整结构
通过反射获取运行时类的完整结构
Field
Method
Constructor
Superclass
Interface
Annotation
实现的全部接口
public Class<?>[] getInterfaces()
确定此对象所表示的类或接口实现的接口
所继承的父类
public Class<? Super T> getSuperclass()
返回表示此Class所表示的实体(类、接口、基本类型)的父类Class
全部的构造器
public Constructor<T> getConstructors();
返回此Class对象所表示的类的所有public构造方法
public Constructor<T>[] getDeclaredConstructors()
返回此 Class 对象表示的类声明的所有构造方法
Constructor类
public int getModifiers()
取得修饰符
public Sring getName()
取得方法名称
public Class<?>[] getParameterTypes()
取得参数的类型
全部的方法
public Method[] getDeclaredMethods()
返回此Class对象所表示的类或接口的全部方法
public Method[] getMethods()
返回此Class对象所表示的类或接口的public的方法
Method类
public Class<?> getReturnType()
取得全部的返回值
public Class<?>[] getParameterTypes()
取得全部的参数
public int getModifiers()
取得修饰符
public Class<?>[] getExceptionTypes()
取得异常信息
全部的Field
public Field[] getFields()
返回此Class对象所表示的类或接口的public的Field
public Field[] getDeclaredFields()
返回此Class对象所表示的类或接口的全部Field
Field 方法
public int getModifiers()
以整数形式返回此Field的修饰符
public Class<?> getType()
得到Field的属性类型
public String getName()
返回Field的名称
Annotation相关
getAnnotation(Class<T> annotationClass)
getDeclaredAnnotations()
泛型相关
Type getGenericSuperclass()
获取父类泛型类型
ParameterizedType
泛型类型
getActualTypeArguments()
获取实际的泛型类型参数数组
类所在的包
Package getPackge()
反射的应用: 动态代理
代理设计模式的原理
使用一个代理价格对象包装起来,然后用该代理对象取代原始对象。
任何对原始对象的调用都要通过代理。代理对象决定是否以及何 时将方法调用转到原始对象上。
静态代理
特征:代理类和目标对象的类都是在编译期间就确定下来, 不利于程序的扩展。每一个代理类只能为一个接口服务。
动态代理
定义
客户通过代理类来调用其他对象的方法,并且是在 程序运行时根据需要动态创建目标类的代理对象。
使用场合
调用
远程方法调用(RMI)
优点
抽象角色(接口)中声明的所有方法都被转移 到调用处理器一个集中的方法中处理,这样, 可以更加灵活和统一的处理众多的方法。
API
Proxy
专门完成代理的操作类,是所有动态代理类的父类。 通过此类为一个或多个接口动态生成实现类。
提供用于创建动态代理类和 动态代理对象的静态方法
static Class<?> getProxyClass(ClassLoader loader, Class<?>...interfaces)
创建一个动态代理类所对应的Class对象
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
直接创建一个动态代理对象
步骤
1.创建一个实现接口InvocationHandler的类, 它必须实现invoke方法,以完成代理的具体操作
public Object invoke(Object theProxy, Method method, Object[] params) throws Throwable{
try {
Object retVal = method.invoke(targetObj,params);
return retVal;
} catch(.){
..
}
}
2.创建被代理的类以及接口
3.通过Proxy的静态方法newProxyInstance() 创建一个Subject接口代理
RealSubject target = new RealSubject();
DebugProxy proxy = new DebugProxy(target);
Subject sub = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(), new Class[]{Subject.class}, proxy);
4.通过Subject代理调用RealSubject实现类的方法
Stirng info = sub.say("Peter,24); System.out.println(info);
如何实现网络中主机的相互通讯
通信双方地址
IP
端口号
一定的规则(网络通信协议)
OSI参考模型
TCP/IP参考模型(TCP/IP协议)
网络通讯要素
通信要素1
IP和端口号
IP
InetAddress
唯一的标识Internet上的计算机(通信实体)
本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
IP地址分类方式1
IPV4
4个字节组成,4个0-255。大概24亿,30亿都在北美,亚洲4亿。以点分十进制表示。
IPV6
128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开.
IP地址分类方式2
公网地址(万维网使用)
私有地址(局域网使用)
192.168.开头的是私有地址,范围即为192.168.0.0--192.168.255.255.
专门为组织机构内部使用
端口号
用于表示正在计算机上运行的进程(程序)
不同的进程有不同的端口号
被规定为一个16位的整数0-65535
端口分类
端口号与IP地址的组合得出一个网络套接字:Socket
通信要素2
网络协议
计算机网络中实现通信必须有一些约定,即通信协议,对速率、 传输代码、代码结构、传输控制步骤、出错控制等制定标准。
TCP/IP协议簇
传输控制协议TCP
网络互联协议IP
是网络层的主要协议,支持网间互联的数据通信
物理链路层、IP层、传输层和应用层
TCP协议
使用TCP协议之前,必须先建立TCP连接,形成传输数据通道
传输前,采用"三次握手"方式,点对点通信,是可靠的
TCP协议进行通信的两个应用进程:客户端、服务端
在连接中可进行大数据量的传输
传输完毕,需释放已建立的连接,效率低。
三次握手
(1)第一次握手:建立连接时,客户端A发送SYN包[SYN=1,seq=x]到服务器B,并进入SYN_SEND状态,等待服务器B确认。
(2)第二次握手:服务器B收到SYN包,必须确认客户A的SYN,同时自己也发送一个SYN包,
即SYN+ACK包[SYN=1,ACK=1,seq=y,ack=x+1],此时服务器B进入SYN_RECV状态。
(3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK[ACK=1,seq=x+1,ack=y+1],
此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据。
三次握手完成后,客户端和服务器就建立了TCP连接。这时可以调用accept函数获得此连接。
三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换TCP窗口大小信息。
在socket编程中,客户端执行connect()时,将会触发三次握手。
四次挥手
(1)首先A B端的TCP进程都处于established状态,当A的应用程序传送完报文段,就会去主动关闭连接。
A会停止发送报文段(但是还会接收),并向B发送[FIN = 1,seq=u]数据,之后进入FIN-WAIT-1状态;
(2)B接收到A发送的请求之后,会通知应用进程,A已经不再发送数据,同时B会向A发送ACK确认数据
[ACK=1,seq=v,ack=u+1 ],B进入CLOSE-WAIT状态,A接收到B发送的数据之后,
A进入FIN-WAIT-2状态;此时A到B方的连接已经关闭了(即半连接状态)。
(3)当B的应用进程发现自己也没有数据需要传送,B应用进程就会发出被动关闭的请求,B此时向A发送
[FIN=1,ACK=1,seq=w,ack=u+1]数据,并且进入LAST-ACK状态;
(4)A接收到B发送的数据之后,向B发送ACK确认数据[ACK =1,seq=u+1,ack=w+1],进入TIME-WAIT状态,
等待2MSL之后正常关闭连接进入CLOSED状态;B接收到A发送的确认之后进入CLOSED状态。B到A方的连接关闭!至此,TCP连接才真正全部关闭!
UDP协议
将数据、源、目的封装成数据包,不需要建立连接
每个数据包的大小限制在64K内
发送不管对方是否准备好,接收方收到也不确认,是不可靠的
可以广播发送
发送数据结束时无需释放资源,开销小,速度快
Socket
网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。
通信的两端都要有Socket,是两台机器间通信的端点。
网络通信其实就是Socket间的通信。
Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
一般主动发起通信的应用程序属于客户端,等待通信请求的为服务端。
分类
流套接字
使用TCP提供可依赖的字节流服务
数据报套接字
使用UDP提供"尽力而为"的数据报服务
常用构造器
public Socket(InetAddress address, int port)
创建一个流套接字并将其连接到指定IP地址的指定端口号
public Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号
常用的方法
public InputStream getInputStream()
返回此套接字的输入流。可用于接收网络信息
public OutputSteam getOutputStream()
返回此套接字的输出流。可用于发送网络信息
public InetAddress getInetAddress()
此套接字连接到的远程IP地址;如果套接字是未连接的,则返回null.
public InetAddress getLocalAddress()
获取套接字绑定的本地地址。即本地的IP地址。
public int getPort()
此套接字连接到的远程端口号;如果尚未连接套接字,则返回0
public int getLocalPort()
返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回-1。即本段的端口号
public void close()
关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。
需要常见新的套接字对象。关闭此套接字也将会关闭该套接字的InputStream和OutputStream.
public void shutdownInput()
如果在套接字上调用 shutdownInput()后从套接字输入 流读取内容,则流将返回EOF(文件结束符)。
即不能在 从此套接字的输入流中接收任何数据。
public void shutdownOutput()
禁用此套接字的输出流。对于TCP套接字,任何以前写入的数据都将被发送,
并且后跟TCP的正常连接终止序列。如果套接字上调用shutdownOutput()后写入套接
字输出流,则该流将抛出IOException。即不能通过此套接字的输出流发送任何数据。
OSI参考模型
应用层
表示层
会话层
传输层
网楼层
数据链路层
物理层
TCP/IP参考模型(或TCP/IP协议)
应用层
传输层
网络层
物理+数据链路层
Socket 的TCP编程
基于Socket的TCP编程
客户端Socket的工作过程包含 以下四个基本的步骤:
1.创建Socket
根据指定服务端的IP地址或端口号构造Socket对象。若服务器端响应,则建立客户端到服务器的通信线路。 若连接失败,会出现异常。
2.打开连接到Socket的输入/输出流
使用getInputStream()方法获得输入流, 使用getOutputStream()方法获得输出 流,进行数据传输。
3.按照一定的协议对Socket进行读/写操作
通过输入流读取服务器放入线路的信息(但不能读取 自己放入线路的信息),通过输出流将信息写入线程。
4.关闭Socket
断开客户端到服务器的连接,释放线路
客户端创建Socket对象
客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器发送连接。
构造器
Socket(String host,int port)
向服务器发起TCP连接,若成功,则创建Socket对象,否则抛出异常。
Socket(InetAddress address, int port)
根据InetAddress对象所表示的IP地址以及端口号port发起连接
客户端建立SocketAtClient对象的过程就是向服务器发出套接字连接请求
服务器程序的工作过程包含以 下四个基本的步骤:
1.调用 ServerSocket(int port)
创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
2.调用accept()
监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
3.调用该Socket类对象的getOutputStream() 和getInputStream()
获取输出流和输入流,开始网络数据的发送和接收。
4.关闭ServerSocket和Socket对象
客户端访问结束,关闭通信套接字
服务器建立ServerSocket对象
ServerSocket对象负责等待客户端请求建立套接字连接,类似邮局某个窗口中的业务员。
也就是说,服务器必须事先建立一个等待客户请求建立套接字连接的ServerSocket对象。
所谓"接收"客户的套接字请求,就是accept()方法会返回一个Socket对象。
基于UDP协议的网络编程
类DatagramSocket和DatagramPacket实现了基于UDP协议网络程序。
UDP数据报通过数据报套接字DatagramSocket发送和接收,系统不能 保证UDP数据报一定能够安全送到目的地,也不能确定什么时候到。
DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的 IP地址和端口号以及接收端的IP地址和端口号。
UDP协议中每个数据报都给出了完整的地址信息,因此无须建立 发送和接收方的连接。如同发快递包裹一样。
DatagramSocket常用方法
DatagramPacket的常用方法
UDP网络通信
流程
1.DatagramSocket与DatagramPacket
2.建立发送端,接收端
3.建立数据包
4.调用Socket的发送、接收方法
5.关闭Socket
发送端与接收端是两个独立的运行程序
URL编程
URL类
URL:统一资源定位符,它表示Internet上某一资源的地址。
它是一种具体的URL,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
通过URL我们可以访问Internet上的各种网络资源。浏览器 通过解析给定的URL可以在网络上查找指定相应文件或 其他资源。
URL的基本结构
<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
构造器
public URL(String spec)
通过一个表示URL地址的字符串可以构造一个URL对象。
public URL(URL context, String spec)
通过基 URL 和相对 URL 构造一个URL对象
public URL(String protocol,String host, String file)
new URL("http", "www.atguigu.com", "download.html")
public URL(String protocal, String host, int port, String file)
new URL("http", "www.atguigu.com", 80, "download.html")
URL类的构造器都声明抛出非运行时异常,必须要 对这一异常进行处理,通常是用 try-catch语句进行捕获。
常用方法
一个URL对象生成后,其属性是不能被修改的,但可以通过它给定的方法来获取这些属性。
public String getProtocol()
获取该URL的协议名
public String getHost()
获取该URL的主机名
public String getPort()
获取该URL的端口号
public String getPath()
获取该URL的文件路径
public String getFile()
获取该URL的文件名
public String getQuery()
获取该URL的查询名
针对HTTP协议的URLConnection
URL的方法openStream():能从网络上读取数据
若希望输出数据,例如向服务器端的CGI(公共网关接口, 是用户浏览器和服务器端的程序进行连接 接口)
程序发送一些数据,则必须先与URL建立连接,然后才能对其进 行读写,此时需要使用URLConnection。
URLConnection
表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个URL对象上通过方法
openConnection() 生成对应的URLConnection对象。 如果连接过程失败,将产生IOException
URL netchinaren = new URL("http://www.atguigu.com/index.html");
URLConnection u = netchinaren.openConnection()
通过URLConnection对象获取的输入流和输出 流,即可以实现现有的CGI程序进行交互。
public InputStream getInputStream() throws IOException
public OutputStream getOutputStream() throws IOException
URI、URL和URN的区别
URI,统一资源标识符,用来唯一的标识一个资源。
URL,统一资源定位符,它是一种具体的URI,即 URL可以用来表示一个资源,而且还指明了 如何locate这个资源。
URN,统一资源命名,是通过名字来标识资源
URI是以一种抽象的,高层次概念定义统一资源标识, 而URL和URN则是具体的资源标识的方式。URL和 URN都是一种URI。
在Java的URI中,一个URI实例可以该表绝对的, 也可以是相对的,只要它符合URI的语法规则。
而URL类则不仅符合语义,还包含了定位该资 源的信息,因此它不能是相对的。
简介
速度更快
代码更少
强大的Stream API
便于并行
最大化减少空指针异常:Optional
Nashorn引擎,允许在JVM上运行JS应用
Lambda 表达式
使用原因
Lambda是一个匿名函数,可以把Lambda表达式理解 为是一段可以传递的代码(将代码像数据一样进行 传递)。
使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能 力得到了提升。
语法
Lambda表达式
Lambda操作符或箭头操作符
->
将Lambda分为两个部分
左侧
指定了Lambda表达式需要的参数列表
右侧
指定了Lambda体,是抽象方法的实现逻辑,也即 Lambda表达式要执行的功能
格式一:无参,无返回值.Runnable r1 = ()->{System.out.println("hello Lambda!");}
格式二:Lambda需要一个参数,但是没有返回值:Consumer<String> con = (String str)->{System.out.println(str);}
格式三:数据类型可以省略,因为可由编译器推断得出,称为"类型推断":Consumer<String> con = (str)->{System.out.println(str);}
格式四:Lambda若只需要一个参数时,参数的小括号可以省略:Consumer<String> con = str ->{System.out.println(str);}
格式五:Lambda需要两个或以上的参数,多条执行语句,并且可以由返回值:
Comparator<Integer> com = (x,y)->{System.out.println("");return Integer.compare(x,y)}
格式六:当Lambda体只有一条语句时,return与大括号若有,都可以省略:
Consumer<String> con = (x,y)->Integer.compare(x,y);
类型推断
Lambda表达式中无需指定类型, 程序依然可以编译,这是因为javac 根据程序的上下文,
在后台推断出 了参数的类型。Lambda表达式的 类型依赖于上下文环境,是由编译 器推断出来的。这就是"类型推断"
Java8核心函数式接口
只包含一个抽象方法的接口,称为函数式接口
可以通过Lambda表达式来创建该接口的对象。
可以在一个接口上使用@FunctionalInterface注解, 这样做可以检查它是否是一个函数式接口。同时 javadoc也会包含一条声明,说明这个接口是一个 函数式接口。
在java.lang.function包下定义了Java8的丰富的函数式接口
java不但可以支持OOP还可以支持OOF(面向函数编程)
在java8中,Lambda表达式就是一个函数式接口的实例。
Java内置四大核心函数式接口
方法引用与构造器引用
方法引用
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法调用
方法引用可看做是Lambda表达式深层次的表达。 换句话说,方法引用就是Lambda表达式,
也就是函数式接口的一个实例,通过方法的名字来指向一个 方法.
要求:实现接口的抽象方法的参数列表和返回值类型,必须 与方法引用等待方法的参数列表和返回值类型保持一致。
格式:使用操作符"::"将类(或对象)与方法名分隔开来。
如下三种主要使用情况:
对象::实例方法名
类::静态方法名
类::实例方法名
构造器引用
格式
ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。
可以把构造器引用赋值给定义的方法,要求 构造器参数列表要与接口中抽象方法的参 数列表一致,且方法的返回值即为构造器对 应类的对象。
Function<Integer, MyClass> fun = (n) -> new MyClass(n);
数组引用
格式
type[] :: new
Function<Integer, Integer[]> fun = (n) -> new Integer[n];
等同于
Function<Integer, Integer[]> fun = Integer[] :: new;
强大的 Stream API
说明
把真正的函数式编程风格引入到Java中。
Stream是Java8中处理集合的关键抽象概念, 它可以指定你希望对集合进行的操作,可以
执行非常复杂的查找、过滤和映射数据等 操作。使用Stream API对集合数据进行操作,
就类似于使用SQL执行的数据库查询。也可 以使用Stream API来并行执行操作。
简言之, Stream API 提供了一种高效且易于使用的处 理数据的方式。
作用
实际开发中,项目中多数数据源都来自于MySQL,Oracle等。但现在数据源可以更多了,
有MongDB,Redis等,而这些 NoSQL的数据就需要Java层面去处理。
Stream 和 Collection 集合的区别: Collection 是一种静态的内存数据结构,
而Stream是有关计算的。前者 是主要面向内存,存储在内存中,后者 主要是面向CPU,通过CPU实现计算。
是什么
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
集合讲的是数据,Stream讲的是计算。
注意点
1.Stream 自己不会存储元素
2.Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。
3.Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
Stream 操作的核心步骤
1.创建Stream
一个数据源(如:集合、数组),获取一个流
创建方式一
通过集合
Java8中的Collection接口被扩展,提供了两个获取流的方法
default Stream<E> stream()
返回一个顺序流
default Stream<E> parallelStream()
返回一个并行流
创建方式二
通过数组
Java8中的Arrays的静态方法stream()可以获取数组流
static <T> Stream<T> stream(T[] array)
返回一个流
重载形式,能够 处理对应基本 类型的数组
public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)
创建方式三
通过Stream的of()
可以调用Stream类静态方法of(),通过显示值 创建一个流。它可以接收任意数量的参数。
public static<T> Stream<T> of(T... values)
创建方式四
创建无限流
可以使用静态方法Stream.iterate()和Stream.generate(),创建无限流。
迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
生成:public static<T> Stream<T> generate(Supplier<T> s)
2.中间操作
一个中间操作链,对数据源的数据进行处理
多个中间操作可以连接起来形成一个流水线,除非流水 线上触发终止操作,否则中间操作不会执行任何的处理, 而在终止操作时一次性全部处理,称为"惰性求值"。
1.筛选与切片
filter(Predicate p)
接收Lambda,从流中排序某些元素
distinct()
筛选,通过流所生成元素的hashCode()和equals()去除重复元素
limit(long maxSize)
截断流,使其元素不超过给定数量
skip(long n)
跳过元素,返回一个扔掉了前n个元素的流。若流 中元素不足n个,则返回一个空流。与limit(n)互补
2.映射
map(Function f)
接受一个函数作为参数,该函数会被应用到 每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的DoubleStream
mapToInt(ToIntFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的 IntStream
mapToLong(ToLongFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的 LongStream
flatMap(Function f)
接收一个函数作为参数,将流中的每个值都 换成另一个流,然后把所有流连接成一个流
3.排序
sorted()
产生一个新流,其中按自然顺序排序
sorted(Comparator com)
产生一个新流,其中按比较器顺序排序
3.终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值, 例如:List、Integer,甚至是 void
流进行了终止操作后,不能再次使用。
1.匹配与查找
allMatch(Predicate p)
检查是否匹配所有元素
anyMatch(Predicate p)
检查是否至少匹配一个元素
noneMatch(Predicate p)
检查是否没有匹配所有元素
findFirst()
返回第一个元素
findAny()
返回当前流中的任意元素
count()
返回流中元素总数
max(Comparator c)
返回流中最大值
min(Comparator c)
返回流中最小值
forEach(Consumer c)
内部迭代(使用Collection接口 需要用户去做迭代,称为外部 迭代。相反,StreamAPI使用内 部迭代)
2.归约
reduce(T iden, BinaryOperator b)
可以将流中元素反复结合起来,得到一个值,返回T
reduce(BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。返回Optional<T>
3.收集
collect(Collector c)
将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream 中元素做汇总的方法。
Collector接口中方法的实现决定了如何对流执行收集的操作(如收集到List、Set、Map)。
惰性求值与内部迭代
多个中间操作可以连接起来形成一个流水线,除非流水 线上触发终止操作,否则中间操作不会执行任何的处理, 而在终止操作时一次性全部处理,称为"惰性求值"。
筛选与切片
filter(Predicate p)
接收Lambda,从流中排序某些元素
distinct()
筛选,通过流所生成元素的hashCode()和equals()去除重复元素
limit(long maxSize)
截断流,使其元素不超过给定数量
skip(long n)
跳过元素,返回一个扔掉了前n个元素的流。若流 中元素不足n个,则返回一个空流。与limit(n)互补
映射与排序
2.映射
map(Function f)
接受一个函数作为参数,该函数会被应用到 每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的DoubleStream
mapToInt(ToIntFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的 IntStream
mapToLong(ToLongFunction f)
接收一个函数作为参数,该函数会被应用到 每个元素上,产生一个新的 LongStream
flatMap(Function f)
接收一个函数作为参数,将流中的每个值都 换成另一个流,然后把所有流连接成一个流
3.排序
sorted()
产生一个新流,其中按自然顺序排序
sorted(Comparator com)
产生一个新流,其中按比较器顺序排序
查找与匹配
allMatch(Predicate p)
检查是否匹配所有元素
anyMatch(Predicate p)
检查是否至少匹配一个元素
noneMatch(Predicate p)
检查是否没有匹配所有元素
findFirst()
返回第一个元素
findAny()
返回当前流中的任意元素
count()
返回流中元素总数
max(Comparator c)
返回流中最大值
min(Comparator c)
返回流中最小值
forEach(Consumer c)
内部迭代(使用Collection接口 需要用户去做迭代,称为外部 迭代。相反,StreamAPI使用内 部迭代)
归约与收集
2.归约
reduce(T iden, BinaryOperator b)
可以将流中元素反复结合起来,得到一个值,返回T
reduce(BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。返回Optional<T>
3.收集
collect(Collector c)
将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream 中元素做汇总的方法。
Collector接口中方法的实现决定了如何对流执行收集的操作(如收集到List、Set、Map)。
并行流与串行流
并行流就是把一个内容分为多个数据块,并用不同的线程分别处理每个数据 块的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
Java8中将并行进行了优化,我们可以很容易的对数据进行并行操作。
StreamAPI可以声明性的通过parallel()和sequential()在并行流和顺序流之间进行切换。
Optional 容器类
Option<T> 类是一个容器类,它可以 保存类型T的值,代表这个值存在。或者仅仅保存null,
表示这个值不存 在。原来用null表示一个值不存在, 现在Optionl可以更好的表达这个概 念。并且可以避免空指针异常。
Optional类的Javadoc描述如下:这是一个 可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
创建Optional 类对象的方法
Optional.of(T t)
创建一个Optional实例,t必须非空。
Optional.empty()
创建一个空的Optional实例
Optional.ofNullable(T t)
t可以为null
判断Optional容器 中是否包含对象
boolean isPresent()
判断是否包含对象
void ifPresent(Consumer<? super T> consumer)
如果有值,就执行Consumer接口的实现代码,并且该值会作为参数传给它。
获取Optional 容器的对象
T get()
如果调用对象包含值,返回该值,否则抛异常
T orElse(T other)
如果有值则将其返回,否则返回指定的other对象
T orElseGet(Supplier<? extends T> other)
如果有值则将其返回,否则返回由Supplier接口实现提供的对象
T orElseThrow(Supplier<? extends X> exceptionSupplier
如果有值,则将其返回,否则抛出由Supplier接口实现提供的异常
模块化系统
Jigsaw -> Modularity
实现目标
主要目的
减少内存的开销
只须必要模块,而非全部JDK模块,可简化各种类库和大型应用的开发和维护
改进Java SE平台,使其可以适应不同大小的计算设备
改进其安全性,可维护性,提高性能
jShell命令
设计理念
即写即得、快速运行
实现目标
让Java可以像脚本语言一样运行,从控制台启动jShell, 利用jShell在没有创建类的情况下
直接声明变量,计算 表达式,执行语句。即开发时可以在命令行里直接运 行Java的代码,
而无需创建Java文件,无需跟人解释 public static void main(String[] args)
jShell也可以从文件中加载语句或者将语句保存到文件中
jShell也可以是tab键进行自动补全和自动添加分号
接口的私有方法
方法的访问权限修饰符都可以声明为private的。
语法改进:try和钻石操作符
Java8中,可以实现资源的自动关闭,但是 要求执行后必须关闭的所有资源必须在 try子句中初始化,否则编译不通过。
Java9中,用资源语句编写try将更容易, 可以在try子句中使用已经初始化过的 资源,此时的资源时final的。
String存储结构变更
String再也不是char[]来存储的,改成了 byte[] 加上编码标记,节约了一些空间。
增强的Stream API
Java8提供的Stream能够利用多核架构实现声明式的数据处理。
在Java9中,Stream API变 得更好,Stream接口中添 加了4个新方法
takeWhile()
用于从Stream中获取一部分数据,接收一个Predicate来进行选择。 在有序的Stream中,takeWhile返回从开头开始的尽量多的元素。
dropWhile()
doWhile的行为与takeWhile相反,返回剩余的元素。
ofNullable()
允许创建一个单元素Stream,可以包含一个非空元素,也可以创建一个空Stream
iterate()重载
提供一个Predicate(判断条件)来指定什么事发后结束迭代。
局部变量的类型推断
减少了啰嗦和形式的代码,避免了信息冗余,而且对齐了变量名,更容易阅读
工作原理
在处理var时,编译器显示查看表达式右边部分, 并根据右边变量值的类型进行推断,作为左边 变量的类型,然后将该类型写入字节码当中。
注意
var不是一个关键字
var是一个类型名,只有在编译器需要知道类型的地方 才需要用到它。除此之外,
它就是一个普通合法的标识符。也就是说,除了不能用它作为类名,其他的都可 以
这不是JavaScript
集合新增创建不可变集合的方法
of(JDK9)
用来创建不可变的集合
copyOf(JDK10)
用来创建不可变的集合
会先判断来源集合是不是AbstractImmutableList类型的 ,如果是,就直接返回,如果不是,则调用of创建一个新的集合。
使用of和copyOf创建的集合为不可变集合,不能进行 添加、删除、替换、排序等操作,
不然会报 java.lang.UnsupportedOperationException异常
字符串新增一系列处理方法
判断字符串是否为空白
" ".isBlank() //true
去除首尾空白
" Javastack ".strip(); //"Javastack"
去除尾部空格
" Javastack ".stripTrailing(); // " Javastack"
去除首部空格
" Javastack ".stripLeading(); // "Javastack "
复制字符串
"Java".repeat(3); //"JavaJavaJava"
行数统计
"A\nB\nC".lines().count(); //3
Optional加强
Stream<T> stream()
将一个Optional对象转换为一个(可能是空的)Stream对象
value非空,返回仅包含此value的Stream;否则,返回一个空的Stream
boolean isEmpty()
判断value是否为空
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
value非空,执行参数1功能;如果value为空,执行参数2功能
Optional<T> or(Supplier<? extends Optional<? extends T> supplier)
value非空,返回对应的Optional; value为空,返回形参封装的Optional
T orElseThrow()
value非空,返回value;否则抛出异常NoSuchElementException
ZGC
ZGC是一个并发,基于region,压缩性的垃圾收集器,只有root扫描阶段会STW,
因此GC停顿时间不会随 着堆的增长和存活对象的增长而变长。
优势
GC暂停时间不会超过10ms
既能处理几百兆的小堆,也能处理几个T的大堆(OMG)
和G1相比,应用吞吐能力不会下降超过15%
为未来的GC功能和利用colord指针以及Load barriers优化奠定基础。
初始只支持64位系统
设计目标
支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于 15%。
将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层 堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。
1.谈谈你对Volatile的理解?
1.volatile是Java虚拟机提供的轻量级的同步机制
1.保证(内存)可见性
1.A线程从共享内存中读取变量C到A工作内存
2.A线程在A工作内存中操作变量C并刷新到主内存中
3.主内存修改后,B线程立即更新变量C的值
2.不保证原子性
3.禁止指令重排
2.JMM(Java内存模型)
1.可见性
代码验证
2.原子性
数据的一致性,完整性.也即某个线程正在做某个 具体业务时,中间不可以被加塞或者被分割,需要 保证整体完整。要么同时成功,要么同时失败。
代码实现
number++在多线程下是非线程安全的。
如何解决原子性问题?
synchronized
性能较差
使用java.util.concurrent.atomic包下原子封装类
代码实现
3.代码演示可见性+原子性
4.有序性
禁止指令重排序小总结
线程安全性获得保证
工作内存和主内存之间同步延迟现象导致的可见性问题 可以通过synchronized或volatile关键字进行解决,
他们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排序导致的可见性问题和有序性问题可以利用 volatile关键字解决,volatile的另一个特性作用就是禁 止指令重排序。
3.在哪些地方用到过volatile?
1.单例模式DCL代码
DCL:Double Check Lock 双端检索机制
代码实现(双重检查)
2.单例模式volatile分析
禁止指令重排
2.CAS知道吗?
1.比较并转换(CompareAndSwap)
CASDemo代码
期望值与真实值相同,则可以修改交换; 期望值与真实值不相同,则不可以修改交换。
2.CAS底层原理(自旋锁)?谈谈 对UnSafe(UnSafe类)的理解?
atomicInteger:getAndIncrement();
UnSafe
UnSafe类(是原子封装类保证原子性的原因)
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,
UnSafe类相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe类存在于
sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于UnSafe类中。
注意UnSafe类中的所有方法都是native修饰的,也就是说UnSafe类中的方法都直接调用操作系统底层资源执行相应任务。
2.变量valueOffset,表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。
3.变量value是用volatile修饰的,保证了多线程之间的内存可见性
CAS是什么
unsafe.getAndAddInt()
底层汇编
简单版小总结
CAS可以保证原子性是因为UnSafe类
3.CAS缺点
循环时间长开销大
只能保证一个共享变量的原子操作
引出来的ABA问题
3.谈谈原子类AtomicInteger的ABA问题?原子更新引用?
ABA问题怎么产生的
CAS 会导致"ABA问题"
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变
为B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one执行成功。
尽管线程one的CAS操作执行成功,但是不代表这个过程是没有问题的。
原子引用
代码案例
时间戳原子引用
AtomicStampedReference
ABA问题解决
AtomicStampedReference
代码实现
4.ArrayList是线程不安全的,编码写一个不安全的实例并给出解决方案?
线程不安全 代码实现
抛出异常:java.util.ConcurrentModificationException
并发修改异常:java.util.ConcurrentModificationException
分析方法
1.故障现象
2.导致原因
3.解决方案
4.优化建议(同样的错误不犯第2此)
解决方案
故障现象
java.util.ConcurrentModificationException
ArrayList
解决方案
1.new Vector<>()
2.Collections.synchronizedList(new ArrayList)
3.new CopyOnWriteArrayList();
写时复制
CopyOnWrite容器即写时复制的容器。往一个容器中添加元素时,不直接
往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出
一个新的容器Object[] newElements,然后新的容器Object[] newElements
里添加元素,添加完元素以后,再将原容器的引用指向新的容器 setArray(newElements).
这样做的好处是:可以对CopyOnWrite容器进行 并发的读,而不需要加锁,
因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器
源码分析
Set
解决方案
1.Collections.synchronizedSet(new HashSet())
2.new CopyOnWriteArraySet
Set底层还是CopyOnWriteArrayList
Map
解决方案
1.Collections.synchronizedMap(new HashMap())
2.new ConcurrentHashMap()
5.公平锁/非公平锁/重入锁/递归锁/自旋锁谈谈你的理解?手写一个自旋锁?
公平锁和非公平锁
是什么
公平锁
多个线程按照申领锁的顺序来获取锁,先来后到
非公平锁
多个线程获取锁的顺序不是按照申领锁的顺序, 有可能后申请的线程比先申请的线程优先获取 锁。在高并发情况下,有可能造成优先级反转 或饥饿现象。
两者区别
题外话
Java ReentrantLock而言,通过构造函数指定该锁 是否是公平锁,默认是非公平锁。非公平锁的优点 在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁
可重入锁(又名递归锁)
是什么
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码, 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
线程可以进入任何一个它已经拥有的锁同步着的代码块。
ReentrantLock/Synchronized就是一种典型的重入锁
可重入锁最大的作用就是避免死锁
ReentrantLockDemo
参考1
参考2
自旋锁
指的是尝试获取锁的线程不会立即阻塞,而是采用循环 的方式尝试去获取锁,这样的好处是减少线程上下文切 换的消耗,缺点是循环会消耗CPU
SpinLockDemo
独占锁(写)/共享锁(读)
独占锁
指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。
共享锁
指该锁可以被多个线程所持有。
对ReentrantReadWriteLock而言,其读锁是共享锁,写锁 是共享锁。读锁的共享锁可保证并发读是非常高效的,读 写,写读,写写过程都是互斥的。
代码
读写锁
6.CountDownLatch/CyclicBarrier/Semaphore使用过吗?
CountDownLatch
让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
CountDownLatch主要方法
CountDownLatchDemo
CyclicBarrier
CyclicBarrier字面的意思是可循环(Cyclic)使用的屏障(Barrier). 它要做的事是,让一组线程到达一个
屏障(也可以叫同步点)时被阻塞, 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程
才会继续干活,线程进入屏障通过CyclicBarrier的await()方法
CyclicBarrierDemo
Semaphore
信号量主要用于两个目的,一个用于多个共享资源的互斥使用, 一个用于并发线程数的控制
SemaphoreDemo
7.阻塞队列知道吗?
队列+阻塞队列
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当阻塞队列是满时,向队列中添加元素的操作将会被阻塞
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
同样试图往已满的阻塞队列中添加新元素的线程将同样也会被阻塞,直到其他的线程从队列中移除
一个或多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增
有什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都已经帮我们实现。
在concurrent包发布之前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,
而这会给我们的程序带来不小的复杂度。
BlockingQueue核心方法
架构梳理+种类分析
架构介绍
Collection:
List:
Queue:
BlockingQueue:
ArrayBlockingQueue:由数组队列组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayBlockingQueue:使用优先级队列实现的无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue:由链表结构组成的无界阻塞队列
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
种类分析
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayBlockingQueue:使用优先级队列实现的无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue:由链表结构组成的无界阻塞队列
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
用在哪里
生产者消费者模式
传统版
class ShareData { //资源类
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
/**
* 生产者
* @throws Exception
*/
public void increment() throws Exception {
lock.lock();
try {
//1.判断
while(number != 0) {
//等待,不能生产
condition.await();
}
//2.干活
number ++;
System.out.println(Thread.currentThread().getName()+"\t" + number);
//3.通知唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 消费者
* @throws Exception
*/
public void decrement() throws Exception {
lock.lock();
try {
//1.判断
while (number == 0) {
condition.await();
}
//2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3.通知唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 题目:一个初始值为零,两个线程对其交替操作,一个加1一个减1,来5轮
*
* 1.线程 操作 (方法) 资源类
* 2.判断 干活 通知
* 3.防止虚假唤醒机制
*/
public class ProdConsumer_traditionDemo {
public static void main(String [] args) {
ShareData shareData = new ShareData();
new Thread(()->{
for (int i = 1; i <= 5; i++) {
try {
shareData.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"Prod").start();
new Thread(()->{
for (int i = 1; i <= 5; i++) {
try {
shareData.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"Cons").start();
}
}
阻塞队列版
class MyResource{
private volatile boolean FLAG = true; //默认开启,生产+消费(生产好了就消费)
private AtomicInteger atomicInteger = new AtomicInteger(0);
BlockingQueue<String> blockingQueue = null;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd() throws Exception {
String data = null;
boolean returnVal;
while(FLAG) {
data = atomicInteger.incrementAndGet()+""; //i++
returnVal = blockingQueue.offer(data,2L,TimeUnit.SECONDS);
if (returnVal) {
System.out.println(Thread.currentThread().getName()+"\t 插入队列"+data+"成功");
} else {
System.out.println(Thread.currentThread().getName()+"\t 插入队列"+data+"失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName()+"\t大老板叫停了,表示FLAG=false,生产动作结束");
}
public void myConsumer() throws Exception {
String result = null;
while(FLAG) {
result = blockingQueue.poll(2L,TimeUnit.SECONDS);
if (null == result || result.equalsIgnoreCase("")) {
FLAG = false;
System.out.println(Thread.currentThread().getName()+"\t 超过2s没有收到蛋糕,消费退出");
System.out.println();
System.out.println();
return;
}
System.out.println(Thread.currentThread().getName()+"\t 消费队列"+result+"成功");
}
}
public void stop() throws Exception{
this.FLAG = false;
}
}
/**
* volatile/CAS/atomicInteger/BlockingQueue/线程交互/原子引用
*/
public class ProdComsumer_BlockingQueueDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 生产线程启动");
try {
myResource.myProd();
} catch (Exception e) {
e.printStackTrace();
}
},"Prod").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 消费线程启动");
System.out.println();
System.out.println();
try {
myResource.myConsumer();
} catch (Exception e)
{
e.printStackTrace();
}
},"Cons").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {e.printStackTrace();}
System.out.println();
System.out.println();
System.out.println();
System.out.println("5秒钟时间到,main线程叫停,活动结束");
try {myResource.stop();} catch (Exception e) {e.printStackTrace();}
}
}
线程池
中间消息件
8.线程池用过吗?对ThreadPoolExecutor的理解?
Callable接口
使用代码
线程池
为什么使用线程池,优势
主要工作
控制运行的线程的数量,处理过程中将任务放入队列, 然后在线程创建后启动这些任务,如果线程数量超过 了最大数量,超出的数量的线程排队等候,等其他线程 执行完毕,再从队列中取出任务来执行
主要特点
线程复用
控制最大并发数
管理线程
优势
1.降低消耗资源。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统 资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池如何使用?
架构说明
编码实现
了解
Executors.newScheduledThreadPoolExecutor()
java8新出
重点
Executors.newFixedThreadPool(int)
执行一个长期的任务,性能好很多
Executors.newSingleThreadPool()
一个任务一个线程执行场景
Executors.newCacheThreadPool()
适用:执行很多短期异步的小程序或者负载较轻的服务器
ThreadPoolExecutor
底层原理类
线程池的几个重要参数介绍
7大参数
1.corePoolSize:线程池中的常驻核心线程数
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数, 此值必须大于等于1
3.keepAliveTime:多余的空闲线程的存活时间。当前线程池数量 超过corePoolSize时,
当空闲时间达到keepAliveTime值时,多余空 闲线程会被销毁直到只剩下corePoolSize个线程为止。
4.unit:keepAliveTime的单位
5.workQueue,任务队列,被提交但尚未被执行的任务
6.threadFactory:表示生成线程池中工作线程的线程工厂, 用于创建线程一般默认的即可
7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的 最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
线程池的底层工作原理
9.线程池用过吗?生产上如何设置合理参数?
线程池的拒绝策略
是什么
等待队列已经排满了,再也塞不下新任务了,同时, 线程池中的max线程也达到了,无法继续为新任务 服务。 这时候就需要拒绝策略机制合理的处理这个问题
JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedExceptionHandler异常阻止系统正常执行
CalledRunsPolicy:"调用者运行"一种调节机制, 该策略既不会抛出任务,也不会抛出异常,而是将 某些任务回退到调用者,从而降低新任务的流量
DiscardOldestPolicy:抛弃队列中等待最久的任务, 然后把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy:直接丢弃任务,不予任何处理也不抛 出异常。如果允许任务丢失,这是最好的一种方案
以上内置拒绝策略均实现了 RejectedExecutionHandler接口
在工作中单一的/固定数的/可变的三种创 建线程池的方法,用的哪个多?超级大坑
答案是一个都不用,生产上只使用自定义的
Executors的JDK中已经提供了,为什么不用?
1.线程资源必须通过线程池提供,不允许 在应用中自行显式创建线程。
适用线程池的好处是减少在创建和销毁线程上所 消耗的时间以及系统资源的开销,解决资源不足的 问题。如果不适用线程池,有可能造成系统创建大 量同类线程而导致消耗完内存或者"过度切换"的问题
2.线程池不允许适用Executors创建,而是 通过ThreadPoolExecutor的方式,这样的处 理方式写的同学更加明确线程池的运行规 则,规避资源耗尽的风险
Executors返 回的线程池对 象的弊端如下
1.FixedThreadPool 和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE, 可能会堆积大量的请求,从而导致OOM
2.CachedThreadPool 和ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE, 可能会创建大量的线程,从而导致OOM
你在工作中是如何使用线程池的, 是否自定义过线程池使用
合理配置线程池你是如何考虑的?
首先查看是CPU密集型还是IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
CPU密集型
IO密集型
1
IO
10.死锁编码及定位分析?
是什么
产生死锁的原因
系统资源不足
进程推进的顺序不合适
资源分配不当
代码
解决
jps命令定位进程号
jps -l
jstack找到死锁查看
jstack 进程号
12.synchronized和lock有什么区别?lock有什么好处?
1.原始构成
Synchronized是关键字,属于JVM层面
Lock是具体类(java.util.concurrent.locks.Lock),是API层面的锁
2.使用方法
Synchronized不需要用户手动去释放锁,当Synchronized代码执行 完后系统会自动让线程释放对锁的占用
ReentrantLock需要用户手动去释放锁,若没有主动释放锁,就有可能 导致出现死锁的现象。需要lock()和unlock()方法配合try/finally 语句块来完成
3.等待是否可中断
Synchronized不可中断,除非抛出异常或者正常运行结束
ReentrantLock可中断
1.设置超时时间:tryLock(long timeout,TimeUnit unit)
2.lockInterruptibly()放代码块 中,调用interrupt()方法可中断
4.加锁是否公平
Synchronized是非公平锁
ReentrantLock两者都可以,默认公平锁,构造方法可以传入 boolean值,true为公平锁,false为非公平锁
5.锁绑定多个条件Condition
Synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们, 可以精确唤醒,而不是像synchronized要么随机唤醒 一个线程要么随机唤醒全部线程。
代码实现
1.JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots
什么是垃圾?
简单来说就是内存已经不再 被使用到的空间就是垃圾
要进行垃圾回收,如何判断 一个对象是否可以被回收?
引用计数法
枚举根节点做 可达性分析(根 搜索路径)
case
Java中可以 作为GC Roots 的对象
虚拟机栈(栈帧中的局部变量区,也叫作局部变量表)
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法区中JNI(Native方法)引用的变量
2.说说你做过的JVM调优和参数配置,请问如何盘点查看JVM系统默认值
JVM的参数类型
标配参数
-version
-help
java -showversion
X参数(了解)
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式
XX参数
Boolean类型
KV设值类型
公式
-XX:属性key=属性值value
case
jinfo举例,如何查看当前运行程序的配置
题外话(坑题)
两个经典参数:-Xms和-Xmx
如何解释?
-Xms
等价于 -XX:InitialHeapSize
-Xmx
等价于 -XX:MaxHeapSize
盘点家底查看JVM默认值
-XX:+PrintFlagsInitial
-XX:+PrintFlagsFinal
PrintFlagsFinal举例,运行Java命令的同时打印出参数
-XX:+PrintCommandLineFlags
打印命令行参数
java -XX:+PrintCommandLineFlags -version
3.平时工作用过的JVM常用基本配置参数有哪些?
基础知识复习
常用参数
-Xms
初始大小内存,默认为物理内存的1/64
等价于:-XX:InitialHeapSize
-Xmx
最大分配内存,默认为物理内存的1/4
等价于:-XX:MaxHeapSize
-Xss
设置单个线程栈的大小,一般默认为512k-1024k
等价于-XX:ThreadStackSize
-Xmn
设置年轻代的大小
-XX:MetaspaceSize
设置元空间的大小
元空间的本质和永久代类似,都是对JVM规范中方法区 的实现。不过元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
典型设置案例
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
-XX:+PrintGCDetails
输出详细GC收集日志
GC
FullGC
-XX:SurvivorRatio
设置新生代中eden和S0和S1空间的比例,默认-XX:SurvivorRatio=8,Eden:SO:S1=8:1:1, 假如-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1,SurvivorRatio值就是设置Eden区的比例 占多少,S0/S1相同。
-XX:NewRatio
配置新生代和老年代在堆结构中的占比 默认 -XX:NewRatio=2新生代占1,老年代2,年轻代占整个堆的1/3 假如 -XX:NewRatio=4新生代占1,老年代4,年轻代占整个堆的1/5 NewRatio值就是设置老年代的占比,剩下的1给新生代
-XX:MaxTenuringThreshold
设置垃圾最大年龄
5.谈谈对OOM的认识?
java.lang.StackOverflowError
java.lang.OutofMemoryError:Java heap space
java.lang.OutofMemoryError:GC overhead limit exceeded
java.lang.OutofMemoryError:Direct buffer memory
java.lang.OutofMemoryError:unable to create new native thread
非root用户登录Linux系统测试
服务器级别调参调优
java.lang.OutofMemoryError:Metaspace
使用java -XX:+PrintFlagsInitial命令 查看本机的初始化参数,-XX:Metaspace 为21810376B(大约20.8M)
6.GC垃圾回收算法和垃圾收集器的关系?分别是什么?
GC算法(引用计数/复制/标记清除/标记整理)是内 存回收的方法论,垃圾收集器就是算法落地实现。
因为目前为止还没有完美的收集器出现,更没有万能 的收集器,只是针对具体应用最适合的收集器,进行 分代收集
4种主要垃圾收集器
串行垃圾回收器(Serial)
为单线程环境设计且只使用一个线程进行垃圾回收, 会暂停所有的用户线程。所以不适合服务器环境
并行垃圾回收器(Parallel)
多个垃圾收集线程并行工作,此时用户线程是暂停的, 适用于科学计算/大数据处理后台处理等弱交互场景
并发垃圾回收器(CMS(ConcMarkSweep))
用户线程和垃圾收集线程同时执行(不一定是并行,可能是 交替执行),不需要停顿用户线程互联网公司多用它,适用 对响应时间有要求的场景(强交互)
上述3个小总结,G1特殊后面说
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的 区域然后并发的对其进行垃圾回收
7.怎么查看服务器默认的垃圾收集器是哪个? 生产上如何配置垃圾收集器的? 说说对垃圾收集器的理解?
怎么查看服务器默认的垃圾收集器是哪个?
默认的垃圾收集器有哪些?
垃圾收集器
部分参数预先说明
DefNew
Default New Generation
Tenured
Old
ParNew
Parallel New Generation
PSYoungGen
Parallel Scavenge
ParOldGen
Parallel Old Generation
Server/Client模式分别是什么意思?
新生代
串行GC(Serial)/(Serial Copying)
并行GC(ParNew)
并行回收GC(Parallel)/(Parallel Scavenge)
老年代
串行GC(Serial Old)/(Serial MSC)
并行GC(Parallel Old/Parallel MSC)
并发标记清除GC(CMS)
4步过程
优缺点
优
并发收集低停顿
缺
1.并发执行,对CPU资源压力大
2.采用的标记清除算法会导致大量内存碎片
垃圾收集器配置代码总结
底层代码
实际代码
如何选择垃圾收集器
8.G1垃圾收集器
以前收集器的特点
年轻代和老年代是各自独立且连续的内存块
年轻代收集使用单Eden+S0+S1进行复制算法
老年代收集必须扫描整个老年代
都是以尽量少而快速的执行GC为原则
G1是什么
特点
1.G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
2.G1整体上采用标记整理算法,局部是通过复制算法,不会产生内存碎片
3.宏观上看G1之中不再区分年代代和老年代。把内存划分为多个独立的 子区域(region),可以近似理解为一个围棋的棋盘。
4.G1收集器讲整个的内存区都混合在一起了,但其本身依然在小范围内要 进行年轻代和老年代的区分,保留了年轻代和老年代,但他们不再是物理 隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然 会采用不同的GC方式来处理不同的区域。
5.G1虽然也是分代收集器,但整个内存区分不存在物理上的年轻代和老年代 的区别,也不需要完全独立的survivor(to space)堆来复制准备。G1只有 逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
底层原理
Region区域化 垃圾收集器
最大好处是化整为零,避免了全内存扫描,只需要按照区域来扫描即可。
回收步骤
4步过程
case案例
常用配置参数(了解)
-XX:+UseG1GC
-XX:G1HeapRegionSize=n:设置G1区域的大小。值是2的幂,范围是1MB-32MB. 目标是根据最小的Java堆大小划分出约2048个区域。
-XX:MaxGCPauseMillis=n:最大停顿时间,这是个软目标,JVM将尽可能(但不 保证)停顿小于这个时间
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就出发GC,默认是45
-XX:ConcGCThreads=n:并发GC使用的线程数
-XX:G1ReserverPercent=n:设置作为空闲空间的预留内存百分比, 以降低目标空间溢出的风险,默认值是10%
和CMS相比的优势
1.G1垃圾收集器不会产生内存碎片
2.可以精确控制停顿.该收集器是把整个堆 (新生代、老年代)划分为多个固定大小的区 域,每次根据允许停顿的时间去收集 垃圾最多的区域。
小总结
9.生产环境服务器变慢,诊断思路和性能评估谈谈?
整机:top
top,系统性能命令的完整版
uptime,系统性能命令的精简版
CPU:vmstat
查看CPU(包含不限于)
查看额外
查看所有CPU核信息
mpstat -P ALL 2
查看进程使用CPU的用量分解信息
pidstat -u 1 -p 进程编号
vmstat -n 2 3 :每2秒采样一次,共采样3次
内存:free
应用程序可用内存数
查看额外
pidstat -p 进程号 -r 采样间隔秒数
硬盘:df
查看磁盘剩余空间数
磁盘IO:iostat
磁盘io性能评估
查看额外
pidstat -d 采样间隔秒数 -p 进程号
网络IO:ifstat
默认本地没有,下载ifstat
查看网络IO
10.假如生产环境出现CPU占用过高,谈谈分析思路和定位
结合Linux和JDK命令一起分析
案例步骤
1.先用top命令找出CPU占比最高的
2.ps -ef或者jps进一步定位,得知是一个怎样的后台程序出现问题
3.定位到具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
参数解释
-m 显示所有的线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式
4.将需要的线程ID转换为16进制格式(英文小写字母)
printf "%x\n" 有问题的线程ID
5.jstack 进程ID|grep tid(16进制线程ID小写字母) -A60
11.对于JDK自带的JVM监控和性能分析工具有哪些?你是怎么用的?
是什么
性能监控工具
jps(虚拟机进程状况工具)
jinfo(Java配置信息工具)
jmp(内存映像工具)
jstat(统计信息监控工具)
jstack(堆栈异常跟踪工具)
jvisualvm
jconsole
12.JVM GC 结合SpringBoot的微服务优化
打包优化
JVM调优
内部启动
java -jar 应用包名.jar/war
外部启动
1.使用maven clean package打包
2.在有包的路径下,运行jar命令, 公式如下: java -server jvm的各种参数 -jar 第一步上面的jar/war包名