杨柳亭

杨柳亭

垃圾回收和内存分配

垃圾回收

垃圾判别算法

引用计数法

一个对象A,只要有任何一个对象引用了A ,则A 的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

  • 优点

    • 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点

    • 需要单独的字段存储计数器,增加了存储空间的开销。
    • 每次赋值伴随着计数器的增减,增加时间开销。
    • 无法处理循环依赖的问题

可达性分析

将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。

基本思路:

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

可作为"GC Roots"的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程调用的方法堆中的参数,局部变量,临时变量。
  • 方法区中类静态属性引用的对象,例如java 类中的引用类型的静态变量
  • 方法区中常量引用的对象,例如字符串常量池中的引用
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用,例如基础类型对应的Class对象,一些常驻的异常对象,还有系统类加载器
  • 所有被同步锁持有的对象。
  • 反应java虚拟机内部情况的JMXBean,JVMTI中的注册的回调,本地代码缓存等(???)

注:如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行,这点也是导致GC进行时必须“Stop The World”的一个重要原因。

垃圾回收算法

标记清除

标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

  • 效率比较低:递归与全堆对象遍历两次
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。

标记复制

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

  • 优点

    • 不会造成空间碎片。
  • 缺点

    • 需要两倍的内存空间
    • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

注:如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。

标记整理

从根节点开始标记所有被引用对象
将所有的存活对象压缩到内存的一端,按顺序排放,清理边界外所有的空间。

  • 优点

    • 消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
    • 消除了复制算法当中,内存减半的高额代价。
  • 缺点

    • 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
    • 对于老年代每次都有大量对象存活的区域来说,极为负重。
    • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
    • 移动过程中,需要全程暂停用户应用程序。即:STW

分代收集

基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

增量回收

垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

注:线程上下文切换频繁,是的垃圾回收成本上升,导致吞吐量上升

分区算法

将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。

JVM四种引用

强引用

特点:GC时,永远不会被回收
使用场景

new 对象

软引用SoftReference(obj);

特点:内存不足时(自动触发GC),会被回收
使用场景

缓存

弱引用WeakReference(obj)

特点:无论内存是否充足,只要进行GC,都会被回收
使用场景

内部对象为弱引用 WeakReference为强引用

虚引用PhantomReference<>(new Object(),new ReferenceQueue<>())

特点:如同虚设,和没有引用没什么区别
使用场景

  1. 管理堆外面的引用

首先标记出所需要回收的对象,在标记完成后,统一回收掉所有被标记的对象

将可用内存划分为两部分,每次只使用其中一块,当一块内存用完时,将还存活对象复制到另外一块上面,然后再把已使用的内存空间清理一遍

标记出所需要回收的对象,将所有需要存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存

JVM GC 流程

JVM堆的内存分布

Minor GC 新生代GC

Major GC/Full GC

  1. 开始时,对象会先分配到eden区
  2. 引用运行,越来越多对象分配在eden区域
  3. 当eden区域放不下时,就会发生minor GC(young GC),利用可达性分析标记出垃圾对象,然后将有用对象移动到survivor0区域,将标记出来的垃圾对象全部清除,此时eden区域就全部清理干净了。整个过程使用了 mark-sweep(标记整理)方法回收eden区,使用mark-copy(标记复制) 方法将可用对象移动到 survivor0区域。
  4. 随着时间推移,eden如果又满了,再次触发minor GC,同样还是先做标记,这时eden和s0区可能都有垃圾对象了,注意:这时s1(即:to)区是空的,S0区和eden区的存活对象(S0 区域满了),将直接搬到s1区。然后将eden和S0区的垃圾清理掉,这一轮minor GC后,eden和S0区就变成了空的了。
  5. 随着对象的不断分配,eden空可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,所以s0与s1的角色其实会互换,即:存活的对象,会从eden和s1区,向s0区移动。然后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的
  6. 对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,而且也会造成一定开销,降低gc效率,于是有了“代龄(age)”及“晋升”。对象在年青代的3个区(eden,s0,s1)之间,每次从1个区移到另1区,年龄+1,在young区达到一定的年龄阈值(-XX:MaxTenuringThreshold(默认15))后,将晋升到老年代。
  7. 如果老年代,最终也放满了,就会发生major GC(即Full GC),由于老年代的的对象通常会比较多,因为标记-清理-整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,

