HashMap 源码剖析:https://www.cnblogs.com/chenssy/p/3521565.html
Java 中创建子类实例时会创建父类实例吗
构建一个对象时,jvm 会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及父类的实例属性,注意,这里并不仅仅为当前对象的实例属性分配空间,还需要给父类的实例属性分配空间。总之,会为父类分配堆内存,但这块堆内存是属于子类的堆内存
synchronized 和 volatile(可见性与禁止指令重排序) 的区别
可见性:指在多线程情况下,某一线程对变量进行更改后,其余线程能够立即知晓
指令重排:JVM 对指令进行优化自动安排自指令顺序
volatile,final,synchronized 都能实现可见性
volatile 的本质是在告诉 JVM 当前变量在寄存器中的值是不确定的,需要在主存中读取,synchronized 则是锁定,只有当前线程可以访问变量,其他线程被阻塞
volatile 只能使用在变量级别,synchronized 可以使用在变量、方法
volatile 只能保证变量修改的可见性,但不能保证变量修改的原子性,而synchronized 可以保证变量修改的可见性和原子性
volatile 不会造成线程的阻塞,而 synchronized 会造成线程的阻塞
volatile 标记的变量不会被编译器优化,而 synchronized 标记的变量可以被编译器优化
使用 volatile 需要满足的条件:
运算结果不依赖于变量的当前值,或者保证只有一个线程修改变量的值
不需要与其他状态变量参与不变约束
在访问变量时不需要加锁
volatile 的不变性
将当前处理器缓存的数据回写到系统内存
这个写回内存的操作会造成其他 cpu 里缓存了该内存地址的数据无效
volatile 变量在赋值之后会有一个 lock add 指令,这个指令相当于内存屏障,重排序时不能将屏障后的指令排序到屏障之前
volatile 禁止指令重排序:普通变量仅会保证在方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量的赋值操作顺序与程序代码中的操作顺序一致
volatile保证可见性:add 指令会使得其他工作线程的工作内存缓存的数据失效
Java 线程同步的方法
同步方法,即有 synchronized 关键字修饰的方法
同步代码块,即有 synchronized 关键字修饰的代码块
使用特殊域变量(volatile)实现线程同步
使用 reentranLock 实现线程同步
使用局部变量实现线程同步,如果使用 ThreadLocal 管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己变量的副本,而不会对其他变量产生影响
垃圾回收算法与垃圾回收器
垃圾回收算法
标记清除算法:将所有需要回收的对象进行标记,标记结束后对标记的对象进行回收,但是效率低,会造成大量的碎片
复制算法: 复制算法将空间分为两部分,每次使用其中的一部分。当一块内存用完了,就将这块的所有对象复制到另一块,将已使用的块清除。不会产生碎片,但是会浪费一定的内存空间。在堆中的年轻代使用该算法,因为年轻代的对象多为生存周期比较短的对象。年轻代将内存分为一个 Eden,两个 survivor。每次使用 Eden 与一个 survivor。当回收时,将 survivor 与 Eden 中存活的对象复制到另一个 survivor,最后清理掉 Eden 与 survivor。当 survivor 与 Eden 中存活的对象大小超过另一个 survivor 则需要老年代来担保
标记整理算法:复制算法在对象存活率较高时,复制会使得效率降低。根据老年代的特点,使用标记整理算法。标记后将所有存活的对象移向一端,将其他的清理,解决了碎片的问题
分代收集算法:年轻代,老年代根据自己各自不同的特点采取不同的算法。
垃圾回收器
serial 收集器:是单线程收集器,在进行垃圾回收时需要停止其他的所有工作线程
parNew 收集器:是 serial 的多线程版本,在单线程的环境下,parNew 绝不会比 serial 收集器有更好的效果,因为存在着线程的开销,但是随着 cpu 的增加便会体现出优势,默认情况下的线程数和 cpu 数量相等
parallel scavenge 收集器: 年轻代收集器,多线程并行收集,使用复制算法,与 parNew 相似。CMS,parNew,serial 的设计目标是为了缩短用户线程的停顿时间,但是 parallel scavenge 的设计目标是实现一个可控的吞吐量(cpu 运行用户代码时间/cpu消耗的总时间)。可以设置两个参数最大垃圾收集停顿时间、吞吐量大小。但是最大垃圾收集停顿的时间越小,系统设置的新生代越小,GC 频率增加
serial old 收集器:是 serial 在老年代的实现版本
CMS 收集器:是一种为目标获取最短停顿时间的收集器,基于标记清除(老年代是唯一一个基于标记清除的算法,除 G1 以外)的算法实现。整个过程有四个步骤:初始标记、并发标记、重新标记、并发清除,其中初始标记与并发标记仍要停止所有用户线程。初始阶段,主要负责标记 GC root 能直接关联的对象,速度很快;并发标记是从 GC root 开始继续向下进行标记;重新标记是统计那些在并发标记的过程中发生变化的标记;这个阶段的时间要比初始标记长但是比并发标记短。并发清除是清除老年代中的垃圾。
CMS 的缺点:
采用标记清除的算法,会产生碎片
不能处理浮动垃圾
浮动垃圾:在并发清除时,用户线程还在运行,还会有新的垃圾产生,这部分只能等到下次 GC 才能处理
对 cpu 特别敏感。由于 CMS 最耗时的并发标记和并发清除和用户线程是同时执行的,因此可以降低停顿时间,但是并发标记会占用一部分的 cpu 资源,导致应用程序变慢
G1 收集器:唯一一个可以同时用于年轻代和老年代的垃圾收集器。G1 收集器采用标记整理的算法,避免碎片。使用该收集器时,其堆的内存布局就发生变化,将堆分为不同的大小相等的 region ,G1 追踪每个 region 的垃圾堆积的价值大小,然后有一个优先列表,优先回收价值最大的 region (每个 region 有一个 remembered set,为了避免作可达性分析时扫描整个堆,当引用在不同的 region 之间时,则将相关引用信息 记录到 remembered set 中),避免在整个堆中进行全区域的垃圾收集,能建立可预测的停顿时间模型。整个过程包括以下四个步骤:初始标记,并发标记,最终标记,筛选回收。初始标记、并发标记和 CMS 相似;
最终标记:将在兵法标记阶段那些发生变化的对象的变化记录写入线程 remembered set log,同时与 remembered set 合并;
筛选回收阶段:通过堆每个 region 的价值湖人成本进行筛查,以得到一个最好的回收方案,并回收
Java 中的 init 和 clinit 方法
1.Java 在编译之后会在字节码文件中生成 <init> 方法,称之为实例构造器,该实例构造器会将语句块、变量初始化,调用父类的构造器等方法收敛到 <init> 方法中,收敛顺序为:
父类变量初始化
父类语句块
父类构造函数
子类变量初始化
子类语句块
子类构造函数
所谓收敛到 <init>方法中的意思就是将这些操作放入到 <init>中去执行
2.Java 编译之后会在字节码文件中生成 <clinit> 方法,称之为类构造器,类构造器同实例构造器一样,会将静态语句块、静态变量初始化,收敛到 <clinit> 方法中,收敛顺序为
父类静态变量初始化
父类静态语句块
子类静态变量初始化
子类静态语句块
若父类为接口,则不会调用父类的 <clinit> 方法。一个类可以没有 <clinit> 方法
<clinit> 方法是在类加载的过程中执行,而 <init> 是在类实例化的过程中执行的,所以 <clinit> 一定比 <init> 先执行
重载和重写的区别
重载
重载发生在本类,方法名必须相同,参数列表不同,与返回值无关,只和方法名,参数列表,参数的类型有关
方法名必须相同
方法的参数列表一定不一样
访问修饰符和返回值类型可以相同也可以不相同
简单来说,重载就是对于不同的情况写不同的方法。比如,同一个类中,写不同参数的构造函数来初始化不同的参数
重写
重写发生在父类和子类之间,比如所有类都继承自 Object 类,Object 类本身就有 equals、hashcode 等方法,在任意子类中定义了重名和同样的参数列表就构成了方法重写
方法名必须相同,返回值类型必须相同
参数列表必须相同
访问权限不能比父类中重写的方法权限更低。例如:如果父类中的一个方法被声明为 public 那么重写的方法就不能被声明为 protected
父类的私有方法和声明为 final 的方法不能被重写
构造方法呗不能被重写