醉后不知天在水,满船清梦压星河

Java基础

Java语言基础

Java修饰符

Java语言修饰符分为访问修饰符非访问修饰符

  • 访问修饰符 —— 保护类、变量、方法和构造方法的访问

    访问修饰符一共有四种:public private protected default

    1. default

      默认修饰符,在同一包内可见

      使用对象:类、接口、变量、方法

    2. public

      对所有类可见

      使用对象:类、接口、变量、方法

    3. private

      在同一类中可见

      使用对象:变量、方法

      注意:不能修饰外部类

    4. protected

      对同一包的类和所有子类可见

      使用对象:变量、方法

      注意:不能修饰外部类

      当子类与基类在同一包中的时候:被声明为protected的变量、方法和构造器能被同一个包中任何其他类访问到

      当子类与基类不在同一个包中的时候:在子类中,子类实例可以访问其从基类继承到的protected方法,而不能访问基类实例的protected方法

      如何去理解:子类与基类不再同一个包中的时候,protected的表现?

      package p1;
      public class Father1 {
          protected void f() {}    // 父类Father1中的protected方法
      }
      
      package p1;
      public class Son1 extends Father1 {}
      
      package p11;
      public class Son11 extends Father1{}
      
      package p1;
      public class Test1 {
          public static void main(String[] args) {
              Son1 son1 = new Son1();
              son1.f(); // Compile OK     ----(1)
              son1.clone(); // Compile Error     ----(2)
      
              Son11 son = new Son11();    
              son11.f(); // Compile OK     ----(3)
              son11.clone(); // Compile Error     ----(4)
          }
      }

      首先在上述代码中:Father1是基类,具有两个子类Son1与Father1同包,Son11与Father1不同包;其次Test与Father1同包。对于son1.f()son11.f()来说,实际调用的方法时在Father1中的f()方法,而Father1中的f()方法是对同包及其子类可见,所以作为同包下的类Test是可见——(1)、(3)处编译成功;对于son1.clone()来讲其实际调用的方法是java.lang.Object.clone(),其对java.lang.Object包下的类及其子类可见,而Test不属于java.lang包,其次也不是Father1的子类,所以son1().clone()对于Test类来说是不可见,而对于Son1来说是可见的,因为Son1是Father1的子类。

    访问修饰符一览表

    修饰符 当前类 同一包类 子孙类(同一包) 子孙类(不同一包) 其他包
    public Y Y Y Y Y
    protected Y Y Y Y N
    default Y Y Y N N
    private Y N N N N
  • 非访问修饰符 —— 实现一些其他的功能

    1. static

      用来修饰类方法和类变量

    2. final

      用来修饰类、方法和变量

      final修饰的类不可以被继承;修饰的方法不能被继承类重新定义(不能被重写);修饰的变量为常量不可更改

    3. abstract

      用来创建抽象类和抽象方法

    4. synchronized和volatile

      主要用于线程的编程

Java运算符

自增自减运算符

  • ++i/--i —— 先进行自增/自减,再进行表达式运算
  • i++/i-- —— 先进行表达式运算,再进行自增/自减

位运算符

位运算符作用在所有的位上,并且按位运算。

假如A=0011 1100,B=0000 1101

