【Java】面试常问知识点(Java基础)

JVM

java 栈:线程私有,生命周期和线程,每个方法在执行的同时都会创建一个 栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用;

本地方法栈:本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法;

Java类加载器过程,实现

类加载器执行流程

启动(Bootstrap)类加载器: 引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

扩展(Extension)类加载器: 扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器: 系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

类加载过程一共有7个阶段

加载 加载一个Class需要完成以下3件事: 1.通过Class的全限定名获取Class的二进制字节流 2.将Class的二进制内容加载到虚拟机的方法区 3.在内存中生成一个java.lang.Class对象表示这个Class 获取Class的二进制字节流这个步骤有多种方式: 从zip中读取,如:从jar、war、ear等格式的文件中读取Class文件内容 从网络中获取,如:Applet 动态生成,如:动态代理、ASM框架等都是基于此方式 由其他文件生成,典型的是从jsp文件生成相应的Class 校验 验证一个Class的二进制内容是否合法,主要包括4个阶段: 文件格式验证,确保文件格式符合Class文件格式的规范。如:验证魔数、版本号等。 元数据验证,确保Class的语义描述符合Java的Class规范。如:该Class是否有父类、是否错误继承了final类、是否一个合法的抽象类等。 字节码验证,通过分析数据流和控制流,确保程序语义符合逻辑。如:验证类型转换是合法的。 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。如:验证引用的类、成员变量、方法的是否可以被访问(IllegalAccessError),当前类是否存在相应的方法、成员等(NoSuchMethodError、NoSuchFieldError)。 准备 在准备阶段,虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值。注意这里仅仅会为static变量分配内存(static变量在方法区中),并且初始化static变量的值为其所属类型的默认值。如:int类型初始化为0,引用类型初始化为null。即使声明了这样一个static变量: public static int a = 123; 在准备阶段后,a在内存中的值仍然是0, 赋值123这个操作会在中初始化阶段执行,因此在初始化阶段产生了对应的Class对象之后a的值才是123 。 解析 解析阶段,虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类、接口、方法、成员变量等符号引用。在转换成直接引用后,会触发校验阶段的符号引用验证,验证转换之后的直接引用是否能找到对应的类、方法、成员变量等。这里也可见类加载的各个阶段在实际过程中,可能是交错执行。

初始化 初始化阶段即开始在内存中构造一个Class对象来表示该类,即执行类构造器<clinit>()的过程。需要注意下,<clinit>()不等同于创建类实例的构造方法<init>() <clinit>()方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。 虚拟机会确保先执行父类的<clinit>()方法。 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成<clinit>()方法。 虚拟机会保证<clinit>()方法的执行过程是线程安全的。

最后就是使用和卸载了

类加载器类型

  • 启动类加载器(Bootstrap),C语言编写,负责加载核心Java Class
  • 扩展类加载器(Extension),jre/lib/ext
  • 应用类加载器(App),classpath
  • 用户自定义加载器(Plugin),程序自定义,通过继承ClassLoader父类来创建自己的类加载器
  • AppCLassLoader父加载器是ExtentionClassLoader; ExtentionClassLoader父加载器为null.

双亲委派机制

jvm在加载类的时候,通常是从AppClassLoader开始,他会委托他的父加载器去加载,父加载器会继续向上委派,如果当前加载器父加载器为null,则会让引导类加载器去加载,如果上层加载器加载不到,则会反过来向下委派,让子加载器自己去加载. 双亲委派的优点: a.已经加载过的类(包名+类名相同)不会重复加载,可以防止串java自身核心类库,保证运行代码的安全性 b.自定义加载器可以加载除核心类之外的(包名+类名相同)类,如果用自定义加载器去加载核心类如String.class,虚拟机会抛出安全异常,总的来说双亲委派就会更安全.

怎么打破双亲委派机制

1.自定义类加载,重写loadclass方法 因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写他的加载规则,相当于打破双亲委派机制

2.使用线程上下文类 双亲委派模型的第二次“破坏”是由这个模型自身的缺陷所导致的,双亲委派很好的解决了各个类加载器的基础类统一问题,基础类之所以“基础”,是因为他们总被用户代码所调用,但是如果基础类又要重新调用用户代码,那咋办? 比如说JNDI是java的标准服务,它的代码是由启动类加载器进行加载的,但是jndi的作用就是进行资源的集中管理和查找,它需要调用由开发人员开发在classpath下的类代码,但是启动类加载器不会进行加载。 所以引入线程上下类加载器,通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它会从父线程继承一个,如果在应用程序全局范围内没有设置,那么这个线程上下类加载器就是应用程序类加载器。 那么这样JNDI服务使用这个线程上下类加载器去加载所需的spi代码,也就是父类加载器请求子类加载器去完成类加载的动作,这个实际是打通了双亲委派的逆向层次结构。

JVM内存分配

什么是JMM

JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存。这是 Java 内存模型定义的线程基本工作方式。

JMM定义了什么