垃圾收集器

图像

  1. 两个收集器间有连线,表明它们可以搭配使用:
    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  2. 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK 14中:弃用Parallel Scavenge和SerialOld GC组合 (JEP 366)
  5. (青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363)

经典垃圾收集器

Serial 收集器/Serial Old 收集器

简单高效,占用内存小 适合客户端

image.png

ParNew 收集器/Serial Old 收集器

适合多核CPU 单核由于上下文切换,收集并不理想

image.png

Parallel Scavenge/Parallel Old收集器

吞吐量优先的垃圾收集器,有自适应调节策略 jdk8的默认垃圾收集器

image.png

CMS收集器(老年代)

CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。

对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理

image.png

  • 初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
  • 并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
  • 重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题
  • 并发清理(最耗时):清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

G1 收集器

主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量,JDK9 以后的默认收集器

图像

回收过程
  1. 年轻代GC (Young GC)

  2. 老年代并发标记过程 (Concurrent Marking)

  3. 混合回收(Mixed GC)

  4. 如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。

图像 (2)

顺时针,young gc -> young gc + concurrent mark-> Mixed GC顺序,进行垃圾回收。

特点
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

并行与并发

  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW

  • 分代收集

    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
  • 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿时间模型

    • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    • G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

注:G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

虚拟机运行时数据区域

运行时数据区域

图像

​​图像

程序计数器

通过改变这个计数器 的值来选取下一条需要执行的字节码指令

java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
每个线程都会有属于自己独立的线程计数器,各线程之间计数器不会相互影响,独立存储,这些区域成为线程私有。

Java虚拟机栈

Java方法执行的线程内存模型

每个方法被执行,Java虚拟机栈会创建一个栈帧(1),用于存储局部变量表,操作数栈,方法出口,动态链接等信息。
一个方法的执行对应着一个栈帧的入栈与出栈过程。

局部变量存储存放编译期间可知java虚拟机的基础类型(boolean,byte,char、short、int、float、dubbo、long)、对象引用(reference类型)、和returnAddress类型(指向一条字节码指令的地址)。

局部变量表的存储单位是局部变量槽(slot) 其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个 (一个变量槽的内存占用空间由虚拟机自行决定) 。

当进入一个方法后,栈帧中需要分配多大的局部变量空间已经固定(变量槽数量)
《Java虚拟机规范》中 规定如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

本地方法栈

为虚拟机使用到的 Native 方法服务 (一个 Native Method 就是一个 java 调用非 java 代码的接口)

HotSpot栈内存不允许动态扩容

1
-Xss 设置栈容量 jdk11 windows最小值 180k Linux最小值228k 否则启动时会出现提示

Java堆

被所有线程共享,在虚拟机启动时创建,用于存放对象实例。

  • java堆是垃圾收集器管理的内存区域,也被成为GC堆。G1收集器出现之前,垃圾收集器一般基于分代收集理论设计,但是之后出现了一些不采用分代设计的垃圾收集器。所以收集器不一定存在新生代,老年代,永久代,Eden区,From Survivor区,To Survivor区等。
  • 从内存角度看,所有线程共享的java堆可以划分为多个私有进程的分配缓存区(TLAB),以提升对象分配时的效率。
1
2
3
4
-Xmx 堆最大值
-Xms 堆最小值
-XX:+HeapDumpOnOutOfMemoryError 发生OOM时产生dump内存堆转储快照
-XX:HeapDumpPath=./ 设置快照文件保存位置

jvm堆的默认分配方案


老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
eden区: 8/10 的年轻代空间
survivor0 : 1/10 的年轻代空间
survivor1 : 1/10 的年轻代空间(from 区)

由于及时编译技术的进步,逃逸分析技术的日渐强大,栈上分配,标量替换等优化手段出现,对象实例也未必都全部分配在堆上。

从 jdk 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

JDK8 后方法区(永生代)移除,用元空间代替,元空间使用直接内存

gc

MinorGC触发机制
  • 当年轻代空间不足时,就会触发Minor GC。这里的年轻代满指的是Eden区满,Survivor满不会引发GC。(每次 Minor GC 会清理年轻代的内存。)
  • 因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
MajorGC触发机制
  • 指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或“Full GC”发生了。

    • 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
    • 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。

  • 如果Major GC 后,内存还不足,就报OOM了。

FullGC触发机制
  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

方法区(元空间)

主要用于存储被虚拟机加载的类型信息、常量、静态变量、及时编译器编译后的代码缓存等数据

存储内容

  • 类型信息

  • 域信息

    • 域名称,域类型,域修饰符(public, private, protected, static, final, volatile, transient的某个子集)
  • 方法信息

    • 方法名称

    • 方法的返回类型(或 void)

    • 方法参数的数量和类型(按顺序)

    • 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)

    • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小 (abstract和native方法除外)

    • 异常表(abstract和native方法除外)

      • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
  • non-final的类变量

  • 运行时常量池

在HotSpot中jdk1.8之前也被称为永久代 在jdk7后将永久代的功能转移到元空间

元空间默认初始大小20m

1
2
3
4
-XX:MaxMetaspaceSize 设置元空间最大值,默认为-1 不受限制或者说收本地内存限制
-XX:MetaspaceSize 设置元空间初始空间大小 以字节为单位,达到该值会触发垃圾收集进行类型卸载,同事收集器会对该值进行调整,如果释放大量空间则适当降低该值,如果释放很少空间,那么在不超过最大值情况下适当提高。
-MinMetaspaceFreeRatio:在垃圾收集后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致垃圾收集的频率
-MinMetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比

运行时常量池

方法区的一部分,用于存放编译期产生的各种字面量和符号引用
给基本类型变量赋值的方式就叫做字面量或者字面值

注: 字符串常量池在堆中

直接内存(非本地内存)

1
-XX:MaxDirectMemorySize 设置字节内存大小 如果不进行设置则与java堆的最大值一致

JVM

GC评估标准

吞吐量

CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

  • 吞吐量就是比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

  • 这种情况下,应用程序能容忍较高的暂停时间。因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。

  • 吞吐量优先,意味着在单位时间内,STW的时间最短:0.2 + 0.2 = 0.4

暂停时间

指一个时间段内应用程序线程暂停,让GC线程执行的状态

  • GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。

  • 暂停时间优先,意味着尽可能让单次STW的时间最短

为何永久代被元空间替代

  • 永久代有固定上限,无法进行调整,而元空间直接使用内存,受本机可用内存限制。
  • 元空间里存放的是类的元数据,加载多少类的元数据由系统的实际可用空间来控制,可以加载更多的类
  • JDK8 合并 HotSpot 和 JRockit 代码时,jrockit 不存在永久代,所以合并后没必要设置一个永生代位置

双亲委托优点和缺点

优点:

  • 避免类重复加载,确保全局唯一
  • 保护程序,防止核心api被修改

缺点:

  • 检查类是否加载的委托过程是单向的,顶层的ClassLoader无法访问底层的ClassLoader所加载的类。

双亲委派机制的破坏

线程上下文类加载器

通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

image

热部署(热替换)

服务不能中断,修改必须立即表现正在运行的系统之中

类、类的加载器、类的实例之间的引用关系

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。

一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

图像

to区域被填满了,to区中的有的对象年龄还没被复制15次,也会被移动到年老代中吗?

Minor GC会一直重复from to​的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。默认情况下,如果对象年龄达到15岁,就会移动到老年代中。

虚拟机中的对象

虚拟机类加载机制

image.png
类的生命周期
加载,验证,准备,初始化_和_卸载_这五个顺序是可以确定的,类型的加载过成必须按照这种顺序按部就班的开始, 而_解析_阶段可以在初始化阶段后再开始。(并非完成一步后再进行其他部分,通常可以交叉混合进行)
有且只有六种情况必须对类立刻进行
初始化_(加载,验证等在此之前已完成);

  • 遇到 new getstatic,putstatic或invokestatic四条字节码指令时,如果类型没有进行初始化,则会先出发其初始化阶段。生成该四种指令的典型java代码有:
    • new 一个实例对象
    • 读取或设置一个类型的静态字段(被final修饰或已在编译器把结果放入常量池的静态字段除外)
    • 调用一个类型的静态方法的时候
  • 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
  • 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类
  • 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
  • ** 当使用JDK 7新加入的动态语言支持时???**

加载

在加载阶段,Java虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入 口。

验证

  • 文件格式验证:检验文件是否符合Class文件规范,从而保证输入的字节流能够正确的解析并存储于方法区内。
    • 是否以魔数0xCAFEBABE开头。 ·主、次版本号是否在当前Java虚拟机接受范围之内。
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
    • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
    • … 等
  • 元数据验证:对字节码描述的信息进行语义分析
    • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
    • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方 法重载,例如方法参数都一致,但返回值类型却不同等)。
    • …等
  • 字节码验证:主要是通过数据流分析和控制流分析,确定程序语义是否合法、符合逻辑。
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作 栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
    • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
    • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个 数据类型,则是危险和不合法的。
    • ……
  • 符号引用验证: 发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是对类自身以外(常量池中的各种符号 引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源。
    • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
    • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
    • 符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当 前类访问。
    • …… 等

准备

正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值(值“通常情况”下是数据类型的零值)
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。

  • ConstantValue属于属性表集合中的一个属性,属性表集合中一共有21个不同属性。
  • ConstantValue属性的使用位置:字段表;含义:final关键字定义的常量值。
  • ConstantValue属性作用:通知虚拟机自动为静态变量赋值。

int x =123; static int x = 123;

  • 对虚拟机来说上面两种变量赋值的方式和时刻都有所不同。
  • 非static类型变量(实例变量)
    • 赋值是在实例构造器方法中进行的。
  • static类型变量(类变量)
    • 有两种选择:在类构造器方法或者使用ConstantValue属性。
      • 同时使用final 、static来修饰的变量(常量),并且这个变量的数据类型是基本类型或者String类型,就生成ConstantValue属性来进行初始化。
      • 没有final修饰或者并非基本类型及String类型,则选择在方法中进行初始化。

解析

将常量池内的符号引用替换为直接引用

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同, 但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规 范》的Class文件格式中。
·直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚 拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机 的内存中存在。

初始化

卸载

虚拟机中的对象

对象的创建

  1. 类加载
    1. 加载
    2. 链接
    3. 初始化
  2. 分配空间
    1. 指针碰撞:假设java内存绝对规整,被使用的和未使用的中间放一个指针作为分界点的指示器,分配内存时,仅需把指针挪动挪动一段和对象大小相同的距离即可。
    2. 空闲列表:虚拟机维护一个列表,记录那些内存块是可用的,分配时从列表中获取一块足够大的空间去分配,并更新列表。

使用Serial,ParNew等压缩整理整理过程的收集器是,系统采用指针碰撞;而是用CMS等交换算法收集器时,采用空闲列表分配空间

内存分配(指针碰撞存在并发安全)并发问题解决方案:

  • CAS+重试:
  • TLAB(本地线程分配缓存):把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在JAVA堆中预先分配小块内存,只有本地缓存用完,分配新的缓存区时才需要同步锁定。

内存分配完成,虚拟机必须将已分配的内存空间初始化零值,如果使用TLAB,初始化零值也可以提前至TLAB分配时顺便初始化零值

  1. 初始化零值
    1. 将分配的空间都初始化为零值
  2. 设置对象头
  3. 执行init方法

对象的卸载

Java虚拟机自带的类加载器所加载的类,在整个虚拟机的生命周期中,都不会被卸载。
用户自定义的类加载器是可以被卸载的。
卸载时机:

  1. 该类的所有实例对象都被回收
  2. 该类的类加载器对象已经被回收
  3. 该类对应的Class对象没有被引用,无法在任何地方通过反射获取

对象的内存布局

图像

jvm默认开启class pointer压缩 为4字节
普通对象 markword 默认八个字节 instance data 若属性则为零 padding 补足被8整除
image.png
数组对象
image.png
hotspot 虚拟机对象头markword

存储内容 标志位 状态
对象哈希码,对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁状态
指向重量级锁的指针 10 重量级锁状态
空,不需要记录信息 11 gc标志
偏向线程id、偏向时间戳、对象分代年龄 01 可偏向(偏向锁)

image.png

虚拟机类加载器

image

image

jdk8

  1. 引导类加载器
  2. 扩展类加载器
  3. 应用类加载器

jdk9

  1. 引导类加载器
  2. 平台类加载器(原扩展类加载器,因兼容性被保留)
  3. 应用类加载器

jdk9后的变化

为了保证兼容性,JDK 9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动。

  1. 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过ClassLoader的新方法getPlatformClassLoader()来获取。

JDK 9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件),其中的 Java 类库就已天然地满足了可扩展的需求,那自然无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。

  1. 平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。
    现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。

