JavaSE

1. 自增变量

  1. public class Test {
  2. public static void main(String[] args) {
  3. int i = 1;
  4. i = i++; // i=1先入操作数栈,然后局部变量表i=2,最后把操作数栈中的值出栈赋值
  5. int j = i++; // i=1入栈,局部变量表中的i自增1为2,操作数中的值出栈赋值给j
  6. // 第一个i,先把i=2入操作数栈 2
  7. // 第二个++i,局部变量i自增1为3,然后入操作数栈 此时操作数栈中有2 3
  8. // 第三个i++,先把局部变量i=3入操作数栈,局部变量自增1为4 此时操作数栈中有2 3 3
  9. // 弹出操作数栈中栈顶两个值做乘法,结果再压入栈 此时操作数栈中有2 9
  10. // 最后把操作数栈中的值弹出做加法,9 + 2 = 11
  11. int k = i + ++i * i++;
  12. System.out.println("i=" + i); // 4
  13. System.out.println("j=" + j); // 1
  14. System.out.println("k=" + j); // 11
  15. }
  16. }

自增在变量(局部变量表),运算在栈中(操作数栈),栈中运算完,赋值变量中。
i++:先把i的值压入操作数栈,再自增;
++i:先自增,再把i的值压入操作数栈;
小结:

  • 赋值=,最后计算
  • =右边,从左到右加载值依次压入操作数栈
  • 实际先算哪个看运算符优先级
  • 单纯的自增自减运算都是直接在局部变量表中修改值,不经过操作数栈
  • 最后的赋值之前,临时结果也是存储在操作数栈中

    2. 单例模式

    要点:
  1. 一个类只能有一个实例:构造器私有化;
  2. 必须自行创建该实例:含有一个该类的静态变量来维护这个实例;
  3. 必须向整个系统提供该实例:对外提供获取该实例的方式(直接暴露、用静态变量的get方法获取)

单例模式分为饿汉式跟懒汉式,饿汉式线程安全,懒汉式线程不安全(也有线程安全的写法)。

饿汉式

饿汉式:直接创建实例、枚举类型、静态代码块的饿汉式(适合复杂实例化)

  1. // 饿汉式,直接创建实例
  2. public class Singleton {
  3. public static final Singleton INSTANCE = new Singleton();
  4. private Singleton() {}
  5. }
  6. // 饿汉式,枚举
  7. public enum Singleton {
  8. INSTANCE
  9. }
  10. // 饿汉式,静态代码块
  11. // 比如一个单例类需要加载配置文件中的信息,来赋予其属性的值
  12. public class Singleton {
  13. public static final Singleton INSTANCE;
  14. private String info;
  15. static {
  16. try {
  17. Properties pro = new Properties();
  18. // single.properties文件在src文件夹下
  19. pro.load(Singleton.class.getClassLoader()
  20. .getResourceAsStream("single.properties"));
  21. INSTANCE = new Singleton(pro.getProperty("info"));
  22. } catch (IOException e) {
  23. throw new RunTimeException(e);
  24. }
  25. }
  26. private Singleton(String info) {this.info = info;}
  27. }

懒汉式

懒汉式:线程不安全、线程安全、静态内部类(线程安全)

  1. // 懒汉式(线程不安全)
  2. public class Singleton {
  3. private static Singleton instance;
  4. public static Singleton getInsatance() {
  5. if (instance == null) instance = new Singleton();
  6. return instance;
  7. }
  8. private Singleton() {}
  9. }
  10. // 懒汉式(线程安全)
  11. public class Singleton {
  12. public static Singleton instance;
  13. public static Singleton getInsatance() {
  14. if (instance == null) {
  15. synchronized (Singleton.class) {
  16. if (instance == null)
  17. instance = new Singleton();
  18. }
  19. }
  20. return instance;
  21. }
  22. private Singleton() {}
  23. }
  24. // 懒汉式(静态内部类)
  25. public class Singleton {
  26. private Singleton() {}
  27. public static class Inner {
  28. private static final Singleton INSTANCE = new Singleton();
  29. }
  30. public Singleton getInstance() {
  31. retrun Inner.INSATANCE;
  32. }
  33. }

这里来说说第三种静态内部类的方式。在内部类加载和初始化时才会创建INSTANCE对象,静态内部类不会随着外部类的加载和初始化而初始化,需要单独加载和初始化,因为是在内部类加载和初始化时创建的实例,所以是线程安全的
总结:如果饿汉式枚举类最简单,如果懒汉式静态内部类最简单。

3. 类初始化与实例初始化

image.png7image.png
求上述代码的运行结果。image.png
知识点:类初始化过程、实例初始化过程、方法重写

类初始化过程

  1. 一个类要创建实例,需要先加载并初始化该类
    1. main方法所在的类需要先加载和初始化
  2. 子类初始化要先初始化其父类
  3. 一个类初始化就是执行()方法:
    1. ()方法由静态类变量显示赋值代码静态代码块组成
    2. 静态类变量显示赋值代码和静态代码块按在代码中的顺序执行,且只执行一次