分别是:原子性,可见性,有序性

原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。

可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。

除了volatile关键字之外,final和synchronized也能实现可见性。

synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。

final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。

在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:

volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。

synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的。

八种内存交互操作

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

一些规则:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

垃圾回收算法

  • 标记-清除算法

    执行步骤:

    • 标记:遍历整个内存区域,对需要回收的对象打上标记。
    • 清除:再次遍历内存,对标记过的内存进行回收。

    缺点:

    • 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
    • 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,虽然总的可用内存是够的,但是由于太过分散,无法找到一块连续的且满足分配要求的,因而不得不再次触发一次GC。
  • 复制算法

    将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。

    优点

    • 相对于标记–清理算法解决了内存的碎片化问题,因为复制的时候,会把存活的对象,聚拢在一起。
    • 效率更高(清理内存时,记住首尾地址,一次性抹掉)。

    缺点:

    • 内存利用率不高,每次只能使用一半内存。8G的内存,只能使用4G,这个是无法接受的。

    改进:

    将整个堆划分为两大块:新生代和老年代,分别用于存放不同特点的对象。

    新生代呢,存放生命周期短的对象及其体积小的对象。 老年代呢 ,存放生命周期长的 ,体积大的对象。 而且对于新生代和老年代采用了不同的垃圾回收算法。新生代使用复制算法:

    将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。

    首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。

    但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的。因此这里会用到老年代,进行分配担保,存不下的话将对象存储到老年代。若还不够,就会抛出OOM。另外如果一个对象在多次内存回收后,都还存活,也会进入老年代,这个次数通过 ‐XX:+MaxTenuringThreshold控制,最大值为15.(对象头中的4个bit存放)。

  • 标记整理算法

    当对象的存活率比较高时,或者对象比较大时,用前面的复制算法这样复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。

    执行步骤:

    • 标记:对需要回收的进行标记
    • 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
  • 分代收集算法

    分代收集算法其实没有什么新东西,就是上面新生代和老年代根据对象不同的特点,采用不同的算法进行回收,取名为分代收集。

    对象怎样才会进入老年代呢?

    提升:当对象足够老的时候,会晋升到老年代。怎么才能足够老呢?对象在多次垃圾回收后,依然存活,也就是多次从from->to 又从to->from 这样多次。jvm认为无需让这样的对象继续这样复制,因此将其晋升到老年代。 分配担保:默认的Survivor只占整个年轻代的10%,当从eden区复制到from / to的时候,存不下了,这个时候对象会被移动到老年代。-XX:PretenureSizeThreshold 大对象直接在老年代分配。 动态对象年龄判定:当eden区中,某一年龄的对象已经占用整个eden的一半了,那么大于或者等于这一年龄的对象都会进入老年代。

    哪些对象可以作为 GC Roots 呢

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 本地方法栈(Native 方法)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 所有被同步锁持有的对象

    怎么判断一个对象是否会被GC

    引用计数法

    给对象中添加一个引用计数器:

    • 每当有一个地方引用它,计数器就加 1;
    • 当引用失效,计数器就减 1;
    • 任何时候计数器为 0 的对象就是不可能再被使用的。

    可达性分析算法

    这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

    G1收集器和ZGC收集器原理

    有哪些可以作为GC ROOT对象

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中JNI(Native方法)引用的对象

    三色标记法

    白色:表示该对象还没有被垃圾回收器访问。垃圾回收的最初阶段,所有对象都是白色的。 黑色:表示对象以及被垃圾回收器访问了,而且该对象的所有引用都已经被扫描过。标记为黑色的对象,意味着该对象是存活的。 灰色:位于白色与黑色中间态的对象,表示该对象已经被垃圾回收器访问过,但是它的引用还没有被扫描。 整个标记过程,就是沿着 GC Roots,整个对象结构图以灰色为波峰的波纹从黑向白推进的过程。最初除了 GC Roots 以外,所有对象都是白色;标记阶段不断的产生灰色和黑色对象;最后图中只剩下白色和黑色的对象,黑色就是存活的对象,白色就是可以被回收的对象

    CMS执行流程

    1、初始标记:独占CPU(STW),仅标记GCroots能直接关联的对象,速度比较快;

    2、 并发标记:可以和用户线程并行执行,标记所有可达对象

    3、 重新标记:独占CPU(STW),对并发标记期间用户线程运行产生的垃圾对象进行标记修正(用的是增量更新)---防止对象消失

    4、 并发清理:可以和用户线程并行执行,清理垃圾

    JVM——SWT机制

    stop the world指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 有点像卡死的感觉,这个停顿称为STW。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

    G1收集器

    初始标记:先STW,并记录下gc roots直接能引用的对象,速度很快,如果不做STW,gc root会非常多。 并发标记:根据初始标记的结果,做整个的一个可达性分析,找出所有的被引用的对象,这个过程耗时比较长(大约占整个收集过程的80%左右),但是这个过程和用户线程并发执行,所以用户无感知,但是因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变(就比如在并发标记前是非垃圾,标记之后是垃圾或者并发标记前是垃圾,并发标记后变非垃圾)。 最终标记:先STW,同时修复在并发标记里面出现状态变换的对象,主要用到三色标记里的原始快照算法做重新标记。 筛选回收:会根据用户所指定的STW参数-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)来制定回收计划,判断这轮回收需要回收多少个,所以这个阶段不一定会将所有的垃圾都回收掉,它在回收之前对堆有一个区域的回收时间估算,如果回收1/2就达到了用户指定的最大停顿时间,那么就只会回收1/2,但是这1/2如何去选具体是哪一块区域,有一个筛选算法在下面详解,剩下的在下一次的垃圾回收去回收,这也就是为什么没有重置标记,其实筛选回收是可以与用户线程并发执行的,但是由于我们指定了最大停顿时间,所以,在保证时间的情况下为了提高吞吐量,我们进行了STW,回收完成之后将旧的地址转换成新的地址。(注意:其实这个阶段可以与用户线程并发执行,但是由于G1内部算法过于复杂,没有实现并发执行,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本。)

    特点:

    1、并行与并发:G1能充分利用CPU、多核的环境优势来缩短STW停顿时间,部分其他收集器需要STW的区域,G1也可以并发执行。 2、分代收集:虽然G1去掉了连续内存空间分代的概念,也不需要其他收集器配合使用,但分代收集依然存在。 3、空间整合:整体上看,G1采用了标记–整理的算法,局部看是标记–复制算法。 4、可预测停顿时间:用户可通过-XX:MaxGCPauseMillis参数设置最大停顿时间,拥有良好的用户体验,但是这个参数不可随意设置,不能设置的太小,否则每一次minor gc时间过短,收集的垃圾太少,容易触发full gc。

    适用场景:

    1、50%以上的堆被存活对象占用:当大多数对象都存活的时候,说明老年代被占用的比例也会很大,这个时候就会触发full gc,full gc是很慢的,如果我们使用G1,那么G1就会触发mixed gc,而且mixed gc的GC最大停顿时间还是可控的。 2、对象分配和晋升的速度变化非常大:说明了对象往老年代挪动的频率很频繁,一样的,可以减少full gc的发生。 3、垃圾回收时间特别长,超过1秒:可以设置停顿时间,提升用户体验。 4、8GB以上的堆内存(建议值):内存如果在8G以下,收集的垃圾不是很多,而G1的算法相对于CMS较为复杂,还很有可能效率不如CMS,但是对于大内存,STW时间比较长,所以,在可控停顿时间这里,G1比较合适。 5、停顿时间是500ms以内:停顿时间可由用户控制。

    ZGC收集器

    特点:

    支持TB级别:根据官方文档来看,在Jdk11时ZGC可支持的最大内存为4TB,在jdk13可以支持16TB。 最大停顿时间不超过10ms:之所以能控制在10ms以下,是因为它的停顿时间主要跟Root扫描有关,而跟root数量和堆的大小没有关系。 奠定未来GC特性的基础。 最坏的情况下吞吐量会下降15%。

    内存布局:

    ZGC内部是以Region的方式进行内存布局的,暂时没有设置分代,使用读屏障、颜色指针等技术来实现可并发的标记–整理算法的,且低延迟,Region分为大中小三种类型,详情如下: 小型Region(small Region):容量为2mb,用于放置小于256kb的对象。 中型Region(medium Region):容量为4mb,用于放置容量大于等于256kb但小于4mb的对象。 大型Region(large Region):容量不固定,大小可变,但必须是2的整数倍,且大小大于等于4mb的对象。 注意:由于大型Region的容量大小可变,所以,它的容量有可能小于中型Region,容量最低可达4mb,因为大型Region只放4mb以上的对象(包括4mb),且每个大型Region只存放一个大对象,所以大型对象不会被ZGC重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段),第一,大对象复制的代价比较高,第二,一个Region只会分配一个大对象,所以,当成为垃圾时直接销毁即可。

    过程:

    1、并发标记(Concurrent Mark):

    初始标记(Mark Start):先STW,并记录下gc roots直接引用的对象。
    
    并发标记(Concurrent Mark):根据初始标记的结果,基于gc roots可达性分析算法找出所有被引用的对象,在G1中使用三色标记对对象的状态做维护,ZGC使用颜色指针做标记(颜色指针详情在博客后面的颜色指针)。
    
    最终标记(Mark End):先STW,然后修复一些在并发标记过程中垃圾状态出现变化的对象。
    

    2、并发预备重分配(Concurrent Prepare for Relocate):这个阶段ZGC会根据特定的查询条件扫描一下所有的Region并得出本次收集过程中需要清理哪些Region,将它们重新组成重分配集(Relocation Set),用范围更大的扫描成本换取省去G1中记忆集的维护成本。

    3、并发重分配(Concurrent Relocate):

    初始重分配(Relocate Start):做一些并发重分配的初始化动作。
    
    并发重分配(Concurrent Relocate):这个阶段需要将并发预备重分配阶段计算出来的重分配集中的Region复制到新的Region并为每一个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系,从转发表ZGC就可以明确的知道哪些对象是否处于重分配集之中,在这个阶段时,如果有用户线程访问这个对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后根据Region的转发表找出新的地址并访问,如果有更新再更新地址上的值,并使其指向新对象(这样子只有第一次访问时会变慢,后面的就可以不通过读屏障和转发表直接访问),ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。注意:一旦一个Region中的对象全部复制完成,旧的Region就可以清理释放掉了,但是转发表不能立即释放,因为可能还有访问在使用这个转发表,因为对象的旧地址转新地址是对象在被引用之后才会进行的操作。
    

    4、并发重映射(Concurrent Remap):重映射其实就是将旧的地址转换为新的地址,由于ZGC中对象引用存在“自愈”功能,所以这个阶段其实不做也是可以的,ZGC很巧妙的将这一阶段合并到了下一次的并发标记阶段,反正他们都是要遍历所有对象的,这样也就减少了一次遍历对象的开销,一个Region的所有对象都被修改后,那么这个Region对应的转发表就会被销毁掉。

    触发机制:

    1、定时触发:默认关闭,可通过ZCollectionInterval参数配置。 2、预热触发:最多三次,在堆内存达到10%、20%、30%时触发,主要是统计GC时间,为其他GC机制使用。 3、分配速率:基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。 4、主动触发:(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。

G1收集器和ZGC收集器原理

有哪些可以作为GC ROOT对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象

三色标记法

白色:表示该对象还没有被垃圾回收器访问。垃圾回收的最初阶段,所有对象都是白色的。 黑色:表示对象以及被垃圾回收器访问了,而且该对象的所有引用都已经被扫描过。标记为黑色的对象,意味着该对象是存活的。 灰色:位于白色与黑色中间态的对象,表示该对象已经被垃圾回收器访问过,但是它的引用还没有被扫描。 整个标记过程,就是沿着 GC Roots,整个对象结构图以灰色为波峰的波纹从黑向白推进的过程。最初除了 GC Roots 以外,所有对象都是白色;标记阶段不断的产生灰色和黑色对象;最后图中只剩下白色和黑色的对象,黑色就是存活的对象,白色就是可以被回收的对象

CMS执行流程

1、初始标记:独占CPU(STW),仅标记GCroots能直接关联的对象,速度比较快;

2、 并发标记:可以和用户线程并行执行,标记所有可达对象

3、 重新标记:独占CPU(STW),对并发标记期间用户线程运行产生的垃圾对象进行标记修正(用的是增量更新)---防止对象消失

4、 并发清理:可以和用户线程并行执行,清理垃圾

JVM——SWT机制

stop the world指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应, 有点像卡死的感觉,这个停顿称为STW。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

G1收集器

初始标记:先STW,并记录下gc roots直接能引用的对象,速度很快,如果不做STW,gc root会非常多。 并发标记:根据初始标记的结果,做整个的一个可达性分析,找出所有的被引用的对象,这个过程耗时比较长(大约占整个收集过程的80%左右),但是这个过程和用户线程并发执行,所以用户无感知,但是因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变(就比如在并发标记前是非垃圾,标记之后是垃圾或者并发标记前是垃圾,并发标记后变非垃圾)。 最终标记:先STW,同时修复在并发标记里面出现状态变换的对象,主要用到三色标记里的原始快照算法做重新标记。 筛选回收:会根据用户所指定的STW参数-XX:MaxGCPauseMillis(最大垃圾回收停顿时间)来制定回收计划,判断这轮回收需要回收多少个,所以这个阶段不一定会将所有的垃圾都回收掉,它在回收之前对堆有一个区域的回收时间估算,如果回收1/2就达到了用户指定的最大停顿时间,那么就只会回收1/2,但是这1/2如何去选具体是哪一块区域,有一个筛选算法在下面详解,剩下的在下一次的垃圾回收去回收,这也就是为什么没有重置标记,其实筛选回收是可以与用户线程并发执行的,但是由于我们指定了最大停顿时间,所以,在保证时间的情况下为了提高吞吐量,我们进行了STW,回收完成之后将旧的地址转换成新的地址。(注意:其实这个阶段可以与用户线程并发执行,但是由于G1内部算法过于复杂,没有实现并发执行,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本。)

特点:

1、并行与并发:G1能充分利用CPU、多核的环境优势来缩短STW停顿时间,部分其他收集器需要STW的区域,G1也可以并发执行。 2、分代收集:虽然G1去掉了连续内存空间分代的概念,也不需要其他收集器配合使用,但分代收集依然存在。 3、空间整合:整体上看,G1采用了标记–整理的算法,局部看是标记–复制算法。 4、可预测停顿时间:用户可通过-XX:MaxGCPauseMillis参数设置最大停顿时间,拥有良好的用户体验,但是这个参数不可随意设置,不能设置的太小,否则每一次minor gc时间过短,收集的垃圾太少,容易触发full gc。

适用场景:

1、50%以上的堆被存活对象占用:当大多数对象都存活的时候,说明老年代被占用的比例也会很大,这个时候就会触发full gc,full gc是很慢的,如果我们使用G1,那么G1就会触发mixed gc,而且mixed gc的GC最大停顿时间还是可控的。 2、对象分配和晋升的速度变化非常大:说明了对象往老年代挪动的频率很频繁,一样的,可以减少full gc的发生。 3、垃圾回收时间特别长,超过1秒:可以设置停顿时间,提升用户体验。 4、8GB以上的堆内存(建议值):内存如果在8G以下,收集的垃圾不是很多,而G1的算法相对于CMS较为复杂,还很有可能效率不如CMS,但是对于大内存,STW时间比较长,所以,在可控停顿时间这里,G1比较合适。 5、停顿时间是500ms以内:停顿时间可由用户控制。

ZGC收集器

特点:

支持TB级别:根据官方文档来看,在Jdk11时ZGC可支持的最大内存为4TB,在jdk13可以支持16TB。 最大停顿时间不超过10ms:之所以能控制在10ms以下,是因为它的停顿时间主要跟Root扫描有关,而跟root数量和堆的大小没有关系。 奠定未来GC特性的基础。 最坏的情况下吞吐量会下降15%。

内存布局:

ZGC内部是以Region的方式进行内存布局的,暂时没有设置分代,使用读屏障、颜色指针等技术来实现可并发的标记–整理算法的,且低延迟,Region分为大中小三种类型,详情如下: 小型Region(small Region):容量为2mb,用于放置小于256kb的对象。 中型Region(medium Region):容量为4mb,用于放置容量大于等于256kb但小于4mb的对象。 大型Region(large Region):容量不固定,大小可变,但必须是2的整数倍,且大小大于等于4mb的对象。 注意:由于大型Region的容量大小可变,所以,它的容量有可能小于中型Region,容量最低可达4mb,因为大型Region只放4mb以上的对象(包括4mb),且每个大型Region只存放一个大对象,所以大型对象不会被ZGC重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段),第一,大对象复制的代价比较高,第二,一个Region只会分配一个大对象,所以,当成为垃圾时直接销毁即可。

