HashMap
-
特点:无序、非线程安全、基于数组和链表实现、允许key、value为null
-
容量和负载因子
capacity : 容量,初始大小16。table数组的长度
load_factor : 负载因子,默认0.75
当size>= capacity * load_factor 时数组进行扩容,新的table长度为之前的2倍。
-
jdk 1.8之后,链表长度超过8会转为红黑树,优化查找效率。
LinkedHashMap
-
特点:继承自HashMap、有序,双向链表
-
读取顺序:accessOrder=false时按插入顺序读取,当accessOrder=true时按访问顺序读取
-
扩展了HashMap的Entry,增加了两个属性:Entry before, after 指向前后节点。
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
ConcurrentHashMap
-
同样是线程安全
-
JDK 1.7实现
是由
Segment
数组、HashEntry
数组组成,和HashMap
一样,仍然是数组加链表组成。采用分段锁技术,每当一个线程占用锁访问一个
Segment
时,不会影响到其他的Segment
。(https://postimg.cc/PLh8L8SF)
-
JDK 1.8实现
抛弃了原有的 Segment 分段锁,而采用了
CAS + synchronized
来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
(https://postimg.cc/svsYQq1J)
HashTable
- 不能存储null的key和value,底层数组和链表实现
- 线程安全,使用synchronized来实现线程安全。HashTable只有一把锁,只能允许一个线程进行操作,效率低。如果两个线程同时想get数据,并不能实现,只能一个线程get,其他等待。
- 效率低,推荐使用ConcurrentHashMap
ArrayList LinkedList
-
ArrayList: 基于数组实现,非线程安全。
当数组大小不足时增长率为当前长度的50%。
使用索引在数组中搜索和读取数据是很快的 -
LinkedList: 基于双向链表实现,非线程安全。
插入,添加,删除操作速度更快。LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
ThreadLocal
-
ThreadLocal提供了线程的局部变量,每个线程都可以通过
set()
和get()
来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程间的数据隔离~。 -
原理:
- 每个Thread维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
-
为什么Entry的key为ThreadLocal的弱引用?
当threadLocal不使用后,将其置为null,由于threadLocal没有强引用指向,会顺利倍GC回收。
但是如果这里不是WeakReference,下图虚线是个强引用的话,虽然threadLocal置为了null,但是threadLocal就会因为和entry存在强应用无法被回收,造成内存泄漏,除非线程结束,线程被回收了,threadLocalMap也跟着回收。
参考 https://blog.csdn.net/levena/article/details/78027136
https://blog.csdn.net/qq_42862882/article/details/89820017
Java泛型
- 语法糖
指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
- 泛型的目的
Java 泛型就是把一种语法糖,通过泛型使得在编译阶段完成一些类型转换的工作,避免在运行时强制类型转换而出现 ClassCastException,即类型转换异常。
- 泛型的好处
1.类型安全。使得程序在编译期间就能对类型错误进行捕获
2.消除了代码中的许多强制类型转换(因为使用泛型,编译器在编译时会做类型转换)
3.增加代码可靠性、可读性
- 通配符
A > B > C > D > E
1.<?>未知类型通配符 :因为类型未知,所以不能写入数据,但是可以用Object接收数据,因为肯定是Object或其子类。
2.<? extends T> 上限通配符
List<? extends C> list = new ArrayList<E>() 或 new ArrayList<C>() 或 new ArrayList<D>();
list不能插入数据,比如如果插入E,list有可能可能是 List<C>类型
但是可以读取数据,因为数据肯定是C或C的子类。
3.<? super T> 下限通配符
List<? super C> list = new ArrayList<A>() 或 new ArrayList<B>() 或 new ArrayList<C>();
list可以插入数据,类型为C或C的子类。
但是不能读取数据,比如要去A类型数据,有可能list是 List<B> 类型
- PECS 原则: Producer Extends, Consumer Super
Java深拷贝和浅拷贝
- 浅拷贝(Shallow Copy):
- 对于基本数据类型,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
- 对于引用类型,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
-
深拷贝(deep copy):
对于深拷贝来说,不仅要复制对象的所有基本数据类型,还要为所有引用类型的成员变量申请存储空间,并复制每个引用类型所引用的对象,直到该对象可达的所有对象。 -
延迟拷贝:
延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
equals和==的区别?
-
equals 没有被重写的情况下,和==等价,比较的就是两个对象在栈中的引用(地址)
-
hashcode 直接返回对象的内存地址
-
== 如果是基本类型,比较的是值;如果是对象,比较的是对象在栈中的引用,如果要比较对象的在堆中的内容,则需要重写equals方法。
Java对于eqauls方法和hashCode方法是这样规定的:
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同。上面说的对象相同指的是用eqauls方法比较。
JVM运行时内存数据模型
-
方法区 :属于线程共享区域
存放已经被虚拟机加载的类信息,如
常量,静态变量
。 这块区域也被称为永久代
。 -
堆 :属于线程共享区域
堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。
这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用
分代回收算法
,所有堆内存也分为新生代
、老年代
,可以方便垃圾的准确回收。 -
虚拟机栈
虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。
每一个栈帧由
局部变量区
、操作数栈
等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。 -
本地方法栈
与虚拟机栈所发挥的作用相似。它们之间的区别不过是虚拟机栈为虚拟机执行java方法,而本地方法栈为虚拟机使用到的Native方法服务。
-
程序计数器
记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。
JVM的垃圾回收机制
-
哪种对象需要回收?
有两种方法判断
- 引用计数法
- 可达性分析法
-
怎么回收(回收算法)?
[1] 标记-清除算法
标记清除算法分为两个步骤,标记和清除。 首先将不需要回收的对象标记起来,然后再清除其余可回收对象。
效率不高,并且容易造成内存不连续(内存碎片问题)。
[2] 复制算法
复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。
简单高效,不会造成内存碎片问题,但是浪费内存。
[3] 标记-整理算法
复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。
所以老年代中使用的时候
标记整理算法
,它的原理和标记清除算法
类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。[4] 分代回收算法
现代多数的商用
JVM
的垃圾收集器都是采用的分代回收算法,和之前所提到的算法并没有新的内容。只是将 Java 堆分为了新生代和老年代。由于新生代中存活对象较少,所以采用复制算法,简单高效。
而老年代中对象较多,并且没有可以担保的内存区域,所以一般采用标记清除或者是标记整理算法。