堆和栈
- 堆
- Java的堆是一个运行时数据区,类的对象从堆中分配空间。这些对象通过new等指令建立,通过垃圾回收器来销毁。
- 堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。
- 栈
- 栈中主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和对象的引用。
- 栈的优势是,存取速度比堆快,栈数据可以共享(String常量池)。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。
- 常量池
- 存放字符串常量和基本类型常量
- 好处是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
- String
- String str = "abc"的内部工作过程
- (1) 先定义一个名为str的对String类的对象引用变量放入栈中。
- (2) 在常量池中查找是否存在内容为"abc"字符串对象。
- (3) 如果不存在则在常量池中创建"abc",并让str引用该对象。
- (4) 如果存在则直接让str引用该对象
- String str = new String("abc")创建过程
- (1) 先定义一个名为str的对String类的对象引用变量放入栈中。
- (2) 然后在堆中(不是常量池)创建一个指定的对象,并让str引用指向该对象。
- (3) 在常量池中查找是否存在内容为"abc"字符串对象。
- (4) 如果不存在,则在常量池中创建内容为"abc"的字符串对象,并将堆中的对象与之联系起来。
- (5) 如果存在,则将new出来的字符串对象与字符串常量池中的对象联系起来(即让那个特殊的成员变量value的指针指向它)
- intern() Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;
- String str = "abc"的内部工作过程
java软引用与弱引用区别
参考了一些资料
- 强引用
- 我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。
- 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
- 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应强引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
- 软引用(SoftReference)
- 只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
- 当JVM认为内存充足的时候,不会去回收软引用
- 软引用什么时候会被回收
- 弱引用(WeakReference)
- 当发生GC时,如果扫描到一个对象只有弱引用,不管当前内存是否足够,都会对它进行回收。
String str = new String("abc"); //创建一个弱引用,让这个弱引用引用到str字符串 WeakReference weakReference = new WeakReference(str); //切断str引用和 str 字符串之间的引用,此时str 只有一个弱引用weakReference指向它 str = null; // 没有进行垃圾回收,我们还可以通过弱引用来访问他 System.out.println(weakReference.get()); // abc //强制进行垃圾回收 System.gc(); //再次取出弱引用所引用的对象 System.out.println(weakReference.get()); // null
- 虚引用(PhantomReference)
- 垃圾回收时回收,无法通过引用取到对象值
- 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
- 虚引用主要用于检测对象是否已经从内存中删除。程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用对象是否即将被回收。
- 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
final变量用反射修改
-
当final修饰的成员变量在定义的时候就初始化了值,那么java反射机制就已经不能动态修改它的值了。
- 原因:编译期间final类型的数据自动被优化了,即:所有用到该变量的地方都被替换成了常量。
public final String name = "abc"; public String getName() { return "abc"; }
当final修饰的成员变量在定义的时候并没有初始化值的话,那么就还能通过java反射机制来动态修改它的值。
字符串hash函数
- 源码
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
-
为什么是31
- 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一,为啥选择质数,质数可以降低哈希算法的冲突率
- 31可以被 JVM 优化,31 * i = (i << 5) - i
- Effective Java : 选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。
ThreadLocal
- ThreadLocal是并发场景下用来解决变量共享问题的类,它能使原本线程间共享的对象进行线程隔离,即一个对象只对一个线程可见
- 当设置value时,变量的值保存在一个与线程相关的map中(ThreadLocalMap),这样做是为了避免多线程竞争,因为放在Thread对象中就相当于线程私有了,处理的时候不需要加锁
- ThreadLocalMap里面的 Entry extends WeakReference<ThreadLocal<?>>
- 如何在数组中定位位置的,int i = key.threadLocalHashCode & (table.length - 1); 这里可以联想到hashMap中的定位方式
- 发生hash冲突了怎么办呢?
- 通常解决哈希冲突有两种解决方式,一种是拉链法,一种是线性探测法
- ThreadLocalMap 用到的是线性探测法,将所有的值放在一个数组里面然后根据散列的结果到数组中取值
- 存在的潜在问题
- 如果任务对象结束而线程实例仍然存在(常见于线程池的使用中,需要复用线程实例),那么仍然会发生内存泄露。场景:如果ThreadLocal这个对象被回收了,但是我当前的线程还是在继续运行,ThreadLocalMap也是还会继续存活的,但是,这个时候,TheadLocalMap的key已经不存在了, 但是,我们的value可不是WeakReference弱引用,并没有被GC。这样,这个value只要线程还在继续运行,就永远不会被GC掉。
- 线程复用会产生脏数据
- ThreadLocalMap在它的getEntry、set、remove、rehash等方法中都会主动清除ThreadLocalMap中key为null的Entry,但如果我们声明ThreadLocal变量后,再也没有调用过上述方法,依然会发生内存泄露
- 扩容
- 扩容是两倍两倍的进行扩容,然后将老的数据从老的table中拿出,然后重新hash得到一个新的index,然后填充到新的table中。这就是一个简单的两个数组的拷贝,然后重新设置了一下阈值
- set方法源码及分析
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 这里就是采用的线性探测法。一直遍历这个数组,然后找到值
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到值了,替换掉原来的值
if (k == key) {
e.value = value;
return;
}
// 发现这个位置上没有值,就讲这个位置设置对应的值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// tab[i]==null,这里直接new
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots : 由于是弱引用,所以这个地方,他在进行一个填充值的时候,进行了一个额外的删除已经被GC的Key对应的Value
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 这里面会调用扩容的方法
rehash();
}
编码格式
- 文字到0、1的映射称为编码,反过来从0、1到文字叫解码。
- 最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),0 - 255被用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码
- Unicode编码定义了这个世界上几乎所有字符的数字表示,已经扩展到了 21 位
- Unicode给这串数字ID起了个名字叫[码点]。[码点]经过映射后得到的二进制串的转换格式单位称之为[码元]
- [码点]就是一串二进制数,【码元】就是切分这个二进制数的方法。
- 编码空间被分成 17 个平面(plane),每个平面有 65,536 个字符(2个字节,16位)。0 号平面叫做「基本多文种平面」(BMP),涵盖了几乎所有你能遇到的字符,除了 emoji(emoji位于1号平面 - -)。其它平面叫做补充平面,大多是空的
- UTF-32 UTF-32也就是说它的码元是32位,每32位去读一下码点
- UTF-16 它的码元是16位的,也就是说每16位去读一下码点,获取码点的前16位数字,直到读取完成。
- BMP平面(plane0)中的每一个码点都直接与一个UTF-16 的码元一一映射。
- 其它平面里很少使用的码点都是用两个 16 位的码元来编码的
- UTF-8 使用一到四个字节来编码一个码点
- 从 0 到 127 的这些码点直接映射成 1 个字节,西文,都位于此段,该编码方式非常节约空间
- 接下来的 1,920 个码点映射成 2 个字节
- 在 BMP 里所有剩下的码点需要 3 个字节 对于中文,就位于此段,3个字节
- 参考
抽象类和接口的区别
- 抽象类
- 抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
- 抽象类不能被实例化,只能被继承
- 接口
- 接口是抽象类的延伸,可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
- 接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
- 接口的字段默认都是 static 和 final 的
- 区别
- 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
- 很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为
volatile
用于保持内存可见性(随时见到的都是最新值)和防止指令重排序 参考
java内存模型
- 主内存,工作内存
- 主内存:虚拟机中的一块内存,对应java堆中的对象实例数据
- 工作内存:每条线程自己的内存空间,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作,都必须在工作内存中进行,不能直接读取主内存中的变量,对应虚拟机栈中的部分区域
- 内存间的交互
- lock:作用于主内存,把变量标识为线程独占状态。
- unlock:作用于主内存,解除独占状态。
- read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
- load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
- use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
- assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
- store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
- write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。
- 可见性:当一条线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的
- 普通变量的值是线程间传递需要通过主内存来完成的,一个值在一个线程被修改,需要向主内存进行回写,另一条线程在去从主内存中进行读取操作,新值才会对另外一条线程可见
volatile保持可见性
- 关键字修饰的变量看到的随时是自己的最新值
- volatile的特殊规则就是:
- read、load、use动作必须连续出现。
- assign、store、write动作必须连续出现。
- 使用volatile变量能够保证:
- 每次读取前必须先从主内存刷新最新的值。
- 每次写入后必须立即同步回主内存当中。
- 注意:volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作,即:
- 基本类型的自增(如count++)等操作不是原子的。
- 对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的。
防止指令重排
指令重排序是cpu采用了允许将多条指令不按照程序规定的顺序分开发送给各相应电路单元来处理
volatile关键字通过“内存屏障”来防止指令被重排序 (只有在Happens-Before内存模型中才会出现指令重排)
volatile 读操作的性能消耗与普通变量几乎没有什么差别,写操作可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不返生乱序执行
来看一个单利模式 DCL(Double Check Lock,双重检查锁)
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}
它可以”抽象“为下面几条JVM指令:
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象
引用instance指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量:
private static volatile Singleton instance;
Thread类的sleep() yield() 和 wait()的区别?
- sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复
- wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
多线程如何保证线程安全
- 同步 Synchronized 参考
- 普通同步方法,锁是当前实例对象.JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法
- 静态同步方法,锁时当前类的Class对象。
- 代码块同步:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。使用monitorenter和monitorexit指令实现。
- 使用原子类(atomic concurrent classes) 如AtomicInteger等,AtomicInteger通过声明一个volatile value(内存锁定,同一时刻只有一个线程可以修改内存值)类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。
- 实现并发锁
- 使用volatile关键字
- 使用不变类和线程安全类
GC
找到被回收的对象
- 引用计数法
- 堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
- 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。
- 缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销
- 早期的JVM使用引用计数
- 可达性分析法
- 通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下继续寻找它们的引用节点,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
- GC Roots对象:虚拟机栈中引用的对象;方法区中类静属性引用的对象;方法区中常量引用的对象;本地方法中JNI引用的对象
- 真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
- 如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
回收算法
-
标记清除算法
- 首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象
- 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
- 缺点:
- 标记和清除过程的效率都不高。这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。
- 标记清除后会产生大量不连续的内存碎片
-
复制算法
- 它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
- 优点:
- (1)标记阶段和复制阶段可以同时进行。
- (2)每次只对一块内存进行回收,运行高效。
- (3)只需移动栈顶指针,按顺序分配内存即可,实现简单。
- (4)内存回收时不用考虑内存碎片的出现
- 缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
- 复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法
-
标记—整理算法
- 标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存
- 优点:
- (1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。
- (2)使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。
- 缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
分代收集
Java的堆内存划分:新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成
-
年轻代:
- 几乎所有新生成的对象首先都是放在年轻代的。
- 新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。
- 大部分对象在Eden区中生成。 当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。
- 回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。
- 当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。
-
年老代
- 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2)
- 当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高
- 一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组
-
持久代
- 用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
- 对永久代的回收主要回收两部分内容:废弃常量和无用的类。
- 永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。
-
堆内存分配策略
- 对象优先在Eden分配。
- 大对象直接进入老年代。
- 长期存活的对象将进入老年代。
-
分代的回收算法
- 新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
- 老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。由于老年代中的对象生命周期比较长,因此Major GC并不频繁。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
- 新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代
其他
-
垃圾回收执行时间
- GC分为Scavenge GC和Full GC。
- Scavenge GC :发生在Eden区的垃圾回收。
- Full GC :对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢
- 有如下原因可能导致Full GC:
- 1.年老代(Tenured)被写满;
- 2.持久代(Perm)被写满;
- 3.System.gc()被显示调用;
- 4.上一次GC之后Heap的各域分配策略动态变化.
- GC分为Scavenge GC和Full GC。
-
相关函数
- System.gc(),请求Java的垃圾回收。仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
- finalize()
- 在finalize()方法返回之后,对象消失,垃圾收集开始执行。
- finalize()的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。其他做法开辟的内存空间,例如:1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法,(2)打开的文件资源等
- 一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。
-
触发主GC的条件
- 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用
- Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
- 在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收
-
减少GC开销的措施
- 不要显式调用System.gc()。 这样会增加了间歇性停顿的次数。
- 尽量减少临时对象的使用。 少用临时变量就相当于减少了垃圾的产生,从而延长了出现第二个垃圾回收的时间,减少了主GC的机会。
- 对象不用时最好显式置为Null。 有利于GC收集器判定垃圾,从而提高了GC的效率。
- 尽量使用StringBuffer,而不用String来累加字符串。 String是固定长的字符串对象,累加String对象时,本质上是重新创建新的String对象
- 能用基本类型如Int,Long,就不用Integer,Long对象。 基本类型变量占用的内存资源比相应对象占用的少得多
- 尽量少用静态对象变量。 静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
- 分散对象创建或删除的时间。 集中在短时间内大量创建新对象,所需内存变多,会增加主GC的频率。集中删除对象会突然出现了大量的垃圾对象,会增加主GC的频率。
List,Set,Map的区别
- List:
- 1、可以允许重复的对象
- 2、可以插入多个null元素
- 3、是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序
- 4、常用的实现类有ArrayList,LinkedList和Vector。ArrayList提供了使用索引的随意访问,底层结构是数组,优查询劣增删,而LinkedList经常用于添加或删除元素的场合,底层结构是链表
- Set:
- 1、不允许重复的对象
- 2、无序容器,你无法保证每个元素的存储顺序,TreeSet通过Conparator或者Comparable维护了一个排序顺序
- 3、只允许一个null元素
- 4、Set常用的实现类是HashSet,LinkedHashSet以及TreeSet。最流行的是基于HashMap实现的HashSet,TreeSet还实现了SortedSet接口,因此TreeSet是一个根据其conpare()和compareTo()的进行排序的有序容器。
- Map:
- 1、Map不是collection的子接口或者实现类,Map是一个接口
- 2、Map的每个Entry都持有俩个对象,一个键一个值,可能会持有相同的值对象但键对象必须是唯一的。
- 3、TreeMap也通过Comparator或者Comparable维护了一个排序顺序
- 4、Map里你可以拥有任意个null,但只能有一个null键
- 5、常用的实现类HashMap,LinkedHashMap,Hashtable,TreeMap
HashMap
- 基于数组和链表实现的。当产生hash冲突的时候,会变成链表,1.8以后增加了红黑树,当链表中的数量达到8的时候,会转变成红黑树
- 它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储位置的,通过key的hashCode来计算hash值,然后通过hash值选择不同的数组来存储。通过链表来解决hash冲突的
- 数组的容量为什么是2的指数次幂?
- 运行效率问题,计算hashIndex的时候,会用到数组的长度,2的指数次幂可以使用位运算来执行计算
- 计算数组位置的时候使用的是 hash & (length - 1),如果length不是2的指数次幂的话,计算的结果就会大于length
- 不使用取余的方式,是因为效率问题
- 加载因子为什么是0.75? 时间和空间的折中取值
- 为什么是8的时候才会转换为红黑树?加入红黑树的其中一个主要原因是为了防止恶意使用带来的性能骤降。泊松分布,负载因子在0.75的时候,一个链表形成以后,新加的元素定位到同一个位置上的概率,随着数组长度的增加,概率会指数级下降,当长度达到8的时候,概率就非常小了。也就是说,产生红黑树的几率是很小的
- hash值的获取,为什么不是直接取hashCode,而是还经过了一层计算
- (h = key.hashCode()) ^ (h >>> 16)
- (length - 1) & hash
- 将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来,为啥要这样做呢?不如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征,当两个哈希码很接近时,那么这高区的一点点差异就可能导致一次哈希碰撞
- 使用异或运算的原因:异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢
- 为什么槽位数必须使用2^n : 为了让哈希后的结果更加均匀,槽位数不是16,而是17,即(17 - 1) & hash,会发现计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,通过位运算e.hash & (length - 1)来计算,a % (2^n) 等价于 a & (2^n - 1)
- 给定一个key,如何找到对应的value:if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){}
- fail—fast 机制:在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
- fail—safe 机制:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
- 具体的put方法的注释
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 当前位置为null,添加数据
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 两个值的hash值一样,key一样或者equals
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) // 红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 普通链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 最后一个数值为null
p.next = newNode(hash, key, value, null); // 插入到尾部
// 大于某个值的时候,将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判断是否有重复的数据
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果有相同的key,进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// fail-fast机制
++modCount;
// 判断是否已经达到阈值,进行扩容处理
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
ConcurrentHashMap
HashSet
- HashSet底层实际上是使用 HashMap 来作为存储结构.
- add方法 : public boolean add(E e) {return map.put(e, PRESENT)==null;}
- 本质上是调用了map的put方法,value值则为 PERSENT,HashSet类中的一个静态字段
- static final Object PRESENT = new Object();
- 由于hashSet只是关注key,不关注value,所有他的 equals 和 hashMap的部相同
- 和hashMap的区别
- HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对.
- HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法
ArrayList & LinkedList
-
ArrayList
- 底层使用数组实现,默认初始容量为10.当超出后,会自动扩容为原来的1.5倍
- 数组的扩容是新建一个大容量(原始数组大小+扩充容量)的数组,然后将原始数组数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。
- 采用了Fail-Fast机制
- remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC
- ArrayList不是线程安全的,多线程环境下可以考虑用CopyOnWriteArrayList
-
LinkedList
- 底层基于链表实现
-
ArrayList与LinkedList区别
- ArrayList的实现是基于动态数组是实现,LinkList是基于链表实现
- 对于随机访问get和set,ArrayList要优于LinkedList
- 对于新增和删除,LinkedList要由于ArrayList,此时ArrayList要移动数组,而在LinkedList当中,只需要有链表的改变
- LinkedList不支持高效的随机访问
- 空间浪费,ArrayList的结尾会预留一定的容量空间,LinkedList每一个元素都需要消耗一定的空间
https
- 如果数据全部用对称加密传输的话,黑客如果拿到了key,就会很轻易的破解了传输协议,数据被泄漏
- 如果非对称加密传输数据的话,客户端向服务端传递数据可以保证安全,但是服务端向客户端返回数据的时候,由于客户端只有公钥,所以服务端只能用私钥加密数据,此时黑客也是可以解密出数据的
- 对称和非对称加密一起使用的话:如果有中间人的话,中间人向客户端返回一个自己的公钥,然后中间人真正拿着服务端的公钥,此时客户端请求是,中间人可以解密,然后在用真正的公钥加密传递给服务端,服务端返回数据后,解密,然后用自己的私钥加密返回客户端,此时完全可以欺骗客户端
- 解决中间人攻击的话,可以使用CA认证,首先用ca的私钥,将服务端的公钥进行加密传递给客户端,客户端用自己本机内置的ca公钥解密服务端传递过来的公钥,可以正确解密,就可以认为是机构合法
- https的整体流程细节
- 1.客户端向服务器发起HTTPS请求,告诉服务端一些加密和版本信息
- 2.服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。
- 3.服务器将自己的公钥发送给客户端。
- 4.客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,即客户端密钥
- 5.客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。
- 6.服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。
- 7.然后服务器将加密后的密文发送给客户端。
- 8.客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。
Java动态代理
classloader
JVM内存模型,内存区域
死锁
- 概念:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西
- 场景:线程A持有锁localA并想获得锁localB的同时,线程B持有锁localB并尝试获得锁localA,那么这两个线程将永远地等待下去
- 当threadA开始执行run方法的时候,它会先持有对象锁localA,然后睡眠2秒,这时候threadB也开始执行run方法,它持有的是localB对象锁.当threadA运行到第二个同步方法的时候,发现localB的对象锁不能使用(threadB未释放localB锁),threadA就停在这里等待localB锁.随后threadB也执行到第二个同步方法,去访问localA对象锁的时候发现localA还没有被释放(threadA未释放localA锁),threadB也停在这里等待localA锁释放.就这样两个线程都没办法继续执行下去,进入死锁的状态
- 怎么避免
- 一个线程尽量每次只能获得一个锁
- 4个条件
- 互斥条件:线程要求对所分配的资源进行排他性控制,即在一段时间内某 资源仅为一个进程所占有.此时若有其他进程请求该资源.则请求进程只能等待.
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放).
- 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放.
- 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。