操作符 描述 例子
& 如果对应位都是1,则结果为1,否则为0 A&B得到12,即0011 1101
| 如果相应对应为都是0,则结果为0,否则为1 `A
^ 如果对应为值的值相同,则结果为0,否则为1 A^B得到为49,即0011 0001
~ 按位取反,运算符翻转操作数的每一位,即0=>1,1=>0 ~A得到为-61,即1100 0011
<< 按位左移运算。左操作数按位左移右操作数指定的位数 A<<2得到为240,即1111 0000
>> 按位右移运算。左操作数按位右移操右操作数指定的位数 A>>2得到为15,即1111
>>> 按位右移补零操作运算符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充

instanceof运算符

用于操作对象实例,检查该对象是否是一个特定类型(类类型或者接口类型

     String name="James";
        boolean result=name instanceof String;

面向对象的特性

封装的特性

封装是指一种将抽象性函数接口的实现细节部分包装、隐藏起来的方法。

封装的好处:

  1. 防止该类的代码和数据被外部类定义的代码随机访问,要访问该类的代码必须通过严格的接口控制
  2. 我们可以修改自己实现的代码,而不用修改那些调用我们代码的程序片段
  3. 良好的封装能够减少耦合
  4. 类内部结构可以自由更改
  5. 可以对成员变量进行更精确的控制
  6. 隐藏信息、实现细节

封装的步骤:

  1. 修改属性的可见性来限制对属性的访问(一般限制为private

    public class Person{
        private String name;
        private int age;
    }
  2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋值的方法,用于对私有属性的赋值。(getter和setter方法

多态的特性

多态是同一个行为具有多个不同表现形式或形态的能力。

多态就是同一个接口,使用不同的实例而执行不同的操作,多态是对象多种表象形式的体现。多态的优点:

  1. 消除类型之间的耦合关系
  2. 可替换性
  3. 可扩充性
  4. 接口性
  5. 灵活性
  6. 简化性

多态存在的三个必要条件:

  1. 继承
  2. 重写
  3. 父类引用指向子类对象

多态的实现方式:

  1. 重写
  2. 接口

继承的特性

继承是Java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征(属性)和行为 (方法)。

继承中需要注意的是:

  1. 子类用有父类非private的属性和方法
  2. 子类可以用有自己的属性和方法
  3. 子类可以用自己的方式去实现父类的方法(实现抽象父类中的方法亦或是对具体类中的方法进行重写
  4. java中的继承是单继承
  5. 父类中声明为public的方法在子类中也必须为public
  6. 父类中声明为protected的方法在子类中要么声明为protected,要么声明为public,不可以声明为private

this关键字与super关键字:

this —— 指向当前自身对象

super —— 调用父类中的方法

重写与重载

  • 重写 —— Override重写值子类对父类允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变

    重写的好处:子类可以根据需要实现父类的方法

    重写需要注意的:

    1. 重写方法不能抛出新的检查异常或者比较被重写方法申明更宽泛的异常
    2. 参数列表必须与被重载方法完全相同
    3. 返回类型与被重写方法类型可以不相同,但是必须是父类返回值的派生类(就是返回值类型的子类

Java中的类和对象

类可以看成构建对象的模板,一个类可以包含以下类型的变量:

  1. 局部变量 —— 在方法、构造方法或者语句块中定义的变量称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动被销毁。
  2. 成员变量 —— 成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问到
  3. 类变量 —— 类变量也声明在类中,方法体之外,但必须声明为static类型
  4. 构造方法 —— 每个类都有构造方法。如果没有显式地为类定义构造方法,Java编译器会为该类提供一个默认的构造方法。(构造方法没有返回值,与类同名,且一个类可以拥有多个构造方法

源文件声明规则:

当在一个源文件中定义多个类(公共类、非公共、内部类、匿名类等等),并且还有importpackage语句的时候,需要特别注意这些规则:

  1. 一个源文件只能有一个public
  2. 一个源文件可以有多个非public
  3. 源文件的名称应该为public类类名保持一致
  4. 如果一个类定义在某个包中,那么package语句一定在源文件的首行
  5. 如果源文件包含import语句,那么应该放在package语句和类定义之间;如果没有package语句,那么import语句应该在源文件的最前面
  6. import语句和package语句对源文件中定义的所有的类都有效。在同一个源文件中,不能给不同的类不同的包声明。

Java中的特殊类

在Java中存在两种特殊的类:接口、抽象类

抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类都是抽象类。

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通一样。抽象类不能实例化对象,所以抽象类必须被继承才能被使用。在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

抽象类的特点:

  1. 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
  2. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能
  3. 构造方法、类方法不能声明为抽象方法
  4. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类

接口

接口是一个抽象类型,是抽象方法的集合。一个类通过集成接口的方式,从而来继承接口的抽象方法。接口并不是类,编写接口的方式和类很相似,但是属于不同的概念。接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的方法,否则就必须声明为抽象类。

接口的特点:

  1. 接口没有构造方法
  2. 接口中所有的方法必须是抽象方法
  3. 接口不能包含成员变量,除了static和final变量
  4. 接口中的每一个方法会被隐式的指定为public abstract
  5. 接口中的变量会被隐式指定为public static final

Java核心API

Number&Math类

Java的Math类中包含了用于执行基本数据运算的属性和方法。

序号 函数名 含义
1 xxxValue() 将Number对象转换为xxx数据类型的值并返回
2 compareTo() 将Number对象与比较,前者小于后者返回-1,前者大于后者返回1,前者等于后者返回0
3 abs() 返回参数化的绝对值
4 ceil() 返回大于等于给定参数的最小整数,类型为双精度浮点型
5 rint() 返回与参数最接近的整数,返回类型为double
6 round() 四舍五入,即将原来的数字加上0.5后向下取整
7 min() 返回两个参数中的最小值
8 max() 返回两个参数中的最大值
9 exp() 返回自然数底数e的参数次方
10 log() 返回参数的自然数底数的对数值
11 pow() 返回第一个参数的第二参数的次方
12 sqrt() 求参数的算术平方根
13 sin() 求指定double类型参数的正弦值
14 cos() 求指定double类型参数的余弦值
15 tan() 求指定double类型参数的正切值
16 asin() 求指定double类型参数的反正弦值
17 acos() 求指定double类型参数的反余弦值
18 atan() 求指定double类型参数的反正切值
19 atan2() 将笛卡尔坐标转换为极坐标,并返回极坐标的角度值
20 toDegrees() 将参数转化为角度
21 toRadians() 将角度转化为弧度
22 random() 返回一个随机数

Character 类

序号 函数 作用
1 isLetter() 是否是一个字母
2 isDigit() 是否是一个数字字符
3 isWhitespace() 是否是一个空白字符
4 isUpperCase() 是否是一个大写字母
5 isLowerCase() 是否是一个小写字母
6 toUpperCase() 指定字母的大写形式
7 toLowerCase() 指定字母的小写形式
8 toString() 返回字符的字符串形式

Java IO操作类

读取控制台输入

使用System.in来读取控制台的输入。

BufferedReader reader=new BufferedReader(new InputStreamReader(System.in));
        String result=null;
        try {
            result=reader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(result+"!");
  1. read() —— 读取单个字符
  2. readLine()—— 读取一整行的内容,常用来读取字符串

文件读写

文件读写分为字节流和字符流

  • 字节流 —— 字节流处理的基本单位为单个字节,通常用来处理二进制数据。Java中最基本的两个字节流是InputStreamOutputStream,分别代表了最基本的输入字节流和输出字节流。
  • 字符流 —— 字符流处理的最基本单位是Unicode码元(大小2个字节),它通常用来处理文本数据。

字节流处理文件

    //字节流读入数据
    File file=new File("E:\\Test\\test.txt");
        InputStream in=new FileInputStream(file);
        byte[] b=new byte[(int) file.length()];
        in.read(b);
        in.close();
        System.out.println(new String(b));
    //字符流写入数据
    File file=new File("E:\\Test\\test.txt");
        OutputStream out=new FileOutputStream(file);
        String s="Hello World~,我是xxx";
        out.write(s.getBytes());
        out.close();

字符流处理文件

    //字符流读取文件
    File file=new File("E:\\Test\\test.txt");
        Reader reader=new FileReader(file);
        char[] c=new char[1024];
        int len=reader.read(c);
        System.out.println(new String(c,0,len));
    //字符流写入文件
    File file=new File("E:\\Test\\test.txt");
        Writer writer=new FileWriter(file);
        String s="输入的字符流!aaaa";
        writer.write(s);
        writer.close();

文件对象常用API

序号 函数 作用
1 mkdir() 创建一个文件夹,成功返回true,失败返回false。失败表示File对象指定的路径已经存在,或者由于整个路径还不存在,该文件夹不能被创建
2 mkdirs() 创建一个文件夹和它所有的父文件夹
3 isDirectory() 判断File对象是否是目录
4 list() 当File对象为目录的时候,返回目录中的文件名构成的字符串数组
5 delete() 删除目录,需要注意的是要删除的FIle对象必须是目录,必须保证该目录下没有其他文件才能正确删除,否则将删除失败

Java中的异常

异常主要分为三种类型:

  1. 检查性异常

    在正常系统操作期间可能发生的预期问题。例如,打开一个不存在的文件,通常使用throw关键字来处理这种异常

  2. 运行时异常

    运行时异常可能被程序员避免的异常,与检查性异常相反,运行时异常可以在编译时被忽略

  3. 错误

    错误不是异常,而是脱离程序员控制的问题。例如,当栈溢出时,一个错误就放生了。

常用检查性异常

异常 描述
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常
CloneNotSupportedException 当调用Object类中的clone()方法克隆对象,但该对象的类无法实现Cloneable接口时,抛出该异常
IllegalAccessException 拒绝访问一个类的时候,抛出该异常
InstantiationException 当试图使用Class类中的newInstance方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化,抛出该异常
InterruptException 一个线程被另一个线程中断时,抛出该异常
NotSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

常用的运行时异常

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出该异常
ArrayIndexOutOfBoundsException 当使用非法索引访问数组时,抛出该异常
ArrayStoreException 当将错误类型的对象存储到一个对象数组时,抛出该异常
ClassCastException 当视图将对象强制转换为不是实例的子类时,抛出该异常
IllegalArgumentException 向方法传递了一个不合格或不正确的参数,抛出该异常
IllegalMonitorStateException 抛出异常表明某一线程已经试图等待对象的监视器,或者试图通过其他正在等待对象的监视器而本身没有指定监视器的线程
IllegalStateException 在非法或不适当的时间调用方法时产生的信号
IllegalThreadStateException 线程处于请求操作所要求的适当状态时抛出异常
IndexOutOfBoundsException 指示某排序索引超出范围时抛出
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常
NullPointerException 当应用程序试图在需要对象的地方使用null时,抛出该异常
NumberFormatException 试图将字符串转换成一种数值类型,但该字符不能转换为适当格式时,抛出该异常
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯
StringIndexOutOfBoundsException 此异常由String方法抛出,指示索引为负或者超出字符串的大小
UnsupportedOperationException 当不支持请求的操作时,抛出该异常

JVM

JVM是可运行Java代码的假想计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域。

Java代码的执行顺序:

  1. Java源文件=> 编译器 => 字节码文件
  2. 字节码文件 => JVM => 机器码

JVM的内存区域

g21s0K.jpg](https://imgtu.com/i/gaGWQJ)

JVM内存区域主要分为:

  1. 线程私有区域 —— 程序计数器,虚拟机栈,本地方法栈。该区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁。
  2. 线程共享区域 —— Java堆、方法区。该区域生命周期与虚拟机的开启/关闭而创建/销毁
  3. 直接内存

JVM内存各个区域的作用:

  1. 程序计数器 —— 当前线程所执行的字节码指令的地址,每条线程都要有一个独立的程序计数器
  2. 虚拟机栈 —— 描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程
  3. 本地方法栈 —— 为native关键字的方法服务
  4. —— 创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集最重要的内存区域
  5. 方法区 —— 用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

JVM运行时内存

Java堆从GC角度分为:新生代和老年代

grMqm9.jpg

新生代

用来存放新生对象。一般占据堆的1/3空间,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代分为Eden区、ServiviorFrom区、ServivorYo区。

  1. Eden区 —— Java新生对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代),当Eden区内存不够的时候,就会触发MinorGC,对新生代进行一次垃圾回收
  2. ServivorFrom —— 上一次GC的幸存者,作为这一次GC的扫描者
  3. ServivorTo —— 保留了一次MinorGC过程中的幸存者

MinorGC算法

采用复制算法

  1. Eden、ServivorFrom复制到ServivorTo,年龄加1

    首先,把Eden和ServivorFrom区域中存活的对象复制到ServivorTo区域,(如果有对象的年龄已经达到了老年代的标准,则复制到老年代区),同时将这些对象的年龄加1(如果ServivorTo不够位置就放到老年区

  2. 清空Eden、ServivorFrom

    然后清空Eden、ServivorFrom中的对象

  3. ServivorTo和ServivorFrom互换

    最后ServivorTo和ServivorFrom互换,原ServivorTo成为下一次GC时的ServivorFrom区

老年代

主要存放应用程序中生命周期长的内存对象。在进行MajorGC的时候,会先触发一次MinorGC,让新生代中的对象晋升到老年代,导致老年代空间不够用时触发MajorGC。当无法找到足够大的连续空间给新创建的较大对象时也会提前触发一次MajorGC。

MajorGC算法

采用标记清除算法

首先扫描一次所有老年代,标记处存活的对象,然后回收没有标记的对象。MajorGC会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记处来方便下次直接进行分配,当老年代也满了装不下的时候,就会抛出OOM(内存溢出 Out of Memory)异常。

永久代

内存的永久保存区域,主要存放Class和Meta(元数据)的信息。因为GC不会在主程序运行期对永久代进行清理,随着类加载的增多也会触发OOM异常。

在Java8中永久代的概念被元空间取代,元空间与永久代的区别在于:存储的地方从虚拟机内存变为本地内存,从元空间的大小只与本地内存有关。类的元数据放入本地内存,字符串和类的静态变量放入Java堆中。

垃圾回收机制

在Java中GC需要做的事情有三个:

  1. 那些内存需要回收——确定那些对象是垃圾
  2. 什么时候回收——在什么时候触发GC
  3. 怎么回收——采用什么垃圾回收算法

确定垃圾对象

  • 引用计数法

    在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器值就加一;当引用是失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的,即判定为一个可回收的对象。

  • 可达性分析

    通过GC Roots对象作为起点,如果GC Roots和一个对象之间没有可达路径,则称该对象是不可达。不可达并不代表可回收,不可达对象在经过两次标记之后才可以成为可回收对象。在进行第一次可达性分析的时,会将不可达的对象标记。此时会对不可达的对象进行一次筛选,筛选的条件就是不可达对象有没有实现finialize()方法,假如它有必须执行的finialize()方法的时候,它会被放入到一个F-Queue的队列中等待执行finalize()方法,如果在finalize()中它没有与引用链上的任何一个对象建立引用,就会被第二次标记,此时不可达对象才会变成可回收对象。

垃圾回收算法

  • 标记清除算法——Mark Sweep

    grMjFx.jpg

    分为标注和清理两个阶段,标记阶段标记出所有需要回收的对象,清除阶段回收所有被标记的对象所占用的空间

    该算法清除之后会产生大量的空间碎片,会导致后续无法找到连续的空间分配给大对象。

  • 复制算法——Copying

    为了解决标记清除算法产生内存碎片问题提出的算法。将内存空间分为两个等量的空间,每次只使用其中一个,当这一块的内存满了后将存活的对象复制到另外一块内存中,把已使用的内存清理掉。

    grQqgS.jpg

    该算法实现简单,内存效率高,不易产生碎片。但是可用空间压缩为原本的一半,且存活对象较多,复制起来较为耗时,效率就会降低。

  • 标记整理算法——Mark Compact

    结合标记清除算法与复制算法,标记阶段标记出所有可回收对象,标记后不是清理对象,而是将存活的对象移向内存的另一端,然后清除端边界外的对象

    grlg5q.jpg

  • 分代收集算法

    将GC堆划分为老年代和新生代,根据老年代和新生代的不同特点来使用不同的GC算法。

    1. 新生代与复制算法

      新生代的特点是每次垃圾回收都有大量垃圾需要回收,所以复制算法中需要复制的对象就较少。通常将新生代划分为 Eden区和Servivor区,Survivor区分为两个等量区域,ServivorTo和ServivorFrom。每次只使用Eden区和Servivior区中的一个,当进行会收时会将该两块空间中的存活的对象复制到Servivor未使用的另外一个区域中。

    2. 老年代与标记算法

      老年代的特点是每次只回收少量对象,采用标记-清除算法。如果新生代的内存无法存储某个对象的时候,会将该对象直接存储到老年代。当新生代的对象在Survivor区躲过一次GC后,其年龄就会加1。默认情况下年龄达到15的对象会被移到老年代中。

Java的四种引用类型:

  1. 强引用 —— 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,即使该对象以后永远不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
  2. 软引用 —— 利用SoftReference来来实现。对于只有软引用的对象来说,当系统内存足够的时,它不会被回收;当系统内存空间不足时,它会被回收。软引用通常在对内存敏感的程序中。
  3. 弱引用 —— WeakReference类来实现,它比软引用生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM内存空间是否足够,总会回收该对象
  4. 虚引用 —— 利用PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的作用主要是跟踪对象被垃圾回收的状态。

垃圾收集器

垃圾收集器 描述 优点 缺点
Serial垃圾收集器 单线程、复制算法。在进行垃圾收集的同时,必须暂停其它所有工作的线程,直到垃圾收集结束,是Java虚拟机运行在Client模式下默认的新生代垃圾收集器 简单高效,可以获得最高的单线程垃圾收集效率 阻塞线程
ParNew垃圾收集器 Serial垃圾收集器的多线程版本,复制算法。ParNew是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器 简单高效 阻塞线程
Parallel Scavenge垃圾收集器 多线程复制算法,重点关注程序达到可控制的吞吐量,主要使用与在后台运算而不需要太多交互的任务 高效利用CPU时间
Serial Old垃圾收集器 单线程标记整理算法,是Serial垃圾收集器的老年代版本,是运行在Client模式下默认的老年代收集器
Parrallel Old收集器 多线程标记整理算法 Parrallel Old在老年代同样提供吞吐量优先
CMS垃圾收集器 多线程标记清除算法,是一种老年代垃圾收集器。
主要目标是获取最短垃圾回收停顿时间。
分为四个阶段:
1. 初始标记—— 标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有工作线程
2. 并发标记—— 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
3.重新标记 —— 修正在并发标记阶段,因用户程序运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程
4.并发清除——清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程
总体上看CMS收集器的内存回收和用户线程是一起并发执行的
G1垃圾收集器 基于标记整理算法,可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收 区域划分和优先级区域回收机制,确保G1垃圾收集器可以有在有限时间内获得最高垃圾收集效率

Java IO模型

为了避免用户进程直接操作内核,保证内核安全,操作系统将内存寻址空间划分为两部分:内核空间(供内核程序使用)和用户空间(供用户进程使用)。内核空间和用户空间是隔离的,即使用户的程序崩溃了,内核也不受影响。

gsMRER.jpg

  • 阻塞IO模型

    在读写数据过程中会发生阻塞现象。当用户线程发出IO请求之后,内核会去查看数据时候就绪,如果没有就绪就会等待数据就绪,而用户线程处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。

  • 非阻塞IO模型

    当用户线程发起一个read操作后,并不需要等待,而是马上得到一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它就马上将数据拷贝到用户线程,然后返回。因为非阻塞I/O模型需要不断去轮询,这样会导致CPU的占用率很高。

  • 多路复用IO模型

    Java NIO实际上就是多路复用IO模型。在多路复用IO模型中,会有一个线程不断去轮询多个Socket的状态,只有当Socket真正有读写事件时,才真正调用实际的IO读写操作。在Java NIO中通过select()阻塞轮询是否发生IO事件,如果没有就一直阻塞,因此该方式会导致用户线程的阻塞。多路复用IO模型,通过一个线程就可以管理多个Socket,只有当socket出现真正的读写事件时,才会占用资源进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。

    多路复用IO模型是通过轮询的方式检测是否有事件到达,并且对到达的时间逐一进行响应,假如某一个事件的响应的非常久,就会导致后续的响应阻塞在这里,并且会影响新的轮询。

    多路复用IO模型与非阻塞IO模型都会轮询,但是它们之间是有区别的:多路复用IO模型的轮询是在内核完成的,而非阻塞IO模型是用户线程完成的,所以多路复用IO模型轮询的速度会比非阻塞IO模型快得多

  • 信号驱动IO模型

    当用户线程发起一个IO请求操作,会给对应的Socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO操作

  • 异步IO模型

    当用户线程发起read操作之后,立刻就可以开始去做其他的事情。而另一方面,从内核的角度来看,它收到一个Asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。(异步IO是需要操作系统的底层支持,Java 7中,提供了Asynchronous IO

Java NIO

NIO三大核心概念:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从Channel读取到Buffer,或者从Buffer写入到Channel。Selector用于监听多个通道事件。

Java类加载机制

JVM类加载机制分为五个部分:加载 => 验证 => 准备 => 解析 => 初始化

  1. 加载 —— 内存中会生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据入口
  2. 验证 —— 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求
  3. 准备 —— 在方法区中分配这些变量所使用的内存空间
  4. 解析 —— 虚拟机将常量池中的符号引用替换为直接引用的过程
  5. 初始化 —— 开始真正执行类中定义的Java程序代码

初始化节点是执行类的<client>方的过程,该方法是由编译器自动收集类中类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证在子<client>方法执行前父类的<client>方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器就不为这个类生成<client>方法

类加载中不会执行初始化阶段的情况:

  1. 通过子类引用父类静态字段,只会触发父类的初始化,而不会触发子类的初始化
  2. 定义对象数组,不会触发该类的初始化
  3. 常量在编译期间会存入调用类的常量池,本质上并没有直接引用定义常量的类,不会初始化定义常量所在的类
  4. 通过类名获取Class对象,不会触发类的初始化
  5. 通过Class.forName加载指定的类时,如果指定参数initailize为false时,也不会触发类的初始化
  6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作

Java集合框架

接口继承关系和实现

Java所有的集合类都在java.util包中,主要有三种:Set(集合)、List(列表)、Map(映射)

在Java集合框架中有三个非常重要的接口:

  1. Collection :是Set、List、Queue最基本的接口
  2. Iterator:迭代器,可以通过迭代器遍历集合中的数据
  3. Map:映射表的基础接口

List

List是有序的集合,Java List一共有三个实现类:

  • ArrayList

    最常用的List实现类,内部通过数组实现,它允许对元素进行快速随机访问。因为是数组实现的链表,所以在执行插入和删除的时候效率会比较低,因此ArrayList适合随机查找和遍历,不适合插入与删除

  • Vector

    通过数组实现,支持线程同步,即某一时刻只有一个线程能够写Vector,避免多线程引起的不一致,但是同步花费的时间会比较大,因为访问起来会比ArrayList慢

  • LinkedList

    链表结构存储数据结构,适合数据的动态插入和删除,随机访问和遍历速度比较慢

Set

无序且不重复的集合,判断是否是重复对象是根据对象的HashCode值(在Java中依据对象的内存地址计算出此序号),如果想要两个不同的喜爱那个视为相等的,就必须覆盖Object的hashCodeequals方法

  • HashSet

    哈希表存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序而是按照哈希值来存的,所以数据也是按照哈希值取的

    HashSet首先会判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法,如果equals结果为true,Hashset就视为同一个元素。如果equals为false就不是同一个元素。

    重写equals和hashCode让Set中可存放重复的元素

    \\新建一个类,然后在该类中重写equals和hashCode方法
      public class User {
    
        private String name;
        private Integer age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        @Override
        public int hashCode() {
            int result = 17;
    //        result = result * 31 + (name == null? 0 : name.hashCode());
            result = result * 31 + this.age;
            return result;
        }
    
        @Override
        public boolean equals(Object obj) {
            if(obj instanceof User) {
                return this.age == ((User) obj).getAge();
            }
            return false;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    \\利用HashSet来测试这个类
        public static void main(String[] args) throws IOException {
            HashSet<User> users=new HashSet<>();
            User user=new User();
            user.setAge(22);
            user.setName("aa");
            User user1=new User();
            user1.setAge(22);
            user1.setName("bb");
            users.add(user);
            System.out.println(users.contains(user1));//将会打印出true
        }

    重写hashCode方法之后user与user1的hashCode值是一样的,然后会调用equals方法去比较,因为equals方法也被我们重写为比较age,而user与user1的age都是相同的,所以调用contains方法的时候返回true。

  • TreeSet

    1. 使用二叉树的原理对新存入的对象按照指定的顺序排序(升序或者降序),每增加一个对象都会进行排序,将对象插入二叉树的指定位置
    2. Integer和String对象都可以进行默认的TreeSet排序,而定义的类不可以,自定义的类必须实现Comparable接口,并实现compareTo方法才可以正常使用
  • LinkHashSet (HashSet+LinkedHashMap)

    底层使用LinkedHashMap来保存所有元素,在相关操作上与HashSet一样

Map

  • HashMap(数组+链表+红黑树)

    根据键的HashCode值存储数据,大多数情况下可以直接定位到它的位置,因此具有很快的访问速度,但遍历的顺序确不是一定的。HashMap允许多个线程对其同时访问,所以HashMap是线程不安全的。可以使用CollectionssynchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

    HashMap在Java7与Java8中的实现方式是不一样的:

    Java7

    HashMap里面是一个数组,然后数组中每个元素都是一个单向链表。

    ggDsUA.jpg

    上图中每一个红色都是嵌套类实体Entry的实例,Entry中包含四个属性:key、value、hash值和用于单向链表的next

    1. capacity:当前数组通量,始终保持$2^n$,可以扩容,扩容后数组大小为当前的2倍
    2. loadFactor:负载因子,默认为0.75
    3. thresold:扩容的阈值,等于$capacity\times loadFactor$

    Java8

    Java8中HashMap的实现由数组+链表+红黑树组成。在HashMap中查找的时候首先通过hash值快速定位到数组的具体下标,但是之后需要顺着链表一个个比较下去才能找到需要的元素。为了降低链表的查找消耗,所以在Java8中,当链表中的元素超过8个以后,会将链表转换为红黑树。

    ggfEs1.jpg

  • HashTable

    线程安全,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入分段锁。在不需要线程安全的场景使用HashMap代替,需要线程安全的场景使用ConcurrentHashMap代替