过程:

1、并发标记(Concurrent Mark):

初始标记(Mark Start):先STW,并记录下gc roots直接引用的对象。

并发标记(Concurrent Mark):根据初始标记的结果,基于gc roots可达性分析算法找出所有被引用的对象,在G1中使用三色标记对对象的状态做维护,ZGC使用颜色指针做标记(颜色指针详情在博客后面的颜色指针)。

最终标记(Mark End):先STW,然后修复一些在并发标记过程中垃圾状态出现变化的对象。

2、并发预备重分配(Concurrent Prepare for Relocate):这个阶段ZGC会根据特定的查询条件扫描一下所有的Region并得出本次收集过程中需要清理哪些Region,将它们重新组成重分配集(Relocation Set),用范围更大的扫描成本换取省去G1中记忆集的维护成本。

3、并发重分配(Concurrent Relocate):

初始重分配(Relocate Start):做一些并发重分配的初始化动作。

并发重分配(Concurrent Relocate):这个阶段需要将并发预备重分配阶段计算出来的重分配集中的Region复制到新的Region并为每一个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系,从转发表ZGC就可以明确的知道哪些对象是否处于重分配集之中,在这个阶段时,如果有用户线程访问这个对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后根据Region的转发表找出新的地址并访问,如果有更新再更新地址上的值,并使其指向新对象(这样子只有第一次访问时会变慢,后面的就可以不通过读屏障和转发表直接访问),ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。注意:一旦一个Region中的对象全部复制完成,旧的Region就可以清理释放掉了,但是转发表不能立即释放,因为可能还有访问在使用这个转发表,因为对象的旧地址转新地址是对象在被引用之后才会进行的操作。