image

如果有程序直接依赖了这种继承关系,或者依赖了 URLClassLoader 类的特定方法,那代码很可能会在 JDK 9 及更高版本的 JDK 中崩溃。

  1. 在Java 9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。
  2. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是 C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。
  3. 类加载的委派关系也发生了变动。
    当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

双亲委派模式示意图

image

附加:
在 Java 模块化系统明确规定了三个类加载器负责各自加载的模块:
启动类加载器负责加载的模块
java.base java.security.sasl
java.datatransfer java.xml
java.desktop jdk.httpserver
java.instrument jdk.internal.vm.ci
java.logging jdk.management
java.management jdk.management.agent
java.management.rmi jdk.naming.rmi
java.naming jdk.net
java.prefs jdk.sctp
java.rmi jdk.unsupported

平台类加载器负责加载的模块
java.activation* jdk.accessibility
java.compiler* jdk.charsets
java.corba* jdk.crypto.cryptoki
java.scripting jdk.crypto.ec
java.se jdk.dynalink
java.se.ee jdk.incubator.httpclient
java.security.jgss jdk.internal.vm.compiler*
java.smartcardio jdk.jsobject
java.sql jdk.localedata
java.sql.rowset jdk.naming.dns
java.transaction* jdk.scripting.nashorn
java.xml.bind* jdk.security.auth
java.xml.crypto jdk.security.jgss
java.xml.ws* jdk.xml.dom
java.xml.ws.annotation* jdk.zipfs

应用程序类加载器负责加载的模块
jdk.aot jdk.jdeps
jdk.attach jdk.jdi
jdk.compiler jdk.jdwp.agent
jdk.editpad jdk.jlink
jdk.hotspot.agent jdk.jshell
jdk.internal.ed jdk.jstatd
jdk.internal.jvmstat jdk.pack
jdk.internal.le jdk.policytool
jdk.internal.opt jdk.rmic
jdk.jartool jdk.scripting.nashorn.shell
jdk.javadoc jdk.xml.bind*
jdk.jcmd jdk.xml.ws*
jdk.jconsole

Volatile

Volatile的可见性和指令重排是如何实现的.内存模型的相关概念

内存模型

计算机在执行程序过程中,每条指令都是在CPU中执行的,而在执行过程中,程序的临时数据是放在主内存上的,此时就存在一个问题,CPU执行速度很快,从内存中读取和写入数据相比于CPU执行指令的速度慢很多,因此如果任何数据操作都需要和内存交互进行,会大大降低指令执行速度,因为在CPU中引入的高速缓存。
也就是说当程序在运行过程中会先将运算的数据从主存中复制一份到CPU高速缓存中,CPU直接从高速缓存中获取数据和写入数据,运算结束后在将高速缓存中的数据刷新到内存中。这种操作在单核CPU中没有问题,但是在多核CPU中就存在问题。
例如

1
i = i + 1;

如果同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法(硬件层面上提供):
1)通过在总线加LOCK​锁的方式

​2)通过缓存一致性协议​​在早期的CPU当中,是通过在总线上加LOCK​锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK​锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK​锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

Java内存模型

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

java为确保原子性、可见性、有序性

  1. 原子性

java中对基本类型的变量的读取和赋值操作是原子性的

1
2
3
4
x = 10;         //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4

除了语句1之外都非原子性操作
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

**注 **

jvm32位 对于64位的基础类型long和double不是原子操作,而是分成两个32位操作
jvm64位 jdk8 未确定是否是分成两次操作,测试验证64位并未出现long和double非原子性问题

  1. 可见性

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  1. 有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile关键字

  1. volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。

