java笔记

253 阅读12分钟

jvm虚拟机

线程共享区:方法区和堆

线程隔离区:虚拟机栈、本地方法栈和程序计数器

  • 程序计数器:也被称为PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • Java虚拟机栈:也是线程私有的,它的生命周期与线程相同。Java虚拟机栈描述的是执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。
  • 本地方法栈:为本地(Native)方法服务。
  • Java堆:是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里几乎所有的对象实例都在这里分配内存。
  • 方法区:是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

虚引用:为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

类加载

加载 -> 连接(验证、准备、解析) -> 初始化

加载:将类的字节码二进制流读取到JVM,并生成代表这个类的Class对象。
验证:目的是为了保证class文件字节流符合当前虚拟机的要求。
准备:正式为类中定义的静态变量分配内存并设置类变量初始值(数据类型的零值)的阶段。
解析:虚拟机将常量池的符号引用替换为直接引用的过程。
初始化:初始化阶段类变量才会被赋予我们在代码中声明的值。JVM会根据语句执行顺序对类对象进行初始化。

BufferedInputStream和BufferedOutputStream

BufferedInputStream的read方法会读取尽可能多的字节,执行read时先从缓冲区读取,当缓冲区数据读完时再把缓冲区填满,BufferedInputStream的默认缓冲区大小是8192字节。当每次读取数据量接近或远超这个值时,两者效率就没有明显差别了。能够减少访问磁盘的次数,提高文件读取性能。BufferedOutputStream可以一次读很多字节,但不向磁盘中写入,只是先放到内存里。等凑够了缓冲区大小(默认8KB)或者flush()的时候一次性写入磁盘

java的基本特征

封装:隐藏对象的属性和实现细节,仅对外公开接口,增强安全性和简化编程
继承:子类继承父类的特征和行为,子类父类具有相似的特征和功能,提高了代码复用率
多态:在继承的基础上,一个行为在不同的情况有不同的表现形式,继承,重写,父类引用指向子类对象。抽象和具体之间的关系

String

"abc" 为常量池中的字符串,常量池中的字符串可以用==比较
new String("abc") 在堆中创建对象,对象的值指向常量池中的abc
String.intern() 会将字符串添加到常量池中,并且返回该常量的引用

clone

Object类clone方法为protected ,需要重写
实现Clonenable接口,重写clone()方法,super.clone()为native方法,可以实现对象浅克隆
浅克隆:引用类型变量只会复制引用,不是真正的克隆
如果引用类型变量比较多可以用序列化的方式实现深克隆

并发

原子性 原子在化学中反应上是不可在分割的粒子。因此原子性指的是一个不可以被分割的操作,即这个操作在执行过程中不能被中断,要么全部不执行,要么全部执行。且一旦开始执行,不会被其他线程打断。

可见性 指的是一个线程修改了共享变量后,另外线程能立即感知这个变量被修改。

有序性 指程序按照代码的先后顺序执行。有时候为了优化性能,编译器会对字节码指令进行重排序。但是能保证重排序后的执行结果与重排序之前是一致的。

有序性例子:单例模式双检锁初始化对象时有可能会指令重排序

AtomicInteger自增时在不断的进行compareAndSwap操作(比较并交换),对比预期值和更新值是否符合预期,直到成功为止(自旋),具有并发的三要素。CAS是一条CPU的原子指令,由CPU保证了操作的原子性

乐观锁基于一个乐观的假设,即多个线程之间的操作不会发生冲突,适用于读多写少的场景,因为它能够减少锁的使用,提高并发性能。然而,乐观锁需要依赖于CAS机制来确保数据的一致性

JMM规定所有变量都存储在主内存中,每条线程还有自己的工作内存。线程的工作内存中保存了被线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间也无法直接访问对方的工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

java内存模型解释了为什么会有多线程并发问题,内存模型为什么是这样,为了缓解CPU和内存之间速度的不匹配问题。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中

notify()方法只会随机唤醒等待队列中的一个线程,而notifyAll()方法则会唤醒等待队列中的所有线程。

val futureTask = FutureTask {
    Thread.sleep(1000)
    return@FutureTask "123"
}
Thread(futureTask).start()
println("start")
println(futureTask.get())
println("end")

Callable有返回值,可以获取线程执行结果,fulture.get()调用后会阻塞,直到获取到返回值
Callable可以传入FutureTask或者使用线程池submmit()执行
FutureTask实现了runnable和future接口

线程中断可以使用变量控制或者interrupt(),interrupt()作用是给目标线程打上打断标记,当遇到sleep()、wait()、join()等阻塞方法会抛InterruptedException,抛出异常后会清除打断标记。也可以用isInterrupted()判断线程是否被打了打断标记从而退出线程
Thread.interrupted()检测中断并清除中断状态

// 计算任务例子
if ((System.currentTimeMillis() - time) > 1000){
    println("======")
    time = System.currentTimeMillis()
}

join()的线程优先执行,当前正在执行的线程阻塞,直到调用join方法的线程执行完毕或者被打断

yield() 某个时间段,虽然该线程获得了cpu的执行权,但是并不满足执行的条件, 所以把cpu的执行权让给了其他的线程,换句话说,先把此线程变为Runnable,然后再和其他线程一起竞争,还是有可能再被选到。如果再被选到,则从yield之后继续执行,而不是从run方法的开头