4、并发重映射(Concurrent Remap):重映射其实就是将旧的地址转换为新的地址,由于ZGC中对象引用存在“自愈”功能,所以这个阶段其实不做也是可以的,ZGC很巧妙的将这一阶段合并到了下一次的并发标记阶段,反正他们都是要遍历所有对象的,这样也就减少了一次遍历对象的开销,一个Region的所有对象都被修改后,那么这个Region对应的转发表就会被销毁掉。

触发机制:

1、定时触发:默认关闭,可通过ZCollectionInterval参数配置。 2、预热触发:最多三次,在堆内存达到10%、20%、30%时触发,主要是统计GC时间,为其他GC机制使用。 3、分配速率:基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。 4、主动触发:(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。

Java中的引用

1.强引用(StrongReference)

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

线程池的七大参数

corePoolSize 核心线程数:核心线程一直存在

maximumPoolSize 最大线程数:核心线程+临时线程

keepAliveTime 存活时间:临时线程的存活时间,规定时间没有任务则销毁

TimeUnit 时间单位:毫秒或者秒等

BlockingQueue workQueue:阻塞队列 有界队列需指定容量否则最大值为intger.maxvalue,容易造成oom(ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;)

threadFactory 线程工厂

RejectedExecutionHandler 拒绝策略:线程数达到最大后,执行的拒绝策略,系统提供四种:

abort:抛异常

discard 扔掉不抛异常

discardoldest 扔掉排队时间最久的

callerruns 调用者处理任务

外加:自定义拒绝策略:自定义策略即根据业务决定任务重试几次或等线程数减少等条件下在此进入线程池等待队列的方法

执行流程:

1、判断线程池当前是否为可以添加worker线程的状态,可以则继续下一步,不可以return false:
    A、线程池状态>shutdown,可能为stop、tidying、terminated,不能添加worker线程
    B、线程池状态==shutdown,firstTask不为空,不能添加worker线程,因为shutdown状态的线程池不接收新任务
    C、线程池状态==shutdown,firstTask==null,workQueue为空,不能添加worker线程,因为firstTask为空是为了添加一个没有任务的线程再从workQueue获取task,而workQueue为空,说明添加无任务线程已经没有意义
2、线程池当前线程数量是否超过上限(corePoolSize 或 maximumPoolSize),超过了return false,没超过则对workerCount+1,继续下一步
3、在线程池的ReentrantLock保证下,向Workers Set中添加新创建的worker实例,添加完成后解锁,并启动worker线程,如果这一切都成功了,return true,如果添加worker入Set失败或启动失败,调用addWorkerFailed()逻辑

Java基础知识

Java对象三大特性

封装、继承与多态

封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。

继承

不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 子类可以用自己的方式实现父类的方法。

多态

表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

Java数据类型

byte: 占用1个字节,取值范围-128 ~ 127

short: 占用2个字节,取值范围-2^15^ ~ 2^15^-1

int:占用4个字节,取值范围-2^31^ ~ 2^31^-1

long:占用8个字节 float:占用4个字节

double:占用8个字节

char: 占用2个字节

boolean:占用大小根据实现虚拟机不同有所差异

boolean默认为false

int和integer的区别

1、Integer是int的包装类,int则是java的一种基本数据类型

2、Integer变量必须实例化后才能使用,而int变量不需要

3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。

4、Integer的默认值是null,int的默认值是0

自动装箱与拆箱了解吗?原理是什么?

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

public、protested、private、default区别

  • default: 默认访问修饰符,在同一包内可见
  • private: 在同一类内可见,不能修饰类
  • protected : 对同一包内的类和所有子类可见,不能修饰类
  • public: 对所有类可见

构造方法有哪些特点?是否可被 override?

构造方法特点如下:

  • 名字与类名相同。
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

什么是序列化

序列化是一种将对象转换成字节序列的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造成一个相同的对象。

接口和抽象类的相同点和区别?

相同点:

都不能被实例化。 接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。 不同点:

接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。

实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,只能继承一个抽象类。

当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。

说说 List, Set, Queue, Map 四者的区别?

  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。

设计模式

六大原则

1、开闭原则(Open Close Principle)

开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

3、依赖倒转原则(Dependence Inversion Principle)

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

5、迪米特法则,又称最少知道原则(Demeter Principle)

最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

6、合成复用原则(Composite Reuse Principle)

合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

 

单例模式:某个类只能有一个实例,提供一个全局的访问点。

简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。

工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。

抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。

建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。

原型模式:通过复制现有的实例来创建新的实例。

适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。

组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。

装饰模式:动态的给对象添加新的功能。

代理模式:为其他对象提供一个代理以便控制这个对象的访问。

亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。

外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。

桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。

模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。

策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。

状态模式:允许一个对象在其对象内部状态改变时改变它的行为。

观察者模式:对象间的一对多的依赖关系。

备忘录模式:在不破坏封装的前提下,保持对象的内部状态。

中介者模式:用一个中介对象来封装一系列的对象交互。

命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。

访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。

责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。

迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。

单例模式

//懒汉 线程不安全
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

//懒汉 线程安全
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

//饿汉模式
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

代理模式

步骤 1
创建一个接口。

Image.java
public interface Image {
   void display();
}
步骤 2
创建实现接口的实体类。

RealImage.java
public class RealImage implements Image {
 
   private String fileName;
 
   public RealImage(String fileName){
      this.fileName = fileName;
      loadFromDisk(fileName);
   }
 
   @Override
   public void display() {
      System.out.println("Displaying " + fileName);
   }
 
   private void loadFromDisk(String fileName){
      System.out.println("Loading " + fileName);
   }
}
ProxyImage.java
public class ProxyImage implements Image{
 
   private RealImage realImage;
   private String fileName;
 
   public ProxyImage(String fileName){
      this.fileName = fileName;
   }
 
   @Override
   public void display() {
      if(realImage == null){
         realImage = new RealImage(fileName);
      }
      realImage.display();
   }
}
步骤 3
当被请求时,使用 ProxyImage 来获取 RealImage 类的对象。

ProxyPatternDemo.java
public class ProxyPatternDemo {
   
   public static void main(String[] args) {
      Image image = new ProxyImage("test_10mb.jpg");
 
      // 图像将从磁盘加载
      image.display(); 
      System.out.println("");
      // 图像不需要从磁盘加载
      image.display();  
   }
}

动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。

代理类在程序运行期间,创建的代理对象称之为动态代理对象。这种情况下,创建的代理对象,并不是事先在Java代码中定义好的。而是在运行期间,根据我们在动态代理对象中的“指示”,动态生成的。也就是说,你想获取哪个对象的代理,动态代理就会为你动态的生成这个对象的代理对象。动态代理可以对被代理对象的方法进行功能增强。有了动态代理的技术,那么就可以在不修改方法源码的情况下,增强被代理对象的方法的功能,在方法执行前后做任何你想做的事情。

函数是编程和非函数式编程的区别

函数式编程(Functional programming)是一种编程范式,它将计算视为数学函数的求值,避免使用可变数据和复杂的状态维护。相比命令式编程(imperative programming),函数式编程更强调函数的应用,而命令式编程更强调状态的变化和命令的执行顺序[1]。

==和equals区别

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

Java泛型简介

Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  • 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  • 由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如 List<String>,表明该List只能保存字符串类型的对象。

有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。

泛型擦拭

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个 List<String> 类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。

上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:

List<String> list1 = ...; List list2 = list1; // list2将元素当做Object处理

Java反射机制的了解

Java程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如 Person p = new Student(); ,这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。

有时,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息,而解决这个问题有以下两种做法:

  • 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。
  • 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

具体来说,通过反射机制,我们可以实现如下的操作:

  • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
  • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
  • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

应用场景

Java的反射机制在实际项目中应用广泛,常见的应用场景有:

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

过滤器和拦截器的区别

1.实现原理不同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

2.使用范围不同,

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。 拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。

3.触发时机不同,

  • 过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
  • 拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

4.拦截的请求范围不同,

  • 过滤器Filter执行了两次,拦截器Interceptor只执行了一次。
  • 这是因为过滤器几乎可以对所有进入容器的请求起作用,
  • 而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

5.拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑**。**

JUC简介

JUC是java.util.concurrent的缩写,该包参考自EDU.oswego.cs.dl.util.concurrent,是JSR 166标准规范的一个实现。JSR 166是一个关于Java并发编程的规范提案,在JDK中该规范由java.util.concurrent包实现。即JUC是Java提供的并发包,其中包含了一些并发编程用到的基础组件。

JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具,大致可以分为以下几类:

  • 原子更新

    Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环 境下,无锁的进行原子操作。在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新 数组,原子更新引用和原子更新字段。

  • 锁和条件变量

    java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。JUC 下的大多数工具类用到了Lock和Condition来实现并发。

  • 线程池

    涉及到的类比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。

  • 阻塞队列

    涉及到的类比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。

  • 并发容器

    涉及到的类比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。

  • 同步器

    剩下的是一些在并发编程中时常会用到的工具类,主要用来协助线程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。

接口和抽象类(相同点与不同点)

相同点:

都不能被实例化。 接口的实现类或抽象类的子类需实现接口或抽象类中相应的方法才能被实例化。 不同点

接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现。

实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,只能继承一个抽象类。

当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的累积。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。

Java反射机制是什么

Java反射机制是指在程序的运行过程中可以构造任意一个类的对象、获取任意一个类的成员变量和成员方法、获取任意一个对象所属的类信息、调用任意一个对象的属性和方法。反射机制使得Java具有动态获取程序信息和动态调用对象方法的能力。可以通过以下类调用反射API。

Class类:可获得类属性方法 Field类:获得类的成员变量 Method类:获取类的方法信息 Construct类:获取类的构造方法等信息

简述String/StringBuffer与StringBuilder

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

String为什么不可变长

  1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串引用可以指向池中的同一个字符串。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入数据库,以获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。
  4. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
  5. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串的原因。

重写与重载的区别

重写:子类把父类本身有的方法重新写一遍。方法名,参数列表,返回值类型都不变。对方法体进行修改或者重写

发生在父类与子类之间;方法名,参数列表,返回类型必须相同;访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private);重写方法一定不能抛出新的检查异常或者比重写方法申明更加宽泛的检查型异常