用volatile修饰之后
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  1. volatile保证原子性吗
  2. volatile能保证有序性吗
  3. volatile的原理和实现机制

《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
参考https://www.cnblogs.com/dolphin0520/p/3920373.html

https://www.zhihu.com/question/296949412/answer/747494794

https://blog.csdn.net/wll1228/article/details/107775976

JavaWeb

Filter拦截器

如何实现拦截

实现Filter接口,重写doFilter方法。

doFilter方法有三个传入参数

  • ServletRequest:对于简单的过滤器,大多数过滤逻辑是基于这个对象的。如果处理HTTP请求,并且需要访问诸如getHeader或getCookies等在ServletRequest中无法得到的方法,就要把此对象构造成 HttpServletRequest。
  • ServletResponse:除了在两个情形下要使用它以外,通常忽略这个参数。
    • 首先,如果希望完全阻塞对相关 servlet或JSP页面的访问。可调用response.getWriter并直接发送一个响应到客户机。
    • 其次,如果希望修改相关的servlet或 JSP页面的输出,可把响应包含在一个收集所有发送到它的输出的对象中。然后,在调用serlvet或JSP页面后,过滤器可检查输出,如果合适就修改 它,之后发送到客户机。
  • FilterChain:对此对象调用doFilter以激活与servlet或JSP页面相关的下一个过滤器。如果没有另一个相关的过滤器,则对doFilter的调用激活servlet或JSP本身

JavaStream

Stream

Stream(流)是一个来自数据源的元素队列并支持聚合操作

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

Stream还有两个特征

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

生成流

  • stream() -为集合创建串行流。
  • parallelStream() − 为集合创建并行流。
  • Stream.of() 从一堆对象中创建 Stream 流

数据处理

map

用于映射每个元素到对应的结果,可对当前元素进行数据处理

flatmap

将流的每个元素, 转换为其他对象的流,入参接受一个返回对象流的函数

filter(lamda表达式)

用于通过设置的条件过滤出元素(过滤条件对应的是留下的元素)

limit

用于获取指定数量的流

sorted

用于对流进行排序,可以传入Comparator参数控制排序顺序

distinct

用于消除流中的重复元素

isPrime()

用于检测是否是质数,是留下该元素

结束操作

forEach(Consumer)

遍历迭代流中的每个元素,无返回值

forEachOrdered(Consumer)

确保按照原始流的顺序执行。

collect(Collector)

使用 Collector 收集流元素到结果集合中

collect(Supplier, BiConsumer, BiConsumer)

收集流元素到结果集合中,第一个参数用于创建一个新的结果集合,第二个参数用于将下一个元素加入到现有结果合集中,第三个参数用于将两个结果合集合并

匹配

allMatch(Predicate)

如果流的每个元素根据提供的 Predicate 都返回 true 时,最终结果返回为 true。这个操作将会在第一个 false 之后短路,也就是不会在发生 false 之后继续执行计算。

anyMatch(Predicate)

如果流中的任意一个元素根据提供的 Predicate 返回 true 时,最终结果返回为 true。这个操作将会在第一个 true 之后短路,也就是不会在发生 true 之后继续执行计算。

noneMatch(Predicate)

如果流的每个元素根据提供的 Predicate 都返回 false 时,最终结果返回为 true。这个操作将会在第一个 true 之后短路,也就是不会在发生 true 之后继续执行计算。

Base

String 类和常量池

String 对象的两种创建方式:

1
2
3
4
5
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

这两种不同的创建方法是有差别的。

第一种方式是在常量池中拿对象,如果没有则在字符串常量池中创建一个;
第二种方式是直接在堆内存空间创建一个新的对象。
注意

  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中
  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用

StringBuffer 和 StrinBuilder 区别 ,String 为何不可变

java8 中 String 类使用 final 关键字修饰字符数组来保存字符串private final char value[]
java8 之后 java9 等 String 类使用 final 关键字修饰字节数组来保存字符串private final byte[] value