父类Father的初始化:(1)j = method(),(2)父类的静态代码块;
子类Son的初始化:(1)子类的j = method(),(2)子类的静态代码块;
输出顺序:5、1、10、6

实例初始化过程

  1. 实例初始化执行的就是()方法
    1. ()方法可能重载有多个,有几个构造器就有几个()方法
    2. ()方法由非静态实例变量显示赋值代码非静态代码块、对应构造器代码组成
    3. 非静态实例变量显示赋值代码和非静态代码块按在代码中的顺序执行,而对应的构造器代码最后执行
    4. 每次创建实例对象,调用对应的构造器,执行的就是对应的()方法
    5. ()方法首行是super()或super(实参列表),即对应父类的()方法

类实例化时调用方法顺序:super(),非静态代码块/非静态成员变量显示赋值代码,构造器

方法重写

非静态方法的前面其实有一个默认的对象this,this在构造器(或者())表示正在创建的对象(就是new的什么,this就指向什么),因为这里创建的是子类对象,所以this指向的是子类重写的method方法。
哪些方法不可以被重写:final、static、private

所以这里在创建子类对象的时候会输出:9、3、2、9、8、7

总结

由父及子,静态先行。 非静态代码块>构造器

4. 方法的参数传递机制

  1. public class MethodValueTransfer {
  2. public static void main(String[] args) {
  3. int i = 1;
  4. String str = "hello";
  5. Integer num = 2;
  6. int[] arr = {1, 2, 3, 4, 5};
  7. MyData myData = new MyData();
  8. change(i, str, num, arr, myData);
  9. System.out.println("i = " + i); //1
  10. System.out.println("str = " + str); // hello
  11. System.out.println("num = " + num); // 2
  12. System.out.println("arr = " + Arrays.toString(arr)); // 2 2 3 4 5
  13. System.out.println("myData = " + myData.a); // 11
  14. }
  15. public static void change(int j, String s, Integer n, int[] a, MyData m) {
  16. j += 1;
  17. s += "world";
  18. n += 1;
  19. a[0] += 1;
  20. m.a += 1;
  21. }
  22. }
  23. class MyData {
  24. int a = 10;
  25. }

考点:
方法的参数传递机制(值传递、引用传递);String(使用final char[] 存储)、包装类(final int 存储)的不可变性
image.png

5. 成员变量与局部变量

  1. public class Season6 {
  2. static int s; // 类变量
  3. int i; // 实例变量
  4. int j; // 实例变量
  5. {
  6. int i = 1; // 这里的i作用域只有非静态代码块,局部变量
  7. i++; // 这个i是非静态代码块中的i,不是实例变量,就近原则
  8. j++;
  9. s++;
  10. }
  11. public void test(int j) {
  12. j++; //这里的j是形参,就近原则
  13. i++;
  14. s++;
  15. }
  16. public static void main(String[] args) {
  17. Season6 obj1 = new Season6(); // s = 1; i = 1 j = 1
  18. Season6 obj2 = new Season6(); // s = 2 i = 1 j = 1
  19. obj1.test(10); // s = 3 i = 1 j = 1
  20. obj1.test(20); // s = 4 i = 2 j = 1
  21. obj2.test(30); // s = 5 i = 1 j = 1
  22. System.out.println(obj1.i + "," + obj1.j + "," + obj1.s); // 2 1 5
  23. System.out.println(obj2.i + "," + obj2.j + "," + obj2.s); // 1 1 5
  24. }
  25. }

考点:

  • 就近原则
  • 变量分类:
    • 成员变量:类变量、实例变量;可用修饰符:public、private、protected、final、static、volatile、transient
    • 局部变量:方法体、代码块中、形参;可用修饰符:final
  • 非静态代码块的执行:每创建实例对象都会执行
  • 方法调用规则:调用一次执行一次

image.png

6. Spring Bean的作用域

可以通过scope属性指定bean的作用域
image.png

7. SpringMVC工作流程

image.png

8. MyBatis中实体类属性名与表中的字段名不一致

  1. 在mapper映射文件中写SQL语句时,使用别名(别名为实体类属性)
  2. 在mybatis配置文件中开启驼峰命名规则

    1. <settings>
    2. <setting name=""mapUnderscoreTocamelCase/>
    3. </settings>
  3. 在Mapper映射文件中使用resultMap自定义映射

    Java高级

    8. Linux常用的服务类相关命令

    centos7:
    查看服务是否自启动命令:systemctl list-unit-files [| grep server_name]
    通过systemctl命令设置自启动:systemctl enable/disable service_name

    9. git分支相关命令

  4. 创建分支

创建分支:git branch <branch_name> 查看分支:git branch -v

  1. 切换分支

切换分支:git checkout <branch_name>
创建分支并切换:git checkout -b <branch_name>

  1. 合并分支

比如想把某个分支合并到master分支
先切换到主干:git checkout master 合并: git merge <branch_name>

  1. 删除分支

先切换到主干:git checkout master 删除:git branch -D <branch_name>
image.png

*10. Redis持久化类型与区别