重载:重载是一个类中多态性的一种表现;重载要求同名方法的参数列表不同(参数类型,参数个数甚至参数顺序);重载的时候,返回值类型可以相同也可以不同。无法以返回类型作为重载函数的区别标准。

Java为什么能跨平台,字节码的好处

java源程序是由.java文件构成的 ,.java文件在Java编译器javac的作用下被编译成了.class文件 ,JVM即Java虚拟机为不同的操作平台提供了统一的接口,使得.class文件可以在不同的操作系统平台都可以实现解释

关键字static的作用是什么

static的主要作用有两个:

为某种特定数据类型或对象分配与创建对象个数无关的单一的存储空间。 使得某个方法或属性与类而不是对象关联在一起,即在不创建对象的情况下可通过类直接调用方法或使用类的属性。 具体而言static又可分为4种使用方式:

修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用''类.静态变量''和''对象.静态变量''的方法使用。 修饰成员方法。static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。 修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。 修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。

ArrayList和LinkedList的区别有哪些

ArrayList

优点:ArrayList实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存中连着放着)

缺点:因为地址连续,ArrayList要移动数据,所以它的插入和删除操作效率比较低

添加元素:

  • 如果add(元素),添加到数组的尾部,如果要添加的数据量很大,应该使用ensureCapacity()方法,该方法会预先设置ArrayList的大小,这样可以大大提高初始化速度
  • 如果使用add(int,0),添加到某个位置,那么可能会挪动大量数组元素,并且可能会触发扩容机制。
  1. 不指定ArrayList的初始容量,在第一次add的时候会把容量初始化为10个,这个数值是确定的;
  2. ArrayList的扩容时机为add的时候容量不足,扩容的后的大小为原来的1.5倍,扩容需要拷贝以前数组的所有元素到新数组。