StringBufferStringBuilder 继承 AbstractStringBuilder 而在AbstractStringBuilder中使用字符数组存储字符串char[] value并没有使用 final 关键字修饰

  • 从线程安全性考虑:
    String 中对象不可变,可理解为常量,线程安全.AbstractStringBuilder是公共父类,定义了一些字符串基本操作.Stringbuffer 对方法添加了同步锁或对调用的方法加了同步锁,线程安全.Stringbuilder 并没有对方法加同步锁加同步锁,所以非线程安全
  • 从性能考虑:
    每次改变 String 类型会新建一个 String 对象,让后指针指向新的 String 对象,StringBuffer 每次使用对 StringBuffer 对象本身操作,不会产生新的对象,而是改变对象的引用.而 StringBuilder 相比 StringBuffer 性能会提升一部分,但需要承担线程不安全的风险

在 Java 中定义一个不做事且没有参数的构造方法的作用

在执行子类构造方法时,若没有调用super(),则默认会调用父类的无参构造函数,若父类中没有无参构造函数,编译就会报错

接口和抽象类的区别是什么?

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
  2. 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
  3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
  4. 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
  5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
  6. 关于抽象类 JDK 1.8 以前,抽象类的方法默认访问权限为 protected JDK 1.8 时,抽象类的方法默认访问权限变为 default
  7. JDK 1.8 以前,接口中的方法必须是 public 的 JDK 1.8 时,接口中的方法可以是 public 的,也可以是 default 的

成员变量与局部变量区别

语法上:成员变量可以被public``private``static等修饰,局部变量不能被修饰.但都能被final所修饰
内存中的存储方式:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存
生命周期:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失
成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

为什么重写 equals 时必须重写 hashCode 方法

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不试同一个对象。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况(前面第 1 部分已详细介绍过):
情况 1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。
分两种情况

  • 不会创建“类对应的散列表” 例如:不会创建该类的 HashSet 集合。

在这种情况下equals()用来比较该类的两个对象是否相等。而 hashCode() 则根本没有任何作用,所以,不用理会 hashCode()。

  • 会创建“类对应的散列表” 例如,会创建该类的 HashSet 集合,且自定义对象为键值
  1. 如果两个对象相等,那么它们的 hashCode()值一定相同。这里的相等是指,通过 equals()比较两个对象时返回 true。
  2. 如果两个对象 hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。 “两个不同的键值对,哈希值相等”,这就是哈希冲突。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象),如果不重写 hashcode 可能会导致相同含义的的不同对象被(hashcode 应该相等)pass 掉.而如果只重写 hashCode 不重写 equals 方法,那么 equals 只是判断两个对象是否是同一个对象

值传递和引用传递(java 是值传递)

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

Java 中的异常处理

注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。

  • try 块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块: 用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
    在以下 4 种特殊情况下,finally 块不会被执行:
  • 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
  • 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
  • 程序所在的线程死亡。
  • 关闭 CPU。

如果 try 语句里有 return,返回的是 try 语句块中变量值。
详细执行过程如下:
a.如果有返回值,就把返回值保存到局部变量中;
b.执行 jsr 指令跳到 finally 语句里执行;
c.执行完 finally 语句后,返回之前保存在局部变量表里的值。
如果 try,finally 语句里均有 return,忽略 try 的 return,而使用 finally 的 return.

Java 中 IO 流分为几种?

按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。

java 异常处理

  • try 块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块: 用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

在以下 4 种特殊情况下,finally 块不会被执行:

  • 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
  • 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
  • 程序所在的线程死亡。
  • 关闭 CPU。
    注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值

Java修饰符

访问修饰符

public

protected

private

default

非访问修饰符

final 关键字

  • final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
  • final 修饰的方法不能被重写;
  • final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。

static 关键字

  • 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
  • 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
  • 静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。

abstract

synchronized

Object类

Object类所有方法详解