Redis提供了两种持久化的方法,分别为AOF(Append of File)和RDB(Redis Database)。Redis持久化笔记

10.1 RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘(全量保存),也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
流程:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失
优点:

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高的长河更适合使用
  • 节省磁盘空间
  • 回复速度快

缺点:

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然Redis在fork时使用了写时拷贝技术(copy on write Redis cow细节,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

    10.2 AOF

    AOF是以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
    流程:

  • 客户端的请求写命令会被append追加到AOF缓冲区内;

  • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
    • appendfsync always:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
    • appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
    • appendfsync no:redis不主动进行同步,把同步时机交给操作系统。
  • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

若RDB跟AOF同时开启,则优先使用AOF(数据不存在丢失)
优点:

  • 数据完整性更强
  • 可读的日志文本,通过操作AOF文件,可以处理误操作

缺点:

  • 比起RDB占用更多的磁盘空间。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成恢复不能。

    11. Mysql什么时候适合建索引

    mysql高级:什么时候需要创建索引
    需要创建索引的条件:

  • 主键自动建立唯一索引(唯一+非空)

  • 频繁作为查询条件的字段应该创建索引
  • 查询中与其他表关联的字段,外键关系建立索引
  • 单键/组合索引的选择问题,组合索引性价比更高
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度,建的复合索引尽量与Order by 一致
  • 查询中统计或者分组字段(group by也和索引有关,在分组操作的时候其实也已经做了一次排序了,先排序后分组)

注:单值/组合索引的选择问题?(在高并发下倾向于创建组合索引)
不需要创建索引的条件:

  • 频繁更新的字段不适合创建索引(因为每次更新不止更新实体表数据,还会更新索引文件。)
  • where条件里用不到的字段不适合创建索引。
  • 表记录太少,经常增删改的表
  • 如果某个数据列包含众多重复的内容,为它建立索引,没有太大的实际效果。假如一个表有10万行记录,有一个字段A只有true和false两种值,并且每个值的分布概率大约为50%,那么对A字段建索引一般不会提高数据库的查询速度。索引的选择性是指索引列中不同值的数目与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。一个索引的选择性越接近于1,这个索引的效率就越高。

    12. 什么时候索引会失效

    image.png
    索引失效的情况

    13. JVM垃圾回收机制

    GC发生在JVM哪部分,有几种GC,它们的算法是什么?
    image.png

    GC是什么(分代收集算法)

    次数上频繁收集Young区(新生代):Minor GC
    次数上较少收集Old区(老年代):Full GC/Major GC?
    基本不动元空间(Metaspace),元空间直接使用物理内存。
    JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
    因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
    Minor GC和Full GC的区别:
      普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
      全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上

    GC三大算法

    如何判断一个对象是否应该被回收

    1. 引用计数
    每个对象都有一个引用计数器,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。
    优点:实时性高;区域性,不需要扫描全部对象。
    image.png
    GCRoots 根可达算法

1. 复制算法(Copy)

HotSpot把新生代(Young区)分三个区:Eden、survivor From和survivor To
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)。
因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法(复制算法在复制数量少且小的对象时效率很高),复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
image.png

原理:

Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。

标记复制过程中,复制区是eden+Survivor-from,粘贴区是Survivor-to +old;Survivor-to就那么大,丢不下就得往old丢。动态对象年龄判断的目的是要把粘贴的对象中年龄小的丢Survivor-to,年龄大的丢old。
(重要)动态对象年龄判定:如果在Survivor空间中【低或等于某个年龄的】所有对象大小的总和大于Survivor空间的一半(TargetSurvivorRatio默认0.5),年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
(进行一次Minor GC后,Survivor To区就会成为下一次GC的Survivor From区,同理Survivor From区会成为下一次的Survivor To区)
gc_copying.gif

优缺点:
  • 优点:没有清除和标记的过程,效率高;没有内存碎片;不会暂停程序;
  • 缺点:浪费了一半内存;如果对象存活率很高,则效率极低。

    2. 标记清除(Mark-Sweep)

    老年代一般是由标记清除或者是标记清除与标记压缩(标记整理)的混合实现。当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停。标记清除法分为两个阶段,先标记出要回收的对象,然后统一进行回收。
  1. 标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出需要清除的对象(也可以标记存活对象)。
  2. 清除:遍历整个堆,把未标记的对象清除。

mark_sweep.gif

优缺点:
  • 优点:不会浪费内存空间;
  • 缺点:此算法需要暂停整个应用,会产生内存碎片(内存空间是不连续的) ;效率低,需要两次遍历;

    3. 标记压缩(Mark-Compact)

    老年代一般是由标记清除或者是标记清除与标记压缩(标记整理)的混合实现。
    image.png
    在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
    标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价

    优缺点
  • 优点:解决了标记清理算法产生内存碎片的问题;

  • 缺点:效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

mark_compact.gif

总结

  • 内存效率:复制算法>标记清除>标记整理(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
  • 内存整齐度:复制算法=标记整理>标记清除
  • 内存利用率:标记清除=标记整理>复制算法

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程