arraylist的java底层实现arraylist的add方法底层实际就是一个数组如果这个数组满了就将创建新数组比旧数组大的然后复制旧数组到新数组去

Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。

LinkedList

优点:LinkedList基于链表实现的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除来说,LinkedList比较占优势。LinkedList适用于要头尾操作或指定位置的场景

缺点:因为LinkedList要移动指针,所以查询性能比较低

二者适用场景

  • 当需要对数据进行随机访问的时候,适合ArrayList
  • 当需要对数据进行多次增加删除修改时,采用LinkedList

如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用ArrayList。

当然,绝大多数业务,使用ArrayList就够了,但需要注意避免ArrayList的扩容以及非顺序的插入

在高并发的情况下,线程不安全。多个线程同时操作ArrayList,会引发不可预知的异常或错误。

hashmap底层实现原理

hashmap初始化创建长度为16的数组,扩展因子为0.75即,当数组的个数大于12时,数组扩大两倍

1、map.put(k,v)实现原理 (1)首先将k,v封装到Node对象当中(节点)。 (2)然后它的底层会调用K的hashCode()方法得出hash值。 (3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。同时,要注意的是,如果链表长度大于8的话,且数组长度大于64,链表就会转化为红黑树。 2、map.get(k)实现原理 (1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

hashtable

  • 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 初始size为11,扩容:newsize = olesize*2+1

hashmap

1.底层是数组+链表+红黑树

2.可以存储null键和null值,线程不安全。在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。 3.初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂 4.扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入 5.插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容) 6.当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀

7.先插入后扩容

8.总的来说,HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。但是单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才处理。否则,进行扩容操作。

concurrenthashmap

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

并发编程的三大特性

原子性、可见性与有序性

Java常见容器

Collection

一个独立元素的序列,这些元素都服从一条或多条规则。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)