线程状态:new, runnable, running, Blocked/Waiting/Timed Waiting, Terminated

CountDownLatch可以用于控制一个或多个线程等待多个任务完成后再执行,初始化需要指明需等待的线程数,当一个线程完成了需要等待的任务后,就会调用 countDown() 方法将计数器减 1,在需要等待其他线程的地方使用awit()等待(可以先countDown后await),计数器减为0时等待结束。可以使用AtomicInteger和wait实现

ExecutorSevice
execute无返回值,直接执行,因此不能阻塞线程,并且在执行异常时会抛出异常
submit 有三种类型,返回future,可以阻塞获取线程执行结果,在执行异常时会被异常处理吃掉,不会抛出异常

位运算

  • 位与(&):二元运算符,两个为1时结果为1,否则为0
  • 位或(|):二元运算符,两个其中有一个为1时结果就为1,否则为0
  • 位异或(^):二元运算符,两个数同时为1或0时结果为1,否则为0
  • 位取非(~):一元运算符,取反操作,即1变为0,0变为1
  • 左移(<<):一元运算符,按位左移一定的位置。高位溢出,低位补0,符号位不变。左移n位相当于乘以2的n次方
  • 右移(>>):一元运算符,按位右移一定的位置。高位补符号位,符号位不变,低位溢出。右移n位相当于除以2的n次方
  • 无符号右移(>>>):一元运算符,高位补零,低位溢出。
  • 最高位为符号位,0表示正数,1表示负数
  • 正数的原码,反码和补码都一样,三码合一
  • 负数的反码:符号位保持不变,其他位取反
  • 负数的补码:反码 + 1
  • 0的反码和补码都是0
  • 数据存储和运算都是以补码的方式进行的

其他

java的基本结构是数组和指针,所有的数据结构都可以用这两个基本结构来构造
hashmap(int initialCapacity);
initialCapacity为数组大小,传入后会转化为最接近的2的n次幂,默认大小为16,负载因子为0.75,最大值为16 * 0.75=12
容量总是2的n次幂,可以利用位运算来替代模运算,提高计算效率,可以使元素更加均匀的分布
hashcode & (tableSize - 1)
初始化时不会初始化数组,先添加后判断是否需要扩容,扩容默认扩容两倍
扩容方式: ‌JDK7‌:扩容时,会创建一个新的数组,并将原数组中的元素重新计算hash值后插入新数组中。如果链表长度大于8且数组大小小于64,还会将链表转换为红黑树。 ‌JDK8‌:扩容时,不需要重新计算每个元素的新位置,只需要判断原来的hash值新增的那个bit是1还是0即可。例如,原长度是16,扩容后长度为32,元素的hashCode为52,计算方式为(32-1) & 52 = 20,刚好是原数组的下标加上原数组的长度。

重写了equals方法就必须重写hashcode方法,主要是为了保证当这个类被用作散列表中的key时,通过equals判断逻辑相等的key,其hashcode值也相同,如果重写了equals不重写hashcode,那么就有可能两个equals相等的对象由于存储地址不同而hashcode值不同,导致散列表中出现两个逻辑相等的重复key

ArrayList使用默认构造器构造时会初始化一个空数组节省内存,到添加元素的时候会扩容
扩容时会复制一个新数组,默认扩容1.5倍,插入和删除时只会移动元素,不会新建数组
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

  • src:源数组
  • srcPos:源数组中的起始位置
  • dest:目标数组
  • destPos:目标数组中的起始位置
  • length:要复制的元素个数
    源数组和目标数组传同一个可以移动元素

Arrays.copyOf(T[] original, int newLength)复制全部到一个新数组
Arrays.copyOfRange(U[] original, int from, int toe)复制区间到一个新数组

LinkedList为双向链表,在按index查找时先判断位于前半段还是后半段,前半段就从first开始查找,后半段就从last开始查找

在JDK1.7中ConcurrentHashMap由Segment(分段锁)数组结构和HashEntry数组组成,且主要通过Segment(分段锁)段技术实现线程安全。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构,因此在ConcurrentHashMap查询一个元素的过程需要进行两次Hash操作

在JDK8中ConcurrentHashMap的put()方法,没有hash冲突时通过CAS插入,有hash冲突时会在桶内加锁,get()方法没有加锁
ConcurrentHashmap 不支持 key 或者 value 为 null是因为concurrenthashmap它们是用于多线程的,并发的 ,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空

Integer在[-128,127]之间存在缓存,指向相同的对象

Byte、Short、Integer、Long、Boolean、Character这六种包装类型在进行自动装箱时都使用了缓存策略

装箱会产生新对象

垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize()方法

整型基本数据类型所容纳的数据量为2的位数次方
比如byte为256个数字
boolean字节数取决于虚拟机的实现

汉字在GBK和UTF-16编码占两个字节,在UTF-8编码占三个字节

final
局部内部类访问局部变量需要加final
因为局部内部类生命周期可能比局部变量长,为了防止局部变量出栈被回收,局部变量会被复制到局部内部类,为了保持两者的一致性所以使用final

java先初始化属性,后执行构造器
kotlin先主构造器,后初始化属性

class.forName会对类进行初始化,底层也是调用classLoader
classLoader不会