Object中含有: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait(long)、wait(long,int)、wait()、finalize()

  • registerNatives()
    • 注册 native方法
  • getClass()(1)
    • 获取类的Class 对象
  • hashCode()
  • equals()
  • clone()
  • toString()
  • wait()/ wait(long)/ wait(long,int)
    • 作用是阻塞当前线程,等待其他线程调用 notify()/notifyAll()方法将其唤醒。
    • 注意:
      • 此方法只能在当前线程获取到对象的锁监视器之后才能调用,否则会抛出 IllegalMonitorStateException异常。
      • 调用 wait方法,线程会将锁监视器进行释放;而 Thread.sleep,Thread.yield()并不会释放锁。
      • wait方法会一直阻塞,直到其他线程调用当前对象的 notify()/notifyAll()方法将其唤醒;而 wait(long)是等待给定超时时间内(单位毫秒),如果还没有调用 notify()/nofiyAll()会自动唤醒; wait(long,int)如果第二个参数大于 0并且小于 999999,则第一个参数 +1作为超时时间
  • notify()/notifyAll()(2)
    • 调用 wait方法,将锁释放并阻塞;这时另一个线程获取到了此对象锁,并调用此对象的 notify()/notifyAll()方法将之前的线程唤醒。
    • 注意:
      • 调用 notify()后,阻塞线程被唤醒,可以参与锁的竞争,但可能调用 notify()方法的线程还要继续做其他事,锁并未释放,所以我们看到的结果是,无论 notify()是在方法一开始调用,还是最后调用,阻塞线程都要等待当前线程结束才能开始。
  • finalize()
    • 此方法是在垃圾回收之前,JVM会调用此方法来清理资源。此方法可能会将对象重新置为可达状态,导致JVM无法进行垃圾回收。
    • finalize()方法具有如下4个特点:
      • 永远不要主动调用某个对象的 finalize()方法,该方法由垃圾回收机制自己调用;
      • finalize()何时被调用,是否被调用具有不确定性;
      • 当 JVM执行可恢复对象的 finalize()可能会将此对象重新变为可达状态;
      • 当 JVM执行 finalize()方法时出现异常,垃圾回收机制不会报告异常,程序继续执行。

类型转换规则

自动类型转换

  • boolean类型不能与其他类型进行类型转换
  • 不相关的对象类型不能进行转换
  • 容量大的类型转换为容量小的类型必须强制进行转换
  • 浮点数转换为整数会舍弃小数部分
  • 容量小类型可自动转换为容量大的基础类型
  • char 类型

byte ,short ,char 进行运算会先类型提升为int

引用

反射.class、class.forname() 和 getClass() 的区别

  • 相同:
    • 通过这几种方式,得到的都是java.lang.Class对象;都是类加载的产物
    • 三种情况在生成 Class 对象的时候都会先判断内存中是否已经加载此类。
  • 不同:
    • 出现的时期不同:
      • Class.forname()在运行时加载;
      • Class.class和对象名.getClass()是在编译时加载
    • class.forname() 会装入类并做类的静态初始化
    • Class c = C.class;JVM将使用类C的类装载器将类C加载到内存中(前提是类C还未加载进内存),不进行类的初始化工作,返回C的Class对象
    • Class c = c.getClass()会对类进行静态初始化、非静态初始化,返回引用运行时真正所指的对象(因为子对象的引用可能会赋给父对象的引用变量中)所属的类的 Class 对象

为什么 wait()/notify()方法要放到 Object中呢

因为每个对象都可以成为锁监视器对象,所以放到 Object中,可以直接使用。

Queue

Queue ​image

注意:不要把 null 添加到队列中,否则 poll()方法返回 null 时,很难确定是取到了 null 元素还是队列为空。

PriorityQueue(优先队列) extends AbstractQueue

通过二叉小顶堆实现,可以用一棵完全二叉树表示
优先队列的作用是能保证每次取出的元素都是队列中权值最小的
PriorityQueue 实现了 Queue 接口,不允许放入 null 元素;其通过堆实现,具体说是通过完全二叉树 (complete binary tree) 实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为 PriorityQueue 的底层实现
放入 PriorityQueue 的元素,必须实现 Comparable 接口,PriorityQueue 会根据元素的排序顺序决定出队的优先级

Deque extends Queue(接口)

队列和 Deque 方法的比较

队列方法 等效的 Deque 方法
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

ArrayDeque implements Deque (实现接口)

ArrayBlockingQueue

SynchronousQueue

PriorityBlockingQueue

DelayQueue

0%