Map

一组成对的值键对对象,允许用键来查找值。ArrayList允许我们用数字来查找值,它是将数字和对象联系在一起。而Map允许我们使用一个对象来查找某个对象,它也被称为关联数组。或者叫做字典。

List

List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上加入了大量的方法,使得可以在List中间可以插入和移除元素。下面主要介绍2种List:

ArrayList和LinkedList

Set

Set也是一个集合,但是他的特点是不可以有重复的对象,所以Set最常用的就是测试归属性,很容易的询问出某个对象是否存在Set中。并且Set是具有和Collection完全一样的接口,没有额外的功能,只是表现的行为不同。

Queue

Queue是队列,队列是典型的先进先出的容器,就是从容器的一端放入元素,从另一端取出,并且元素放入容器的顺序和取出的顺序是相同的。LinkedList提供了对Queue的实现,LinkedList向上转型为Queue。其中Queue有offer、peek、element、pool、remove等方法

offer是将元素插入队尾,返回false表示添加失败。peek和element都将在不移除的情况下返回对头,但是peek在对头为null的时候返回null,而element会抛出NoSuchElementException异常。poll和remove方法将移除并返回对头,但是poll在队列为null,而remove会抛出NoSuchElementException异常

Map

常见HashMap

创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。

通过继承Thread类来创建并启动线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。

通过实现Runnable接口来创建并启动线程的步骤如下:

  1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
  2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
  3. 调用线程对象的start()方法来启动该线程。

通过实现Callable接口来创建并启动线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

通过继承Thread类、实现Runnable接口、实现Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。

采用实现Runnable、Callable接口的方式创建多线程的优缺点:

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

采用继承Thread类的方式创建多线程的优缺点:

  • 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
  • 优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。

鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。

猜你喜欢

转载自blog.csdn.net/weixin_46601559/article/details/130457089
今日推荐