Java常见问题

记录Java中的常见概念和原理

参考:

  • https://www.cnblogs.com/fzz9/p/8973315.html
  • https://blog.csdn.net/xinzhou201/article/details/81986594
  • https://github.com/wangzhiwubigdata/God-Of-BigData/blob/master/JVM/jvm%E7%B3%BB%E5%88%97(%E4%B8%89)GC%E7%AE%97%E6%B3%95%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8.md
  • https://www.cnblogs.com/rinack/p/9888717.html

面对对象的三个特点

  • 封装:封装就是隐藏对象的属性和实现细节,仅对外公开接口,形成一个有机的整体
  • 多态:多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
  • 继承:继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法

多态的使用和原理实现

虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

  1. 先从操作栈中找到对象的实际类型 class;
  2. 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
  3. 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
  4. 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;
    如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。

内存管理

  • JVM内存划分
    • 方法区(线程共享):常量、静态变量、JIT(即时编译器) 编译后的代码也都在方法区;
    • 堆内存(线程共享):存放Java实例对象,垃圾回收的主要场所;
    • 虚拟机栈(栈内存):保存局部变量表、操作数栈、动态链接、方法出口信息
    • 本地方法栈 :为 JVM 提供使用 native 方法的服务
    • 程序计数器: 当前线程执行的字节码的位置指示器,下一条命令是分支、循环、还是顺序等等

垃圾回收算法

  • 标记清除算法:”算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
    • 缺点:效率不高;产生内存碎片
  • 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
    • 缺点:不适用于 长期存活的对象;将内存缩小为原来的一半
  • 标记整理算法:同标记清除算法,先标记,然后将存活的对象移动向一端,再将边界以外的内存回收
  • 分代收集算法:将内存对象分为新生代和老生代,新生代频繁创建销毁,多采用复制算法;老生代存活时间长,占用空间大,使用标记整理算法。

Java对象的内存布局

  • Java对象的内存构成:对象头(header)、实例数据(Instance Data)、对齐填充(Padding)
    • 对象头:包含markWord(4字节)和Class对象指针(4字节)
      • markWord:包含哈希码,GC分代年龄,锁状态表示等等
      • 对象指针:通过指针能确定对象属于哪个类,如果对象是一个数组,对象头还会包含数据长度
    • 实例数据:对象实际数据,实际数据大小。实例成员部分就是成员变量的值,包括父类成员变量和本类成员变量。
    • 对齐填充:对齐可选,按8字节对齐。用于确保对象的总长度为 8 字节的整数倍。
  • 对象的创建过程
    1. 类加载检查:虚拟机在解析.class文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。
    2. 为新生对象分配内存:从堆中划分一段对应大小的内存给对象,具体的分配方式有:指针碰撞和空闲列表两种。
      • 指针碰撞:内存规整(采用复制算法或标记整理算法),空闲内存中放着一个指针作为分界点指示器,所以分配内存中只需要将指针向空闲内存挪动一段距离。
      • 空闲列表:内存不规整(采用标记清除法),VM需要维护一个空闲内存列表,来分配一个足够大的内存空间给对象实例。
    3. 初始化:成员变量复制,设置对象头信息,调用对象的构造函数进行初始化
  • 对象的访问方式
    所有对象的存储空间都是在堆里,但是对象的引用在栈中。即堆和栈中都有为这个对象分配空间。
    Java的本地变量表中有存放着对象的引用,根据这个引用可以到java堆中找到对象的实际数据,其中就包含对象头里对象类型数据的指针,然后再根据这个指针到方法区里拿到具体的对象类型数据。

Java多线程

  • 进程:进程是程序的一次执行,是系统运行程序的基本单位。一个进程就是一个执行中的程序。
  • 线程:线程是一个比进程更小的执行单位,一个进程可以产生多个线程。但与进程不同,同类的多个线程共享同一块内存空间和一组系统资源;系统切换线程的负担比切换进程的负担要小。
  • 多线程:多个线程同时运行或交替运行,多线程是高并发系统的基础,所以多线程是必要的。
  • 并发和并行的区别:并发指多个任务交替执行;并行是真正意义上的同时执行。
  • 多线程的创建方式:继承Thread类;实现Runnable接口
  • 线程的生命周期
    1. 新建:创建一个Thread类的实例对象,线程进入新建状态(未被启动)
    2. 可运行:调用了start方法,等待被调度器选中,获得cpu的使用权
    3. 运行:获得CPU资源正在执行,此时除非线程自动放弃CPU或者有优先级更高的线程进入,线程将一直运行到结束
    4. 死亡:线程执行完毕,或被杀死(自然终止:正常运行完;异常终止:调用stop方法)
    5. 阻塞:因某种原因让出CPU,进入可运行状态(睡眠:调用sleep;正在等待:wait,等待notify;被另一个阻塞:调用suspend)

Java线程池

  • 为什么要用线程池:大多数时候,我们不止会执行一个任务。如果每次都创建线程->执行任务->销毁线程,还造成很大的性能开销。线程池允许我们预先创建好多个线程,放在池中,这样可以在需要线程的时候直接获取,避免多次重复创建、销毁带来的开销。
  • 粗浅理解:所谓线程池本质是一个hashSet,里面放着核心线程。多余的任务会放在阻塞队列中。只有当阻塞队列满了后,才会触发非核心线程的创建。所以非核心线程只是临时过来打杂的。直到空闲了,然后自己关闭了。
  • 线程池的种类:
    • newSingleThreadExecutor:这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务
    • newFixedThreadPool:创建一个固定大小的线程池。每次提交一个任务就是创建一个线程,直到线程达到线程池最大大小。
    • newCachedThreadPool:创建一个可缓存的线程池,如果线程池的大小超过任务所需,回收空闲线程,当任务数量增加时,又可以增加线程,不对线程数量进行限制
    • newScheduledThreadPool:创建一个无限大小的线程池,支持定时和周期性的执行任务需求

Synchronized关键字

  • 定义:重锁,防止两个线程同时操作对象中的实例变量,则会出现“非线程安全”
  • 修饰的范围:
    • 修饰方法和代码块:只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
    • 修饰静态方法:静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
    • 修饰类:
    class ClassName {
     public void method() {
        synchronized(ClassName.class) {
           // todo
        }
     }
    }
    • 总结:无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 实现原理:每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
  • happens-before:来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。 因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
    • 1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
    • 常见规则:
      • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
      • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
      • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • CAS:compare and swap又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。
    • ABA问题:比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。

violatile 关键字

  • 概念:线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。 通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
  • 实现原理:为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。一个处理器的缓存回写到内存会导致其他处理器的缓存失效; 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。

你可能感兴趣的:(Java常见问题)