equal方法用于两个对象之间,检测一个对象是否等于另一个对象。
Object类中equal()的源码等价于通过“==”比较两个对象
但一般而言,我们使用equal的目的是为了比较两个对象的内容是否相同,因此,一般会重写equals()方法,来比较它们的内容是否相等。
hashCode()的作用是获取哈希码,也称为散列码,它实际上返回一个int整数,这个哈希吗的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java,每个对象都包含有hashCode()函数。
equal()
方法判断相等,那么它们的hashCode()
方法应该返回相同的值,如果只重写equals()
方法而没有重写hashCode
方法就会在使用哈希几重例如(HashSet
或者HashMap
)时,相等的对象被错误的认为是不相等的,因为哈希集合使用HashCode来决定对象在内部存储的位置。hashCode
,如果hashCode
不同,那么可以确定它们不相等。如果hashCode
相同,两个对象也可能不同,因为有哈希碰撞发生的可能性,但哈希碰撞发生的概率较低,使用hashCode
String对象是不可变的
,对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新值保存进去。
StringBuffer与StringBuilder都继承自AbstractStringBuilder
类,在AbstractStringBuilder中也是使用字符数组保存字符串的,这两种对象都是可变的。private final char value[]; // String
char[] value; // StringBuffer、StringBuilder
@Override
public synchronize StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
如果只是在单线程使用字符串缓冲区,那么StringBuilder的效率会更高些。需要考虑线程安全的,用StringBuffer。
静态内部类只能方法外部类的静态成员和静态方法
非静态内部类:不管是静态方法还是非静态方法都可以在非静态内部类中访问。
静态内部类和非静态内部类的主要不同:
overload
实现了编译时多态,同名函数根据不同的参数列表,表现出不同的形式。子类对象可以绑定到父类对象,并且通过override
重写父类的方法,实现运行时多态。方法重载overload
实现了编译时多态,同名函数根据不同的参数列表,表现出不同的形式。
子类方法对于父类方法的重写override
实现运行时多态,java运行 时系统根据该方法的实例的类型来决定选择调用哪个方法被称为运行时多态。
运行时多态存在的必要条件:
多态的转型分为向上转型和向下转型两种。
向上转型:多态本身就是向上转型的过程
使用格式: 父类类型 变量名 = new 子类类型();
向下转型:一个已经向上转型的子类对象,可以使用强制类型转换的格式,将父类引用类型,转为子类引用类型
使用格式:子类类型 变量名 = (子类类型) 父类类型的变量;
案例一:
class People{
public void eat(){
System.out.println("吃饭");
}
}
class Stu extends People{
@Override
public void eat(){
System.out.println("吃水煮肉片");
}
public void study(){
System.out.println("好好学习");
}
}
class Teachers extends People{
@Override
public void eat(){
System.out.println("吃樱桃");
}
public void teach(){
System.out.println("认真授课");
}
}
public class demo04 {
public static void main(String[] args) {
People p=new Stu();
p.eat(); // 吃水煮肉片
//调用特有的方法
Stu s=(Stu)p;
s.study();
s.eat(); // 吃水煮肉片
}
}
案例二
这个案例的第三个输出可能与我们所想不一致。
当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是被子类覆盖的方法但是如果强制把超类转为子类,就可以调用子类中新添加而超类中没有的方法
上边的第三条:a2.show(b)
,a2是按照多态方式得到的一个B类,入参b是B类,B继承自A,而A中并没有入参为B类的show()
,所有B中的两个show()
函数,入参为A类的是对父类的重写,入参为B的show函数是自己新有的方法。
那么根据上边的观点,a2只能调用在A中定义过的方法,那么B中这个入参为B的show函数是不会被调用的,因为它是子类新有的方法。因此最终调用的是入参为A
类的show函数,因为它可以由多态的方式来接收参数b
,最终的输出为 B and A
上边这个案例实际上还设计到方法调用的优先级问题,优先级由高到低依次为:
List
可以add
非A类型的对象。super可以理解是指向自己父类对象的一个指针,它主要有以下三种用法:
可见性
:
lock
前缀,相当于一个内存屏障,避免了指令重排优化,实现了有序性
。lock
前缀,相当于一个内存屏障
,防止了指令重排。内存屏障:memory barrier 能够让CPU或编译器在内存访问上有效。
内存屏障(也称内存棚栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
写操作
都要写回到主内存。读操作
都能获得内存屏障之前的所有写操作的最新结果。在重排序时,不允许把内存屏障之后的指令重排到内存屏障之前,对于一个volatile变量的写,先行发生于任意后续对于这个volatile变量的读,也叫写后读
。
synchronize解决的是执行控制的问题,保证同一时间只有一个线程可以执行被synchronized
修饰的代码。
java中synchronized底层主要依赖于对象监控器(也称为锁)和线程同步机制实现的:
synchronized
修饰的方法或者代码块时,它需要先获取该对象的监视器。线程安全的:
ConcurrentHashMap:是一种高效但是线程安全的集合
线程不安全的:
CopyOnWriteArrayList是开发过程中常用的一种并发容器,多用于读多写少的并发场景。
原理:当我们向容器添加或删除元素的时候,不直接往当前容器添加删除,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全。
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
public E remove(int index) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
}
}
因为读操作不会对当前容器做任何处理,所以我们可以对容器进行并发的读,而不需要加锁,即读写分离:
public E get(int index) {
return get(getArray(), index);
}
一般来讲,我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁,写操作加锁保证了线程安全,读写分离保证了读操作的效率。
CopyOnWriteArrayList的缺点:
测试一下,首先向CopyOnWriteArrayList里面塞10000个测试数据,启动两个线程,一个不断的删除元素,一个不断的读取容器中最后一个数据。
public void test(){
for(int i = 0; i<10000; i++){
list.add("string" + i);
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (list.size() > 0) {
String content = list.get(list.size() - 1);
}else {
break;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(list.size() <= 0){
break;
}
list.remove(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
CopyOnWriteArraySet的实现是借助CopyOnWriteArrayList实现的,只不过CopyOnWriteArraySet是在CopyOnWriteArrayList上使用indexOf不允许存入重复元素。
HashMap的默认构造函数:
int threshold; // 容纳键值对的最大值
final float loadFactor; // 负载因子
int modCount;
int size;
Node[] table 的初始化长度length(默认是16),loadFactor默认值为0.75,threshold是HashMap所能容纳的键值对的最大值,threshold = length × load factor,也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作。
0.75是对空间和时间效率的一个平衡选择,一般不需要修改,除非在时间和空间比较特殊的情况下:
首先根据key值计算出hashcode,然后根据hashcode计算出hash值,最后通过hash&(length - 1)计算得到实际存储的位置。
这里虽然用到的是&运算,但实际上还是在取模,因为length是2的幂,length - 1就是二进制位全部为1,与运算就能选择需要的低位数据
当链表元素个数大于等于8时,链表换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。
以JDK1.8为例:
在这里插入代码片
HashMap在容量超过了负载因子所定义的容量之后,就会扩容,java里的数组是无法自动扩容的,方法是将hashmap的大小扩大到原来的两倍,并将原来的对象放入到新的数组中。
先来看jdk1.7的源码:
void resize(int newCapacity) {
Entry[] oldtable = table;
int oldcapacity = oldtable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
transfer(newTable); //!!将数据转移到新的Entry数组里
table = newTable; //HashMap的table属性引用新的Entry数组
threshold = (int)(newCapacity * loadFactor);//修改阈值
}
transfer()方法将原有的Entry数组里的元素拷贝到新的Entry数组里。
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了旧的Entry数组
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍历的Entry数组
Entry<K,V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //标记[1]
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
}
newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头位置,这样先放在一个索引上的元素,最终会被放到Entry链的尾部。
jdk1.8做了两处优化
resize之后,元素的位置在原来的位置,或者原来的位置 + oldCap(原来哈希表的长度),不需要向jdk1.7那样,重新计算hash,只需要看看原来的hash值新增的那个bit是0还是1就好,是0的话索引没变,是1的话,索引变成了原索引+oldCap。因为n变为了两边,索引会增加1个bit位
,这个设计非常巧妙,省去了重新计算hash值的时间。
JDK1.7中rehash的时候,旧链表迁移到新链表,如果新表的数组索引位置相同,则链表会倒置,因为采用的是头插法,JDK1.8不会倒置,使用的是尾插法。
红黑树是一种非严格平衡二叉搜索树。
上边的性质保证了在红黑树的相对平衡性,从根节点到叶子节点的最长路径(黑红相间),不超过最短路径(全黑)的2倍,这就保证了树生长的一种相对平衡性,因此它的增删改查效率是O(logN)。
synchronized
保证插入时阻塞其他线程的插入操作,效率低下。JDK1.7的ConcurrentHashMap,把哈希桶分成小数组(Segment),每个小组有n个HashEntry。其中Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色,而HashEntry用于存储键值对数据。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树
结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized
实现更加低粒度的锁。
将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。
java保证并发安全的三大特性:
如何保证原子性:
通过synchronized关键字保证原子性
通过Lock保证原子性
通过CAS保证原子性
volatile关键字
内存屏障
synchronized关键性
Lock
final关键字
如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有操作无序。
程序执行的顺序按照代码的先后顺序执行,JVM存在指令重排,所以存在有序性问题。
如何保证有序性?
通过volatile关键字保证有序性
内存屏障
synchronized关键字
Lock
死锁必须具备以下四个条件:
SHUTDOWN
,线程池不再接受新任务,但是队列中的任务需要执行完毕。STOP
,线程池会终止正在运行的任务,并停止处理排队的任务,并返回正在等待执行的Listinterrupt
方法来中断线程,所以无法响应中断的任务可能无法终止。锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。
共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
CAS:全称compare and swap
,它是一条CPU同步原语,是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
两者的区别主要有如下:
两者都是可重入锁
可重入锁:也叫做递归锁
,可重入锁指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法又调用了另一个需要相同锁的方法,则线程可以直接执行调用的方法,而无需重新获得锁。
两者都是同一个线程每进入一次,锁计数器自增1,等待锁计数器降为0,释放锁。
synchronized依赖于JVM,而reentrantlock依赖于API
ReentrantLock 比 synchronized 增加了一些高级功能
synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置
。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
的持有权。其内部包含一个计数器
,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令
后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED,访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候
threadid 为空,jvm 让其持有偏向锁
,并将 threadid 设置为其线程 id,再次进入的时候会先判断
threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级 锁
,通过自旋循环一定次数来获取锁
,执行一定次数之后,如果还没有正常获取到要使用的对象,此时
就会把锁从轻量级升级为重量级锁
,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。
在 Java 6 之后优化 synchronized 的实现方
式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
ThreadLocal:直译为线程本地变量。
如果想实现每个线程都拥有自己的专属本地变量就用到了ThreadLocal类。ThreadLocal类主要解决的是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成一个存放数据的盒子,盒子里面可以存储每一个线程的私有数据。
ThreadLocal内存结构图:
由结构图可以看出:
每个Thread对象持有一个ThreadLocalMap类型的成员变量,ThreadLocalMap中每个Entry的key是TreadLocal本身,value是ThreadLocal的泛型值。
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收
的时候,key 会被清理掉,而 value 不会被清理掉
。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露
。
解决方案:ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法
AQS 的全称为(AbstractQueuedSynchronizer,抽象队列同步器),是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器, 比如我们提到的 ReentrantLock(可重入锁),Semaphore(信号量),CountDownLatch(倒计时锁)都是基于AQS实现的。
AQS中维护了一个用volatile修饰的state
变量和一个现代等待队列(多线程争用资源被阻塞时会加入到此队列)
state操作是通过CAS来保证其修改的线程安全性,根据state的字段来决定是否能够获得锁。
池化技术:线程池、数据库连接池、http连接池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了一种限制、管理资源的策略。每个线程池还维护一些基本统计信息,例如已经完成任务的数量。
使用线程池的好处:
CPU核心数
)+ 1,比CPU多出来一个线程是为了防止线程偶发的缺页中断
,或者其他原因导致的任务暂停
而带来的影响,一旦任务停止,CPU就会处于空闲状态,这种情况下,多出来的这一个线程就可以充分利用CPU的空闲状态。
固定线程池和单例线程池,默认使用的阻塞队列是容量为Interger.MAX_VALUE的LinkedBlockingQueue,可以认为是无界队列。由于FixedThread的线程数固定,所有当任务特别多时,需要一个没有容量限制的阻塞队列存放任务。
SynchronusQueue
缓存线程池使用的阻塞队列为SynchronousQueue,缓存线程池的最大线程数是Interger的最大值,可以认为线程数是无限扩展的,所以缓存线程池的情况与上边正好相反,因为一旦有任务就可以创建新的线程,而不需要额外保存。
DelayedWorkQueue
第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。
DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和SingleThreadScheduledExecutor 选择DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。
源码中ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参
数有关,每个worker会while死循环从阻塞队列中取数据,通过置换worker中Runnable对象,运行
其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。
java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯,多个线程之间是不能通过直接传递数据交互的,他们之间交互只能通过共享变量来实现。
==JMM的主要目的是定义程序中各个变量的访问规则。==java线程之间的通信由JMM控制。JMM定义了JVM在计算机内存(RAM)中的工作方式,如果想深入理解java并发编程,就要先理解好java内存模型。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中的变量不是完全等同的。这里的变量指的是实例字段、静态字段、构成数组对象的元素,但不包括局部变量与方法参数,因为它们是线程私有的,不会被共享,自然就不会存在竞争的问题。
Java内存模型定义了8种方法来完成主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存。虚拟机实现的时候,必须每一种操作都是原子的、不可再分的。
lock
(锁定):作用于主内存
的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量unlock
(解锁):作用于主内存
的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定read
(读取):作用于主内存变量
,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用。load
(载入):作用于线程的工作内存
的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)use
(使用):作用于线程的工作内存
中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作assign
(赋值):作用于线程的工作内存
的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作store
(存储):作用于线程的工作内存
中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用write
(写入):作用于主内存
的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中如果要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,如果要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。只需要保证相对顺序,不要求连续,下边两种执行结果是一样的:
在执行这8中操作的时候必须遵循如下的规则
JDK1.7中,将1.6中永久代的字符串常量池和静态变量等移动到了堆中。
JDK1.8完全放弃了“永久代”,改用了在本地内存中实现的“元空间”,将1.7中永久代剩下的部分(主要是类型信息)移动到了元空间(放在内存中)。
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
线程私有
,是一块很小的内存空间,作为当前线程的执行的代码行号指示器,用于记录当前虚拟机正在执行的线程指令地址。线程私有
,每个方法执行的时候创建一个栈帧,用于存储局部变量表,操作数、动态链接和方法返回等信息,当栈深度超过了虚拟机允许的最大深度,就会抛出StackOverFlowError
线程私有
,保存的是native方法的信息,当一个JVM创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法。除了程序计数器,其他内存区域都有OOM(out of memory, 内存溢出)的风险。
排查OOM的方法:
JVM常量池主要分为Class文件常量池、运行时常量池、全局字符串常量池、以及基本数据类包装类对象常量池。
Java6和6之前,常量池是存放在方法区(永久代)中的。
Java7,将常量池是存放到了堆中。
Java8之后,取消了整个永久代区域,取而代之的是元空间
。运行时常量池和静态常量池存放在元空间中
,而字符串常量池依然存放在堆中。
两种浮点数类型的包装类则没有实现
。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池
,也即对象不负责创建和管理大于127的这些类的对象。分为两种算法:
循环引用的问题
,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
对于限定单个CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java 虚拟机运行在Client 模式下默认的新生代垃圾收集器。
ParNew垃圾收集器是很多java虚拟机运行在Server 模式下新生代的默认垃圾收集器。
和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;
CMS收集器是第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
final关键字:
/**
* @author yangwuyi
*/
public class FinalTest {
public static final List TEMPEXM = new ArrayList();
public static void main(String[] args) {
FinalTest.TEMPEXM.add("hello ");
FinalTest.TEMPEXM.add("world !");
System.out.println(FinalTest.TEMPEXM);
}
}
static关键字:
final变量不可变的是句柄的地址,其实里面的值可以改变
因为s是static final变量,且它等于helloWorld,在编译的时候就可以知道它的值,所以直接访问s的值不会引起Final类的初始化。作为表现,也就是static静态代码块不会被加载。
虚拟机把描述类的数据加载到内存里,并对数据进行校验、解析和初始化
,最终变成可以被虚拟机直接使用的class对象。
类的整个生命周期包括:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:
双亲委派机制,是按照加载器的层级关系,逐层进行委派。例如要加载一个类MyClass.class,从低层级到高层级一级一级委派,先由应用层加载器委派给扩展类加载器,再由扩展类委派给启动类加载器;启动类加载器载入失败,再由扩展类加载器载入,扩展类加载器载入失败,最后由应用类加载器载入,如果应用类加载器也找不到那就报ClassNotFound异常了。
双亲委派机制的优点:
打破双亲委派机制的方式:
通过构建自定义类加载器重写loadClass()
方法来破坏委托加载的逻辑。在loadClass()
方法中,默认的委派规则是将加载请求先委托给自己的父类,我们可以根据自己的需求决定是否由父类加载器加载还是自己直接加载类。
以下是一些常见的打破双亲委派机制的例子: