java+jvm笔记

JUC

synchornized原理(java锁机制)!!!!!!

在这里插入图片描述升级顺序:

  1. 无锁
  2. 偏向锁,只有一个线程来访问
  3. 轻量级锁,有两个线程交替访问
  4. 重锁,两个及以上线程同时进行访问

synchronized修饰不同内容时谁是锁

锁就是线程进入时要拿要等待的东西,

  1. 如果synchronized修饰的是实例方法,对应的锁是当前实例方法所属对象的实例(线程A调用该方法时,假如还有另一个实例方法也被synchronized修饰,那么线程A完成前,线程B都调用不了这两个方法)
  2. 如果synchronized修饰的是静态方法,对应的锁是类的class对象(线程A调用该方法时,那么线程A完成前,线程B都调用不了这个方法)
  3. 如果synchronized修饰的是代码块,对应的锁是传入到synchronized的对象实例
    在这里插入图片描述

synchronized对应的字节码

synchronized锁代码块对应的字节码
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("Method 1 start");}}
}

在这里插入图片描述
其实就是monitorenter和monitorexit两条jvm指令

synchronized方法对应字节码
public class SynchronizedMethod {public synchronized void method() {System.out.println("Hello World!");}
}

在这里插入图片描述同步方法通过flags设置为ACC_SYNCHRONIZED来实现,底层本质还是调用了monitorenter和monitorexit两条jvm指令(所以有人说这种是隐式调用)

MarkWord

java对象在JVM的c++层面其实也是用一个对象来表示,其在堆内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。

Klass Word指向该java对象所属java类的类的元信息即平时java里面用的.class对象的地址
MarkWord部分就是装着各种flag
实例数据部分就是具体成员变量的值

在这里插入图片描述
Markword 是保存锁状态的关键,对象锁状态可以从偏向锁升级到轻量级锁,再升级到重量级锁,加上初始的无锁状态,可以理解为有 4 种状态。想在一个对象中表示这么多信息自然就要用位来存储。下图只有一行会成立(第一行描述结构,state列是注释实际不存在)即处于不同state时Markword保存的东西不同,

需要注意!在jdk1.6之前只有第二行和第五行两种,后面这四种都有了,因为1.6之前没有优化只有无锁和重量级锁两种。

在这里插入图片描述

  • hashcode:25位,对象的Hash码
  • age:对象分代年龄占4位 biased_lock:偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁
  • thread:持有偏向锁的线程ID,占23位 epoch:偏向时间戳,占2位
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

而且我们可以通过后几位的值,来判断是哪一种锁的等级

  1. 后三位是001表示无锁
  2. 后三位是101表示偏向锁
  3. 后两位是00表示轻量级锁
  4. 后两位是10表示重量级锁

三大锁的上锁放锁的对象和字段

偏向锁:cas把锁对象的markword的threadID换成当前线程
轻量级锁:cas把锁对象的markword的lock record地址字段指向当前线程栈帧新生成的lock record且该lock record指向锁对象
重量级锁:cas把monitor对象的owner字段换成当前线程

放锁就是反过来

偏向锁

应用场景

只有单个线程反复执行同步代码块。该状态只能由jvm延迟后触发,我们无法手工进入

原理

匿名偏向状态是偏向锁的初始状态,在这个状态下第一个试图获取该对象的锁的线程,会使用CAS操作(汇编命令CMPXCHG)尝试将自己的threadID写入对象头的mark word中,使匿名偏向状态升级为已偏向(Biased)的偏向锁状态。在已偏向状态下,线程指针threadID非空,且偏向锁的时间戳epoch为有效值。如果之后有线程再次尝试获取锁时,需要检查mark word中存储的threadID是否与自己相同即可,如果相同那么表示当前线程已经获得了对象的锁,不需要再使用CAS操作来进行加锁,从这里可以看出其场景下高效之处,即开销只有 开头单次cas+重入次数的if(检查threadID)

当前线程如果不是原本使用偏向锁的线程,那么将执行CAS操作(旧值指定threadID为空),试图将当前线程的ID替换mark word中的threadID。只有当对象处于下面两种状态中时,才可以执行成功:

  • 对象处于匿名偏向状态
  • 对象处于可重偏向(Rebiasable)状态,新线程可使用单次CAS将threadID指向自己

如果对象不处于上面两个状态,说明锁存在线程竞争,在CAS替换失败后会执行偏向锁撤销操作。偏向锁的撤销需要等待全局安全点Safe Point(安全点是 jvm为了保证在垃圾回收的过程中引用关系不会发生变化设置的安全状态,在这个状态上会暂停所有线程工作),在这个安全点会挂起获得偏向锁的线程。

在暂停线程后,会通过遍历当前jvm的所有线程的方式找到并检查持有偏向锁的线程状态是否存活:

  • 如果线程还存活,且线程正在执行同步代码块中的代码,则升级为轻量级锁
  • 如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:
    • 不允许重偏向,则撤销偏向锁,将mark word升级为轻量级锁,进行CAS竞争锁
    • 允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向新线程
缺点

一旦两个及以上的线程执行,比如现在第二个线程也要执行该同步代码块,则开销巨大特别是因为需要等到全局安全点撤销偏向锁变成无锁状态,然后升级为轻量级锁,所以后面jdk废弃了偏向锁而且已经出现了concurrent一开始只有hashtable里面方法很多都是同步的

mark word中的hashcode问题

hashcode是懒加载的,只有主动调用才会计算然后填入。偏向锁状态刚好就是通过占用原本mark word中的hashcode位实现的(变成threadID和epoch)且不同于轻量级锁(有lock record来存原本的mark word)和重量级锁(monitor对象有字段来存原本的mark word),所以如果当前是偏向锁而又调用了hashcode就会先撤回成无锁,此时必有一个线程来那就必然升级成另外两种锁于是就有位置装hashcode了,

批量重偏向和批量撤销
  1. 批量重偏向和批量撤销是针对整个类的所有对象的优化。
  2. 偏向锁重偏向一次之后不可再次重偏向。
  3. 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的线程竞争,剥夺了该类的新实例对象使用偏向锁的权利,只能走无锁到轻量锁到重量锁的路径
可重偏向(批量重偏向)

上面提到这种状态下也可以直接cas。

场景

当一个线程A建立了大量对象,并且对它们执行完同步操作解锁后(走了一遍),于是这些对象都处于偏向锁状态,此时若再来另一个线程B则也会尝试获取这些对象的锁,那么这一大堆偏向锁状态的对象都需要先撤销偏向锁,开销爆炸,针对这种情况jvm会进行优化即偏向锁的 批量重偏向(Bulk Rebias)。当触发批量重偏向后,第一个线程结束同步操作后的锁对象当再被同步访问时会被重置为可重偏向状态,以便允许快速重偏向,这样就不必撤销偏向锁以及再升级为轻量级锁的性能消耗,而是重新利用偏向锁(cas threadID即可)。

批量重偏向是 以class为单位 的而不是对象,即每个类的class对象会维护一个偏向锁的撤销计数器,每当该类对象发生偏向锁的撤销时,该计数器会加一,当这个值达到默认阈值20时,jvm就会认为这个锁对象不再适合原线程,因此进行批量重偏向。而距离上次批量重偏向的25秒内,如果撤销计数达到40,还会发生 批量撤销 ,如果超过25秒,那么就会重置在[20, 40)内的计数。下面是实例:

private static Thread t1,t2;
public static void main(String[] args) throws InterruptedException {      TimeUnit.SECONDS.sleep(5);List<Object> list = new ArrayList<>();for (int i = 0; i < 40; i++) {list.add(new Object());}t1 = new Thread(() -> {for (int i = 0; i < list.size(); i++) {synchronized (list.get(i)) {}}LockSupport.unpark(t2);});t2 = new Thread(() -> {LockSupport.park();for (int i = 0; i < 30; i++) {Object o = list.get(i);synchronized (o) {if (i == 18 || i == 19) {System.out.println("THREAD-2 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());}}}});t1.start();t2.start();t2.join();TimeUnit.SECONDS.sleep(3);System.out.println("Object19:"+ClassLayout.parseInstance(list.get(18)).toPrintable());System.out.println("Object20:"+ClassLayout.parseInstance(list.get(19)).toPrintable());System.out.println("Object30:"+ClassLayout.parseInstance(list.get(29)).toPrintable());System.out.println("Object31:"+ClassLayout.parseInstance(list.get(30)).toPrintable());
}

分析上面的代码,当线程t1运行结束后,数组中所有40个对象的锁都偏向t1,然后t1唤醒被挂起的线程t2,线程t2尝试获取前30个对象的锁。我们打印线程t2获取到的第19和第20个对象的锁状态。

线程t2在访问前19个对象时这19个对象的偏向锁会升级到轻量级锁,在访问后11个对象(下标19-29)时,因为偏向锁撤销次数达到了20,会触发批量重偏向,这11个对象的锁状态变为偏向线程t2。

在全部线程结束后,再次查看第19、20、30、31个对象锁的状态:

线程t2结束后,第1-19的对象释放轻量级锁变为无锁不可偏向状态,第20-30的对象状态为偏向锁、但从偏向t1改为偏向t2,第31-40的对象因为没有被线程t2访问所以保持偏向线程t1不变。

批量撤销
场景

该类所有实例的偏向锁,避免未来的偏向锁尝试,只能走无锁到轻量锁到重量锁的路径

private static Thread t1, t2, t3;
public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(5);List<Object> list = new ArrayList<>();for (int i = 0; i < 40; i++) {list.add(new Object());}t1 = new Thread(() -> {for (int i = 0; i < list.size(); i++) {synchronized (list.get(i)) {}}LockSupport.unpark(t2);});t2 = new Thread(() -> {LockSupport.park();for (int i = 0; i < list.size(); i++) {Object o = list.get(i);synchronized (o) {if (i == 18 || i == 19) {System.out.println("THREAD-2 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());}}}LockSupport.unpark(t3);});t3 = new Thread(() -> {LockSupport.park();for (int i = 0; i < list.size(); i++) {Object o = list.get(i);synchronized (o) {System.out.println("THREAD-3 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());}}});t1.start();t2.start();t3.start();t3.join();System.out.println("New: "+ClassLayout.parseInstance(new Object()).toPrintable());
}

对上面的运行流程进行分析:

  • 线程t1中,第1-40的锁对象状态变为偏向锁
  • 接着线程t2运行,线程t2中,第1-19的锁对象撤销偏向锁升级为轻量级锁,然后对第20-40的对象进行批量重偏向
  • 线程t3中,首先直接对第1-19个对象竞争轻量级锁,而从第20个对象开始往后的对象不会再次进行批量重偏向(规定:重偏向只有一次机会),因此第20-39的对象进行偏向锁撤销升级为轻量级锁,于是这时t2和t3线程一共执行了40次的锁撤销,触发锁的批量撤销机制,开始对偏向锁进行批量撤销成无锁又因为只能走无锁到轻量级锁到重量锁的路径于是开始升级

轻量级锁

应用场景

线程间交替访问同步代码块,即每个线程申请锁的时候只有自己一个线程没有竞争,而其他线程还在执行同步代码块外的剩余部分(同步代码块包裹的内容执行超快,而且同步代码块外还有要执行的内容)。

原理
  1. 申请轻量级锁时(只有两种情况:当前锁对象处于无锁状态或者偏向锁状态,而且锁对象的轻量级锁使用完会变回无锁,下次线程申请也是直接拿轻量级锁而非偏向锁,因为前面说过偏向锁只能靠jvm开局默认设置),jvm首先将在当前线程的栈帧中创建一条锁记录(lock record),其有两个字段:
  • displaced mark word(置换标记字):存放锁对象当前的mark word的拷贝(因为无锁之外的状态锁对象的markword都会被cas掉,所以要有地方存着)
  • owner指针:指向当前的锁对象的地址
  1. 在拷贝 mark word 完成后,首先线程CAS尝试将对象的 mark word 中的 lock record 指针指向栈帧中的锁记录(上锁),并将锁记录中的 owner 指针指向锁对象的 mark word
    在这里插入图片描述
    在这里插入图片描述
  • 如果CAS替换成功,表示竞争锁对象成功,则将锁标志位设置成 00,表示对象处于轻量级锁状态,执行同步代码中的操作,完成后cas撤销锁cas恢复锁对象的lock record地址字段为空且从此之后只走无锁-》轻量级锁-》重量级锁的路径
  • 如果CAS替换失败,则判断当前锁对象的mark word是否指向当前线程的栈帧中的lock record:
    • 如果是则表示当前线程已经持有对象的锁,执行的是synchronized的锁重入(for:synchronized(obj))过程,可以直接执行同步代码块。轻量级锁的每次重入,都会在栈中生成一个lock record,但是保存的数据不同:

首次分配的lock record,displaced mark word存储锁对象的正常mark word,owner指针指向锁对象
之后重入时在栈中分配的lock record中的displaced mark word为空,只存储了指向锁对象的owner指针在这里插入图片描述
重入的次数等于该锁对象在栈帧中lock record的数量,这个数量隐式地充当了锁重入机制的计数器。这里需要计数的原因是每次解锁都需要对应一次加锁,只有最后解锁次数等于加锁次数时,锁对象才会被真正释放。在释放锁的过程中,如果是重入则删除栈中的lock record,直到首次拿锁时的lock record则使用CAS还原锁对象的mark word然后删除

    • 否则说明该其他线程已经持有了该对象的锁,那就直接膨胀升级重量级锁,即轻量锁一旦发现的线程竞争那就直接升级而非很多人说的先自旋(源码来看只有重量级锁阶段才会进行自旋)

重量级锁

应用场景

线程并发量较大就开始上重量级锁了。在轻量级锁状态下,如果发现当前线程间已经不是交替访问而是同时竞争同一锁对象,则触发升级成重量级锁。

原理

由两部分处理逻辑组成:自旋+monitor阻塞唤醒。

自旋在这里的意义是,如果每条线程都可以很快拿到锁那就免去阻塞唤醒的耗时流程了

重量锁的逻辑

当前线程自旋若干次尝试上锁,失败了就阻塞,等其他拿到锁的线程释放锁后会唤醒一个线程,当前线程就有机会被唤醒(具体而言是按照阻塞顺序的逆序唤醒),那就继续自旋,一直循环 自旋->阻塞->唤醒->自旋 直到拿到锁。
这里即使当前线程被唤醒且也只唤醒了这唯一的一个线程也不一定能拿到锁,因为其他新线程刚来也会尝试上锁于是又发生竞争,新线程拿锁失败了才会进入阻塞队列

下图是根据源码得到的重量锁上锁放锁流程
在这里插入图片描述在这里插入图片描述

自旋和自适应自旋

上面提到重量级锁才会自旋,自旋通常默认跟着cas操作,因为自旋本身就是循环尝试cas直到操作成功的意思,自旋=循环cas

自旋

while循环里面cas,直到cas成功或者达到自旋次数上限才退出break

共享变量 shared_value = 0# 实际上这个函数是一条汇编指令cmpsxchg
# 由cpu层面实现原子性的cas,这里只是模拟
def compare_and_swap(expected, new_value):if shared_value == expected:shared_value = new_valuereturn True  # CAS 成功return False  # CAS 失败# 工作线程
def worker():# 普通自旋就是固定次数而不是死循环了# 这里只是为了方便演示用了死循环while True:expected = shared_value  # 读取当前值new_value = expected + 1  # 计算新值if compare_and_swap(expected, new_value):return  # CAS 成功后,直接返回return False  # CAS 失败
自适应自旋

这里自旋又有两种,一种是固定自旋次数的,另一种是自适应的(新版本jdk的做法,其实就是线程每次通过自旋拿到锁则都会导致下次自旋次数增加,这很好理解,线程能拿到锁说明很有可能下次自旋一会也能拿到,就没必要那么快就阻塞,允许自旋等待持续相对更长时间,直到下次自旋完都没拿到才走阻塞唤醒的流程)

Monitor

译为「监视器」或「管程」,是JVM即c++层面的一个对象,Monitor对象用于重量级锁的实现,jdk1.6之前monitorenter和monitorexit都是直接操作monitor而没有加入优化的轻量的锁逻辑
在这里插入图片描述

  • 刚开始 Monitor 中 Owner 为 null。
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner。
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED。
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是随机挑选的。

每一个 JAVA 对象都会与一个监视器 Monitor 互相关联(双方都有字段存储对方的地址)一一对应,Monitor 和对象一起创建、销毁。重量级锁的状态下,对象的 Mark Word 为堆中 Monitor 对象的地址。

Monitor在jvm的实现源码:

ObjectMonitor() {_header       = NULL; // 对象头的MarkWord_count        = 0; // 线程获取该锁的次数_waiters      = 0; // 调用wait方法后等待的线程数_recursions   = 0; // 重入计数器(加锁线程的重入次数)_object       = NULL; // 关联的对象(和 _header 的对象相同,比如synchronized括号里的对象)_owner        = NULL; // 占用当前锁的线程_WaitSet      = NULL; // 调用wait方法后等待的ObjectWaiter链表_WaitSetLock  = 0 ;   // 操作WaitSet链表的锁_Responsible  = NULL ;_succ         = NULL ; // 当前线程释放锁后,下一个执行的线程_cxq          = NULL ; // 多线程竞争锁时的单向链表(cxq链表头节点)FreeNext      = NULL ;_EntryList    = NULL ; // 处于等待锁block状态的线程节点会被加入该链表(EntryList链表头节点)_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;}
Monitor为什么是重量级锁

因为monitor通过(线程拿不到monitor使用权时)阻塞线程和(线程执行完释放monitor使用权时)唤醒线程来实现,而这两个操作函数都是内核级别的,需要通过中断int来调用,这就需要用户态和系统态的中断上下文切换(具体开销为,把用户栈切换为内核栈然后将当前寄存器保存,然后去中断向量表找对应中断处理程序的地址然后执行最后恢复上下文)了。

这样讲还是不能体现重量级,后面jdk1.6的偏向锁和轻量级锁轻量就体现在(单线程和线程交替进入同步代码块的场景下)无需创建monitor对象,也无需阻塞唤醒线程(即无中断上下文切换),而是利用java对象在jvm中的对应对象的一些字段和cas操作实现锁的争抢,当然了,正常并发高的场景还得是重量级锁的

锁升级全流程!!!!!!!!!!!
  1. 被synchronized的对象在刚被创建时,根据jvm的配置对象只可能会处于 无锁匿名偏向 两个状态。( 旧版本jvm默认会有4秒的偏向锁开启的延迟时间,在这个偏向延迟内对象处于为无锁态。如果关闭偏向锁启动延迟、或是经过4秒且没有线程竞争对象的锁,那么对象会进入匿名偏向状态,可通过-XX:BiasedLockingStartupDelay=修改延迟时间,这个4s不去升级为偏向锁是因为JVM启动的内部代码有很多地方也用到了synchronized且明确这些地方必然存在线程的竞争,那就没必要等全局安全点撤销偏向锁再逐步升级,as内无锁就开始发生竞争就直接升级锁,而且偏向锁只能在jvm开启且延迟到了后才会出现,如果无锁又发生竞争按照jvm源码就会直接升级为轻量级锁后续走重量锁升级)
    在目前的基础上,可以用流程图概括上面的过程:
    在这里插入图片描述额外注意一点就是匿名偏向状态下,如果调用系统的hashCode()方法,会使对象回到无锁态,并在mark word中写入hashCode

  2. 接着如果当前有一个线程来执行了,如果锁对象无锁则跳去4.走轻量级锁和重量级锁这套流程。那如果锁对象处于偏向锁状态:

来到这说明锁对象处于偏向锁状态,只能是 已偏向匿名偏向 两个状态:

  1. 如果该线程本来就已经拿到了偏向锁即锁对象的threadID指向该线程即锁对象偏向自己(这说明该线程是第一个线程且该线程重入synchronized锁住的代码块了比如for循环:{synchronized(obj)}),那就直接自增重入次数然后返回不做任何操作(偏向锁快之处)return;
  2. 走到这里说明当前要么偏向了别的线程(比如第一个线程来的时候),要么是匿名偏向(比如第二个线程来的时候)。
    这时候jvm先生成一个匿名偏向锁的mark word即threadID空但处于偏向锁状态,以此作为cas的旧值,然后又根据生成的匿名偏向锁将其threadID修改为当前线程的ID,以此作为cas的新值,接着进行一次cas。
    (这样的话只要第一个线程来过,第二个线程的一次cas尝试必然失败,因为这时候锁对象的mark word的threadID必然是第一个线程的不再是匿名的即必不等于我们给定的旧值,也就必然触发偏向锁升级为轻量级锁,即不存在轮流使用偏向锁这种说法???其实有重偏向这种东西但还没研究)
    这样如果这次cas成功了意味着当前是匿名偏向那就相当于把锁对象偏向自己然后return;,反之意味着偏向了别的线程那就说明发生竞争,需要进入全局安全点(耗时)进行偏向锁撤销,然后持有偏向锁的线程此时升级为轻量级锁,接着线程之间开始按照轻量级锁的原则来抢锁,即循环尝试cas拿锁(全局安全点是jvm为了保证在垃圾回收的过程中引用关系不会发生变化设置的安全状态,在这个状态上会暂停所有线程工作只留下VM线程,在暂停线程后,会通过遍历当前jvm的所有线程的方式找到并检查持有偏向锁的线程状态是否存活:
    如果线程还存活,且线程正在执行同步代码块中的代码,则升级为轻量级锁。如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:不允许重偏向,则撤销偏向锁,将mark word升级为轻量级锁,进行循环CAS竞争锁;如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向新线程完成上面的操作后,恢复暂停的线程,从安全点继续执行代码)
monitorenter源码(jdk1.6后,加入轻量的锁)

BasicObjectLock就是传说中的Lock record,记录锁对象地址以及暂存锁对象的mark word(displaced mark word),会被存储在当前线程的当前方法的栈帧中

class BasicLock {privatevolatile markOop _displaced_header;  // 被替换的对象头部放在这里
};
class BasicObjectLock {      // 对象处于栈帧中private:BasicLock _lock;      // 基础锁对象oop  _obj;       // 锁对象的引用
};
CASE(_monitorenter): {// 获取锁对象oop lockee = STACK_OBJECT(-1);// 类似于找到提前生成的缓存的若干个BasicObjectLock对象中的空对象BasicObjectLock* entry = find_free_lock_or_existing(istate, lockee);// 一般都是成功的if (entry != NULL) {entry->set_obj(lockee); // 绑定锁对象bool success = false;// 1. 偏向锁逻辑// 获取锁对象mark word检查是否处于偏向锁状态markOop mark = lockee->mark();if (mark->has_bias_pattern()) {uintptr_t thread_ident = (uintptr_t)istate->thread();uintptr_t anticipated_bias = calculate_bias_value(lockee, thread_ident, mark);// 当前锁对象处于偏向锁状态,开始检查当前线程是否持有该锁对象的偏向锁// 当前线程已持有偏向锁则直接放行不用看后面了if (anticipated_bias == 0) {success = true;} // 偏向锁被第一个来的线程持有,相当于当前线程是后来者了,此时发生竞争了else if (anticipated_bias & markOopDesc::biased_lock_mask_in_place) {// 尝试来一次cas撤销偏向锁if (lockee->cas_set_mark(lockee->klass()->prototype_header(), mark) == mark) {}} // 偏向锁为匿名偏向,因为第一个线程走完了同步块,但他是不会修改回无锁的只会把threadID改为空的,状态仍是偏向锁,又或者因为偏向锁默认延迟若干秒后自动开启,此时就变为偏向锁的状态但又还没指定threadIDelse {if (attempt_rebias(lockee, thread_ident, mark)) {success = true;}}}// 2. 轻量级锁逻辑if (!success) {markOop displaced = lockee->mark()->set_unlocked();entry->lock()->set_displaced_header(displaced);if (lockee->cas_set_mark((markOop)entry, displaced) != displaced) {if (THREAD->is_lock_owned(displaced->clear_lock_bits())) {// 轻量级锁重入entry->lock()->set_displaced_header(NULL);} else {// 竞争升级为重量级锁CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);}}}} else {// 没找到空闲Lock record对象// 未找到空闲锁,重新分配istate->set_msg(more_monitors);}
}
monitorenter源码(jdk1.6前,只有重量锁)

Atomic::cmpxchg_ptr (Self, &_owner, NULL),这行代码底层由cpu提供的一条汇编指令,由cpu保证的原子性(一些总线规划),我们只负责调用,具体意思是如果第二个参数给定的地址的值等于第三个参数,则把该地址的值修改为第一个参数,具体在这里意思是如果锁对象的&_owner

void ATTR ObjectMonitor::enter(TRAPS) {// Self是当前线程Thread * const Self = THREAD ;// cur用于记录CAS操作即cmpxchg_ptr后的返回值,void * cur ;// 通过原子CAS操作(底层是一条cpu提供的汇编指令,由cpu实现的原子性)尝试把monitor的_owner字段设置为当前线程,具体而言,Atomic::cmpxchg_ptr会返回第二个参数操作前的值,cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;// CAS操作成功,因为这表示CAS操作前if (cur == NULL) {// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.assert (_recursions == 0   , "invariant") ;assert (_owner      == Self, "invariant") ;// CONSIDER: set or assert OwnerIsThread == 1return ;}// 线程重入,recursions++ if (cur == Self) {// TODO-FIXME: check for integer overflow!  BUGID 6557169._recursions ++ ;return ;}// 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程if (Self->is_lock_owned ((address)cur)) {assert (_recursions == 0, "internal state error");_recursions = 1 ;// Commute owner from a thread-specific on-stack BasicLockObject address to// a full-fledged "Thread *"._owner = Self ;OwnerIsThread = 1 ;return ;}//省略一些代码。。。。// TODO-FIXME: change the following for(;;) loop to straight-line code.for (;;) {jt->set_suspend_equivalent();// cleared by handle_special_suspend_equivalent_condition()// or java_suspend_self()// 如果获取锁失败,则等待锁的释放;EnterI (THREAD) ;if (!ExitSuspendEquivalent(jt)) break ;//// We have acquired the contended monitor, but while we were// waiting another thread suspended us. We don't want to enter// the monitor while suspended because that would surprise the// thread that suspended us.//_recursions = 0 ;_succ = NULL ;exit (false, Self) ;jt->java_suspend_self();}Self->set_current_pending_monitor(NULL);}

wait和notify

代码中可以根据具体业务调用wait方法使得当前拿到锁的线程释放锁(比如不满足if)并把当前线程放入monitor的waitset等待队列,只有别的线程notify的时候才会回到entrylist即等上锁的阻塞队列否则永远不会醒来(即只有被notify后才配抢锁),这时jvm会去调用其他线程来执行。
在这里插入图片描述

在这里插入图片描述

为什么偏向锁要先延迟一段时间

java报错

Javac 编译时出现 “错误,编码 GBK 的不可映射字符”

必然是.java文件中出现了中文,使用ide时默认是UTF-8来存每个字符的unicode,但在windows命令行cmd执行javac的时候默认编(解)码方案是GBK,所以你需要显式指定-encoding UTF-8

字符集和字符编码(规则)(存储规则)

字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常用字符集名称有:ASCII字符集(既是字符集,又是字符编码)、GBK字符集(既是字符集,又是字符编码,兼容ASCII)、Unicode字符集(单纯的字符集,兼容ASCII)等等

  • 字符集:把每个字符对应到一个数字的哈希映射,比如ASCII字符集最早由美国人弄出,收录了英文字母和一些英文标点字符,但因为别的语言的人也要用电脑,所以才会有其他字符集,比如GBK字符集就是收录了现有汉字,使得每个汉字都能有所对应的数字,Unicode字符集是全局字符集又称万国码,它为世界上所有语言的字符分配了唯一的编码数字,作为标准兼容了所有人
  • 字符编码:每个字符对应的数字具体以何种方式存储到计算机, 比如UTF-8,GBK,也就是即使有了字符集还不够,具体怎么存某个字符的这段数字需要编码来决定,比如unicode是4个字节能够完全存完世界上所有文字的,你当然就可以直接简单地定长4字节来存放每个字,但对于很多字前面都是一堆0,这会浪费空间,所以如果unicode中越前面的字使用越少的字节将可以节省空间,这时候UTF-8做的就是这件事,它是一套以 8 位为一个编码单位的可变长编码,就是将一个码位(字符)编码为 1 到 4 个字节。 既然是变长的存储,那读取解析字节流的时候当然需要知道当前要读取多少个字节作为一个字符,UTF-8使用标记来实现,如下所示,根据不同数值大小在每个字节开头加上标记以指示读取字节数

0000 ~ 007F: 0XXXXXXX
0080 ~ 07FF: 110XXXXX 10XXXXXX
0800 ~ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
10000 ~ FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
本质就是同一段unicode不同的拆分存放方式

上面提到,ASCII既是字符集,又是字符编码,因为某个字母在ASCII字符集中的数字,就是最终存储在计算机的数字。GBK同理且也是像UTF-8那样使用标记法来节约空间且兼容了ASCII,即ASCII字符集中所有字符的数字在GBK中是一样的,且最终存储也真的使用这个数字即一个字节搞定,汉字部分则使用两个字节定长(因为ASCII部分把一个字节占了)

总的来说:一般应用都是用unicode字符集,但同一串数字不同的存储方案在转换时容易导致乱码

锟斤拷来源

上面提到GBK汉字部分使用两个字节定长,如果你用GBK编码保存txt,发给了文本编辑器默认为UTF-8编码的同学,那他打开时文本编辑器很有可能因为识别不出GBK编码的内容而自动使用unicode中一个专门表示不可识别的字符(黑色菱形中间一个问号)来替换你的原始内容,如果他一旦保存文件,那么就是保存了一堆不可识别的字符,这时候再发回给你的GBK的txt,对应的就是锟斤拷

java是值传递

无论你说值传递还是引用传递,本质都是值传递,回忆c++栈帧,那么java也是这样的,栈帧里面存放着当前执行的函数的局部变量表和操作栈等。

public class ValueTest {public static void main(String[] args) {int age1 = 25;String name1 = "hhh";print(age1,name1);}private static void print(int age, String name) {age = 88;name = "kkk";}
}

以上面为例子,print前后都不会改变age1和name1的值,具体流程如下:调用print的一刻,因为age1,name1作为参数传入,在c++的底层,本质是分别开辟一个int大小的空间和String大小(严谨地,这种类的都是地址)的空间(空间大小是编译成汇编后即可确定的,本质是push的时候指定隔开几个字节的位置),然后分别复制age1和name1的值放到对应空间位置,也就是这两个空间就是print的局部变量的底层表示,如下图(颜色不相同代表值不相同),最一开始进入print函数的那一刻就是图一,图二是开始修改name和age之后的,然而不会影响到外部,比如name,只是让这个地址变量指向了堆中kkk字符串的地址,完成该函数时print的栈帧弹出,一切归于虚无,白忙活
在这里插入图片描述

在这里插入图片描述
但是,如果print函数中参数是某个类(非基本类型),那么调用该类的成员函数时,比如Person类的setName方法,那么确实可以修改到该对象的成员变量,因为setName内部的this.以及你有这个对象的地址,当然可以了,这仍然符合c++

java主类的main函数运行前发生了什么

WinMain

这个是执行java XXX的时候第一个被执行的函数

/*** JVM主函数入口*/
#ifdef JAVAWchar **__initenv;int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)
{int margc;char** margv;const jboolean const_javaw = JNI_TRUE;__initenv = _environ;margc = __argc;margv = __argv;#else /* JAVAW */
int	main(int argc, char ** argv)
{int margc;char** margv;const jboolean const_javaw = JNI_FALSE;margc = argc;margv = argv;
#endif /* JAVAW */return JLI_Launch(margc, margv,sizeof(const_jargs) / sizeof(char *), const_jargs,sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,FULL_VERSION,DOT_VERSION,(const_progname != NULL) ? const_progname : *margv,(const_launcher != NULL) ? const_launcher : *margv,(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,const_cpwildcard, const_javaw, const_ergo_class);
}

JLI_Launch

int
JLI_Launch(int argc, char ** argv,              /* main argc, argc */int jargc, const char** jargv,          /* java args */int appclassc, const char** appclassv,  /* app classpath */const char* fullversion,                /* full version defined */const char* dotversion,                 /* dot version defined */const char* pname,                      /* program name */const char* lname,                      /* launcher name */jboolean javaargs,                      /* JAVA_ARGS */jboolean cpwildcard,                    /* classpath wildcard*/jboolean javaw,                         /* windows-only javaw */jint ergo                               /* ergonomics class policy */
){/*初步参数解析:从java命令行后面给的参数项确定jvm宏观上的参数比如jvm路径、类型等*/CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg,  sizeof(jvmcfg));/*根据jvmpath路径加载JVM的实现(动态连接库dll),具体就是把ifn结构体设置为jvm.dll中的JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs即ifn->CreateJavaVM =(void *)GetProcAddress(handle, "JNI_CreateJavaVM");ifn->GetDefaultJavaVMInitArgs =(void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");*/LoadJavaVM(jvmpath, &ifn);/*深度解析参数:argv是java命令行的所有参数,经过ParseArguments后解析-开头的jvm参数到options字符串数组中,并解析出mode(取值为jar或class,表示主类是否在jar包中,默认为class), 和what(带-jar则为命令行中用户给的jar包路径字符串,反之-cp或不写则默认为命令行中用户给的主类的class文件路径字符串),argv指针会不断指向下一个参数直至当前参数不以-开头,然后令what指向这个非-开头的参数即上述括号内的内容,所以如果命令行中java -a -b test -c时参数c会被当成java的main函数的参数而没被正常解析,此时argv指针指向最后几个java程序的参数,因为紧跟着jar包路径或class路径的肯定就是了,即main函数的args数组即public static void main(String[] args)*/ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath);/*如果命令行参数中带-jar则令ClassPath为jar包路径*/if (mode == LM_JAR) {printf("%s[%d] [tid: %lu]: java主程序在jar包中.\n", __FILE__, __LINE__, pthread_self());SetClassPath(what);     /* Override class path */}return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

CreateExecutionEnvironment函数的流程:

  1. 确定当前运行的 JVM 架构:根据命令行参数 -J-d32 或 -J-d64 来确定当前 JVM 的位数,32 位还是 64 位。但会先确定本机的位数即(CHAR_BIT * sizeof(void*))即求指针的位数,CHAR_BIT=8,如果和参数要求的不一致会报错

  2. 获取 JRE 路径:调用 GetJREPath 函数来获取 JRE 的安装路径。底层是根据当前运行的java.exe所在路径来实现,比如我的电脑中jdk放在C:\Program Files\Java\jdk1.8.0_201,其中java.exe在C:\Program Files\Java\jdk1.8.0_201\jre\bin下,于是在这个目录下找java.dll,事实上也确实在

  3. 构建 jvm.cfg 文件路径:使用 JRE 路径构建 jvm.cfg 文件的路径,该文件用于存储已知的 JVM 类型信息。该文件在C:\Program Files\Java\jdk1.8.0_201\jre\lib\amd64

  4. 读取已知的 JVM 类型信息:调用 ReadKnownVMs 函数读取 jvm.cfg 文件中已知的 JVM 类型信息。

  5. 检查 JVM 类型:根据命令行参数和已知的 JVM 类型信息来确定使用的 JVM 类型。

  6. 获取 JVM 路径:根据 JRE 路径和确定的 JVM 类型来获取相应的 JVM 的路径。

  7. (可选)预加载 AWT:根据命令行参数来决定是否需要预加载 AWT(Abstract Window Toolkit)。

JVMInit

int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,int argc, char **argv,int mode, char *what, int ret)
{ShowSplashScreen();return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

ContinueInNewThread

int ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,int argc, char **argv,int mode, char *what, int ret)
{/** If user doesn't specify stack size, check if VM has a preference.* Note that HotSpot no longer supports JNI_VERSION_1_1 but it will* return its default stack size through the init args structure.*/if (threadStackSize == 0) {struct JDK1_1InitArgs args1_1;memset((void*)&args1_1, 0, sizeof(args1_1));args1_1.version = JNI_VERSION_1_1;printf("%s[%d] [%lu]: 试图获取默认的JVM初始化参数(方法栈大小)...\n", __FILE__, __LINE__, pthread_self());ifn->GetDefaultJavaVMInitArgs(&args1_1);  /* ignore return value */if (args1_1.javaStackSize > 0) {threadStackSize = args1_1.javaStackSize;}}{ /* Create a new thread to create JVM and invoke main method */JavaMainArgs args;int rslt;args.argc = argc;args.argv = argv;args.mode = mode;args.what = what;args.ifn = *ifn;printf("%s[%d] [tid: %lu]: 试图创建一个新线程来运行JVM..\n", __FILE__, __LINE__, pthread_self());rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);/* If the caller has deemed there is an error we* simply return that, otherwise we return the value of* the callee*/return (ret != 0) ? ret : rslt;}
}

ContinueInNewThread0

这段代码是在创建一个新线程,并在新线程中执行指定的函数。具体步骤如下:

首先定义了一个变量 rslt 用于存储线程执行完毕后的返回值。

然后定义了一个 thread_id 变量,用于存储新线程的 ID。

接着检查是否定义了 STACK_SIZE_PARAM_IS_A_RESERVATION 宏,如果没有定义,则定义为默认值 0x10000。

接下来调用 _beginthreadex 函数创建新线程(_beginthreadex 是 Microsoft Windows 环境下的一个函数,用于创建新的线程),传入了以下参数:

NULL:表示使用默认的安全属性。
(unsigned)stack_size:线程栈大小。
continuation:指向要在新线程中执行的函数的指针。
args:传递给 continuation 函数的参数。
STACK_SIZE_PARAM_IS_A_RESERVATION:表示线程栈大小参数。
&thread_id:用于存储新线程的 ID。
如果 _beginthreadex 函数返回的线程句柄为 NULL,则尝试再次创建新线程,这次不使用 STACK_SIZE_PARAM_IS_A_RESERVATION 标志。

如果成功创建了线程,则调用 WaitForSingleObject 函数等待新线程执行完成,并调用 GetExitCodeThread 函数获取新线程的返回值,并关闭线程句柄。

如果创建线程失败,则直接调用 continuation 函数执行,并将返回值赋给 rslt。

最后根据预处理宏 ENABLE_AWT_PRELOAD 的定义,在新线程执行完毕后,可能会执行一些与 AWT 相关的预加载操作,并返回 rslt。

/** Block current thread and continue execution in a new thread*/
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {int rslt = 0;unsigned thread_id;#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
#define STACK_SIZE_PARAM_IS_A_RESERVATION  (0x10000)
#endif/** STACK_SIZE_PARAM_IS_A_RESERVATION is what we want, but it's not* supported on older version of Windows. Try first with the flag; and* if that fails try again without the flag. See MSDN document or HotSpot* source (os_win32.cpp) for details.*/HANDLE thread_handle =(HANDLE)_beginthreadex(NULL,(unsigned)stack_size,continuation,args,STACK_SIZE_PARAM_IS_A_RESERVATION,&thread_id);if (thread_handle == NULL) {thread_handle =(HANDLE)_beginthreadex(NULL,(unsigned)stack_size,continuation,args,0,&thread_id);}/* AWT preloading (AFTER main thread start) */
#ifdef ENABLE_AWT_PRELOAD/* D3D preloading */if (awtPreloadD3D != 0) {char *envValue;/* D3D routines checks env.var J2D_D3D if no appropriate* command line params was specified*/envValue = getenv("J2D_D3D");if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) {awtPreloadD3D = 0;}/* Test that AWT preloading isn't disabled by J2D_D3D_PRELOAD env.var */envValue = getenv("J2D_D3D_PRELOAD");if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) {awtPreloadD3D = 0;}if (awtPreloadD3D < 0) {/* If awtPreloadD3D is still undefined (-1), test* if it is turned on by J2D_D3D_PRELOAD env.var.* By default it's turned OFF.*/awtPreloadD3D = 0;if (envValue != NULL && JLI_StrCaseCmp(envValue, "true") == 0) {awtPreloadD3D = 1;}}}if (awtPreloadD3D) {AWTPreload(D3D_PRELOAD_FUNC);}
#endif /* ENABLE_AWT_PRELOAD */if (thread_handle) {WaitForSingleObject(thread_handle, INFINITE);GetExitCodeThread(thread_handle, &rslt);CloseHandle(thread_handle);} else {rslt = continuation(args);}#ifdef ENABLE_AWT_PRELOADif (awtPreloaded) {AWTPreloadStop();}
#endif /* ENABLE_AWT_PRELOAD */return rslt;
}

因为ContinueInNewThread0第一个参数是新线程要运行的函数即rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);所以我们直接看JavaMain函数

JavaMain

/** JVM主线程的执行函数*/
int JNICALL JavaMain(void * _args)
{/*遍历命令行中传入的jvm参数来创建/初始化JVM,早在JLI_Launch函数中的ParseArguments函数就已经把jvm参数字符串解析到options字符串数组中而ifn结构体在之前的JLI_Launch函数中的LoadJavaVM函数被设置为jvm.dll中的JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs即ifn->CreateJavaVM =(void *)GetProcAddress(handle, "JNI_CreateJavaVM");ifn->GetDefaultJavaVMInitArgs =(void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");*/InitializeJVM(&vm, &env, &ifn)//加载主类,后面会提到mainClass = LoadMainClass(env, mode, what);/*从主类中拿到方法名为main且参数是字符串数组类型的静态方法([Ljava/lang/String;)V 是一个Java方法的描述符,描述了该方法的参数和返回类型。[Ljava/lang/String; 表示一个字符串数组类型,其中 [ 表示数组,Ljava/lang/String; 表示字符串类型。V 表示方法的返回类型,V 表示该方法没有返回值(void)*/mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");//以字符串数组形式重新包装剩余的java程序的参数mainArgs = NewPlatformStringArray(env, argv, argc);//传入参数执行main方法调用(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
}

LoadMainClass最关键!!!!!!!!!

/** 装载一个类,并检查其是否声明了 static main(String[] args) 方法*/
/*name是jar包路径或者主类字节码文件的路径,通过mode决定如何解析name
-jar则mode=2,反之1*/
static jclass LoadMainClass(JNIEnv *env, int mode, char *name)
{jmethodID mid;jstring str;jobject result;jlong start, end;//拿到java层面核心jar包rt.jar中的LauncherHelper类(通过启动类加载器)jclass cls = GetLauncherHelperClass(env);NULL_CHECK0(cls);//拿到LauncherHelper类的checkAndLoadMain静态方法NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls, "checkAndLoadMain", "(ZILjava/lang/String;)Ljava/lang/Class;"));//把str理解为对name的包装str = NewPlatformString(env, name);//调用刚刚得到的java层面的checkAndLoadMain方法,//USE_STDERR, mode, str作为参数result = (*env)->CallStaticObjectMethod(env, cls, mid, USE_STDERR, mode, str);return (jclass)result;
}

LauncherHelper类的checkAndLoadMain静态方法

public static Class<?> checkAndLoadMain(boolean var0, int var1, String var2) {/*var0-USE_STDERR,它就是True, var1-mode,当命令行带-jar则mode=2,反之1, var2-jar包路径或者主类字节码文件的路径*///令LauncherHelper类的输出流初始化为System.errinitOutput(var0);/*根据是否带-jar决定对var2路径的解析方式,最终效果是var3拿到主类字节码文件的路径*/String var3 = null;switch(var1) {case 1:var3 = var2;break;case 2://此时var2是jar包路径,从jar包中拿到主类字节码文件的路径var3 = getMainClassFromJar(var2);break;}//把路径中的/替换成.因为java内部用.传递,底层去找类还是/的var3 = var3.replace('/', '.');Class var4 = null;//用应用类加载器加载主类,具体看下面var4 = scloader.loadClass(var3);/*令Class类型的var4即主类的Class对象赋值给LauncherHelper类的静态变量appClass*/appClass = var4;/*利用反射验证用户给的主类是否真的是具体实现是:检查是否有返回值为void且名为main且参数为字符串数组的静态方法*/validateMainClass(var4);//最后把主类的Class对象返回到jvm层面return var4;
}

上面说用应用类加载器加载主类即
var4 = scloader.loadClass(var3);这是怎么回事?

scloader也是LauncherHelper类的静态变量即
private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();
所以LauncherHelper类被加载时ClassLoader.getSystemClassLoader();就会触发

我们看看

ClassLoader的getSystemClassLoader()

@CallerSensitive
public static ClassLoader getSystemClassLoader() {//这个函数很重要!!!!!!!!!!!!!!!!!//总的来说,返回的scl就是AppClassLoader对象initSystemClassLoader();if (scl == null) return null;    return scl;
}

initSystemClassLoader

private static synchronized void initSystemClassLoader() {/*sclSet布尔变量记录ClassLoader类的静态变量scl即系统类加载器是否被设置,默认false*/if (!sclSet) {/*此时肯定发生Launcher类的加载,于是触发该类的单例对象生成,而该类的构造方法会为单例对象生成单例的ExtClassLoader并作为父类加载器生成单例的AppClassLoader。然后Launcher类单例对象的this.loader指向单例AppClassLoader*/sun.misc.Launcher l = sun.misc.Launcher.getLauncher();//此时scl拿的就是Launcher类this.loader即单例AppClassLoaderif (l != null) scl = l.getClassLoader();sclSet = true;}//所以上面说返回的scl就是AppClassLoader对象
}

上面提到Launcher类是单例
这是因为Launcher类的静态变量
private static Launcher launcher = new Launcher();
使得加载该类后就会立即创建该类的一个实例对象,实现了单例

public Launcher() {Launcher.ExtClassLoader var1;var1 = Launcher.ExtClassLoader.getExtClassLoader();this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);Thread.currentThread().setContextClassLoader(this.loader);
}

还需要注意,一直以来提到的三大类加载器中的ExtClassLoader和AppClassLoader都是Launcher类的内部类,所以常会看到sun.misc.Launcher.$ExtClassLoader这种写法,即sun.misc包下的Launcher类的内部类ExtClassLoader

package sun.misc;
public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}public Launcher() {Launcher.ExtClassLoader var1;try {var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}}public ClassLoader getClassLoader() {return this.loader;}public static URLClassPath getBootstrapClassPath() {return Launcher.BootClassPathHolder.bcp;}private static URL[] pathToURLs(File[] var0) {URL[] var1 = new URL[var0.length];for(int var2 = 0; var2 < var0.length; ++var2) {var1[var2] = getFileURL(var0[var2]);}return var1;}private static File[] getClassPath(String var0) {File[] var1;if (var0 != null) {int var2 = 0;int var3 = 1;boolean var4 = false;int var5;int var7;for(var5 = 0; (var7 = var0.indexOf(File.pathSeparator, var5)) != -1; var5 = var7 + 1) {++var3;}var1 = new File[var3];var4 = false;for(var5 = 0; (var7 = var0.indexOf(File.pathSeparator, var5)) != -1; var5 = var7 + 1) {if (var7 - var5 > 0) {var1[var2++] = new File(var0.substring(var5, var7));} else {var1[var2++] = new File(".");}}if (var5 < var0.length()) {var1[var2++] = new File(var0.substring(var5));} else {var1[var2++] = new File(".");}if (var2 != var3) {File[] var6 = new File[var2];System.arraycopy(var1, 0, var6, 0, var2);var1 = var6;}} else {var1 = new File[0];}return var1;}static URL getFileURL(File var0) {try {var0 = var0.getCanonicalFile();} catch (IOException var3) {}try {return ParseUtil.fileToEncodedURL(var0);} catch (MalformedURLException var2) {throw new InternalError(var2);}}static class AppClassLoader extends URLClassLoader {final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {final String var1 = System.getProperty("java.class.path");final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {public Launcher.AppClassLoader run() {URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);return new Launcher.AppClassLoader(var1x, var0);}});}AppClassLoader(URL[] var1, ClassLoader var2) {super(var1, var2, Launcher.factory);this.ucp.initLookupCache(this);}public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {int var3 = var1.lastIndexOf(46);if (var3 != -1) {SecurityManager var4 = System.getSecurityManager();if (var4 != null) {var4.checkPackageAccess(var1.substring(0, var3));}}if (this.ucp.knownToNotExist(var1)) {Class var5 = this.findLoadedClass(var1);if (var5 != null) {if (var2) {this.resolveClass(var5);}return var5;} else {throw new ClassNotFoundException(var1);}} else {return super.loadClass(var1, var2);}}protected PermissionCollection getPermissions(CodeSource var1) {PermissionCollection var2 = super.getPermissions(var1);var2.add(new RuntimePermission("exitVM"));return var2;}private void appendToClassPathForInstrumentation(String var1) {assert Thread.holdsLock(this);super.addURL(Launcher.getFileURL(new File(var1)));}private static AccessControlContext getContext(File[] var0) throws MalformedURLException {PathPermissions var1 = new PathPermissions(var0);ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});return var3;}static {ClassLoader.registerAsParallelCapable();}}private static class BootClassPathHolder {static final URLClassPath bcp;private BootClassPathHolder() {}static {URL[] var0;if (Launcher.bootClassPath != null) {var0 = (URL[])AccessController.doPrivileged(new PrivilegedAction<URL[]>() {public URL[] run() {File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);int var2 = var1.length;HashSet var3 = new HashSet();for(int var4 = 0; var4 < var2; ++var4) {File var5 = var1[var4];if (!var5.isDirectory()) {var5 = var5.getParentFile();}if (var5 != null && var3.add(var5)) {MetaIndex.registerDirectory(var5);}}return Launcher.pathToURLs(var1);}});} else {var0 = new URL[0];}bcp = new URLClassPath(var0, Launcher.factory, (AccessControlContext)null);bcp.initLookupCache((ClassLoader)null);}}static class ExtClassLoader extends URLClassLoader {private static volatile Launcher.ExtClassLoader instance;public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {if (instance == null) {Class var0 = Launcher.ExtClassLoader.class;synchronized(Launcher.ExtClassLoader.class) {if (instance == null) {instance = createExtClassLoader();}}}return instance;}private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {try {return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {public Launcher.ExtClassLoader run() throws IOException {File[] var1 = Launcher.ExtClassLoader.getExtDirs();int var2 = var1.length;for(int var3 = 0; var3 < var2; ++var3) {MetaIndex.registerDirectory(var1[var3]);}return new Launcher.ExtClassLoader(var1);}});} catch (PrivilegedActionException var1) {throw (IOException)var1.getException();}}void addExtURL(URL var1) {super.addURL(var1);}public ExtClassLoader(File[] var1) throws IOException {super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}private static File[] getExtDirs() {String var0 = System.getProperty("java.ext.dirs");File[] var1;if (var0 != null) {StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);int var3 = var2.countTokens();var1 = new File[var3];for(int var4 = 0; var4 < var3; ++var4) {var1[var4] = new File(var2.nextToken());}} else {var1 = new File[0];}return var1;}private static URL[] getExtURLs(File[] var0) throws IOException {Vector var1 = new Vector();for(int var2 = 0; var2 < var0.length; ++var2) {String[] var3 = var0[var2].list();if (var3 != null) {for(int var4 = 0; var4 < var3.length; ++var4) {if (!var3[var4].equals("meta-index")) {File var5 = new File(var0[var2], var3[var4]);var1.add(Launcher.getFileURL(var5));}}}}URL[] var6 = new URL[var1.size()];var1.copyInto(var6);return var6;}public String findLibrary(String var1) {var1 = System.mapLibraryName(var1);URL[] var2 = super.getURLs();File var3 = null;for(int var4 = 0; var4 < var2.length; ++var4) {URI var5;try {var5 = var2[var4].toURI();} catch (URISyntaxException var9) {continue;}File var6 = Paths.get(var5).toFile().getParentFile();if (var6 != null && !var6.equals(var3)) {String var7 = VM.getSavedProperty("os.arch");File var8;if (var7 != null) {var8 = new File(new File(var6, var7), var1);if (var8.exists()) {return var8.getAbsolutePath();}}var8 = new File(var6, var1);if (var8.exists()) {return var8.getAbsolutePath();}}var3 = var6;}return null;}private static AccessControlContext getContext(File[] var0) throws IOException {PathPermissions var1 = new PathPermissions(var0);ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});return var3;}static {ClassLoader.registerAsParallelCapable();instance = null;}}private static class Factory implements URLStreamHandlerFactory {private static String PREFIX = "sun.net.www.protocol";private Factory() {}public URLStreamHandler createURLStreamHandler(String var1) {String var2 = PREFIX + "." + var1 + ".Handler";try {Class var3 = Class.forName(var2);return (URLStreamHandler)var3.newInstance();} catch (ReflectiveOperationException var4) {throw new InternalError("could not load " + var1 + "system protocol handler", var4);}}}
}

Oop-Klass模型

参考1:
参考2:
JVM底层粗略地说:
用Klass类的一个对象来表示java中的一个类(记录java类的元信息)
用Oop类的一个对象来表示java类的一个对象(记录java对象的成员变量等信息)

具体来说:
1.jvm在加载class时,创建instanceKlass,表示其元数据,包括常量池、字段、方法等,存放在方法区;instanceKlass是jvm中的数据结构;
2.在new一个对象时,jvm创建instanceOopDesc,来表示这个对象,存放在堆区,其引用,存放在栈区;它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;instanceOopDesc对应java中的对象实例;
3.HotSpot并不把instanceKlass暴露给Java,而会另外创建对应的instanceOopDesc来表示java.lang.Class对象,并将后者称为前者的“Java镜像”,klass持有指向oop引用(_java_mirror便是该instanceKlass对Class对象的引用);
4.要注意,new操作返回的instanceOopDesc类型指针指向instanceKlass,而instanceKlass指向了对应的类型的Class实例的instanceOopDesc;有点绕,简单说,就是Person实例——>Person的instanceKlass——>Person的Class。

instanceOopDesc,只包含数据信息,它包含三部分:

  1. 对象头,也叫Mark Word,主要存储对象运行时记录信息,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等;
  2. 元数据指针,即指向方法区的instanceKlass实例
  3. 实例数据;
  4. 另外,如果是数组对象,还多了一个数组长度

反正类的class对象在堆(为了java层面能获得类元信息),instanceKlass在方法区(不暴露给java利用class对象给java用),方法区更多是jvm在c++层面的,堆都是给java用的

类的加载

在这里插入图片描述

这时Oop-Klass模型结合类的加载机制具体会发生下面事件:

  1. (1)通过类的全限定名(包名+类名)获取存储该类的class文件
    (2)解析成运行时数据,即instanceKlass实例,存放在方法区
    (3)在堆区生成该类的Class对象,即instanceMirrorKlass实例

初始化阶段

前面准备阶段只对正常静态变量赋予了初值0或null,以及特殊的final静态变量直接赋值,对于正常静态变量而言代码中赋予的初值在初始化阶段的<clinit>()方法完成。

<clinit>() 特点:

  1. 它是 Javac编译器 的自动生成物,存在于字节码代码中。
  2. 编译器收集的顺序是由语句在源文件中出现的顺序决定的。 按先 初始化类变量 → 执行静态语句块 的顺序进行 。
  3. <clinit>()方法不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object

多态底层原理

看个例子,A是B的父类

public class A{//类A独有的方法public void method1(){...;}//被子类重写的方法public void method2(){...;}
}
public class B extend A{//重写了父类A的方法@Overridepublic void method2(){...;}//类B独有的方法public void method3(){...;}
}
A b = new B();
b.method2();//此时调用的就是类B重写后的method2()

此时对于类B(准确说在jvm层面是用以描述类B的元信息的InstanceKlass对象)的方法表(只保存非私有的实例方法)如下:

在这里插入图片描述

方法表不记录的内容
静态方法:静态方法是类级别的方法,不属于某个实例,因此它们不参与动态分派,而是通过invokestatic指令调用。
构造函数:构造函数是用于创建对象实例的特殊方法,它们在对象初始化过程中调用,不参与动态分派,是通过invokespecial指令调用的。
私有方法:私有方法只能在声明它们的类内部访问,不会被子类重写,因此不参与动态分派(又叫虚分派),同样通过invokespecial指令调用

方法表(本质是数组)存储各方法(在方法区)的内存地址
即指向类加载后各类的各方法在方法区生成的jvm层面的Method对象

即自上而下是当前类的最高父类即Object类的方法,然后是次高父类,以此类推直到当前类,如果子类重写了父类的方法,那么令方法表中父类的该方法项指向子类的方法实现,如上图红色箭头

总的来说,执行invokevirtual #数字 (这里#数字 是常量池中该方法的符号引用即CONSTANT_Methodref_info类型,经过编译#数字 中的数字会被确定)指令时,jvm底层:

  1. 通过栈帧中的对象引用找到存放在堆中的对象
  2. 分析对象头,找到该对象所属类在jvm中对应的类的元信息对象即InstanceKlass
  3. 在该类的InstanceKlass对象中拿到vtable虚方法表成员变量(在类加载的连接阶段就已经根据重写的方法生成好了)
  4. 根据#后面的数字作为虚方法表数组的索引或者叫偏移量,从而得到方法的字节码在内存中(方法区)的具体地址
  5. 执行该方法的字节码

具体而言,多态在java中整体上是以下流程:

  1. 编译阶段:编译器检查用于接收 引用b 的类型 A 是否有 method2 方法。确认存在 method2 方法后,编译器生成invokevirtual指令,但此时并不确定具体调用哪个类的实现。
  2. 运行阶段:JVM 在运行时会检查 引用b 的实际类型是 B。JVM 会在 B 类中查找 method2 方法。找到后,执行 B 类的 method2 方法

字符串底层原理

字符串的六种基本创建方式

1. 使用 char[] 数组配合 new 来创建

String s = new String(new char[]{'a', 'b', 'c'});(char占两个字节,而且char存放的都是unicode,反正无论从哪里按照什么编码读取到的字符数据最终在java的char中都是unicode编码的,比如用gdk编码(即每个字符用2个字节表示)读取文本文件的第一个字,拿到该字的gbk编码后java根据这个字找到对应的unicode编码存放到char中),char数组就是String对象存储字符串的底层数据结构
在这里插入图片描述

2. 使用 byte[] 数组配合 new 来创建

String s = new String(new byte[]{97, 98, 99});(byte占一个字节)

从网络传过来的字节数据或者读取文本文件时的字节流就需要用到这种字符串创建方法
在这里插入图片描述本质同1,因为String构造方法本质会把这个byte数组转换为char数组
byte数组转换为char数组在这里插入图片描述也就是根据你提供的byte数组以及以哪种字符集编码来读取该byte数组,java会将对应的字转换成unicode存放到char数组中

3. 使用 int[] 数组配合 new 来创建

String s = new String(new int[]{0x1F602}, 0, 1);
在这里插入图片描述

4. 使用 已有字符串配合 new 来创建

String s1 = new String(new char[]{'a', 'b', 'c'});
String s2 = new String(s1);//源码
public String(String original) {this.value = original.value;this.hash = original.hash;
}

在这里插入图片描述

5. 使用字面量创建(不使用 new )(最常用)!!!!!!!!!!!

String s = "abc";
这种方式创建的字符串有三个特性:非对象、懒加载、不重复

严格的说,字面量在代码运行到它所在语句之前,它仍不是字符串对象,

需要知道,类加载后,该类的字节码中的常量池部分全部都会被加入到运行时常量池中
在这里插入图片描述
首先执行第一句指令即ldc #2,jvm(是用c++写的程序)在解释该指令时针对不同类型的常量会有不同的行为,这里#2对应的是String类型(在常量池中标记为JVM_CONSTANT_String)的常量,于是在c++层面会根据运行时常量池中的#2字符串常量在堆中创建对应的char数组,再创建String对象并value属性指向这个char数组(java中数组都属于对象),最后把String对象放到操作数栈顶,然后当前执行的线程的计数器+1执行astore_1,即令该线程的堆栈中的(栈帧中的局部变量表的第1个)变量s指向(引用)操作数栈顶的这个String对象
在这里插入图片描述其中,常量池是class文件中的,运行时常量池在jvm内存中。当该类被加载时,他的常量池会被放入运行时常量池,并把里面的符号地址(#1…)变成内存地址

对于下面这种情况,那么两次的ldc指令只有第一次会创建"abc"的String对象(不重复),即二者引用存放在StringTable中的同一个String对象"abc"(而且同一个类中的相同字面量在字节码的常量池中也只有一份即都是ldc #2)

String s1="abc";
String s2="abc";

这是因为本质上,上面ldc的流程更具体地说,会有StringTable参与,StringTable的本质是不可扩容的哈希表(可以理解为继承或者组合),负责保证不重复创建相同的String对象,即上面图中的String对象创建后之后,还会调用jvm内部对字符串的intern函数(效果等价于java层面String对象的intern方法),即如果当前字符串常量不在StringTable中(通过c++层面StringTable的哈希表成员的get方法判定),那么就把它创建的String对象的引用加入到哈希表中并返回,反之直接返回哈希表中对该String对象的引用

如果是不同类的相同字面量定义的字符串,
在这里插入图片描述那么三个abc都是完全一致的
在这里插入图片描述

StringTable在jvm的位置

在这里插入图片描述

6. 使用 + 运算符来拼接创建(很重要)!!!!!!

字面量+字面量

如String s = “a” + “b”;
在这里插入图片描述因为"a"和"b"都是常量,于是javac在编译期就已经可以优化,把字符串s的值确定下来,即字节码中的常量池直接就是字符串常量"ab",执行ldc字节码时,直接生成ab的char数组及String对象且引用该char数组即可

+常量(反过来也一样)

如下,final定义的String就是字符串常量
在这里插入图片描述于是也是编译期便可以优化,因为x必然不能改变,原理同上一个,只是这里会单独为“b”放入到常量池,因为x也是要被其他地方使用的

+变量

对于+变量,比如String s,是无法直接得出等于"ab"的,因为有可能执行到下面的时候x突然改变,
在这里插入图片描述
在这里插入图片描述
因此对于+变量的字符串拼接,等价于先创建StringBuilder对象然后逐个append最后toString,而StringBuilder的toString方法就是根据StringBuilder内部的char数组创建String对象
在这里插入图片描述下面的变量+变量同理
在这里插入图片描述

底层看string!!!!!

jvm是c++写的,java每个对象在jvm中都是用oop(instanceOopDesc)来描述的,java每个类则是kclass

new String(“XXX”)和"XXX"的区别

共性

s=new String(“XXX”)和直接s="XXX"都会在stringtable这个运行时字符串常量池(本质是hashtable)进行查找以及缓存(本质是ldc字节码导致的行为),即把“XXX”放入stringtable,具体而言,哈希值是字面量XXX及其长度的函数,根据该哈希值映射到数组的某个idx,value是XXX这个string对象的地址,这样如果后面再来一句s=new String(“XXX”)和s="XXX"都会先进入stringtable这个哈希表缓存中查找string是否已存在,是则直接返回其地址,反之会根据内容XXX当场生成string对象并放入stringtable。需要注意:stringtable是jvm层面即c++写的。

差别

new String(“XXX”)每次(除了首次为了缓存生成的XXX string对象即下下图中下面的instanceOopDesc)会必然多生成一个string对象即new String()即下下图中的上面的instanceOopDesc,然后将这个空字符串对象的value,即char[]本质是char指针指向字符串首个字符的地址即string类用于真正存储内容的成员变量,指向"XXX"这个string对象的value,也就是下下张图中的typeArrayOopDesc
在这里插入图片描述即s=new String(“XXX”)行为下,上下两个string对象的value和hash一样
在这里插入图片描述
下图则是s="XXX"行为,可以看出不会多生成一个string对象
在这里插入图片描述

这种简单+常量拼接的会被javac直接优化为s1="12"来对待,所以一开始创建12的string对象及其缓存然后返回,s2则查缓存得到12的string对象
在这里插入图片描述

下图则是说明其他字符串创建方式不会走缓存stringtable,本质是没使用ldc字节码
在这里插入图片描述

如果是s=new String(“A”) + new String(“B”) ,则先创建了一个stringbuilder空对象,然后分别走缓存的A和缓存的B,下图画少了两个空string对象即new String()本身及其对内容char数组和hash的引用,接着分别stringbuilder.append两个string对象最后tostring,而stringbuilder的tostring不会走缓存(下面有细说),因此共生成了5个对象,1 * stringbuilder + 2 * String(“A”) + 2 * String(“B”)
在这里插入图片描述
可看出两次new String两次ldc一次new Stringbuilder
在这里插入图片描述

stringbuilder的tostring源码:可以看到也是对其内部的char数组new String而我们上面说过这种创建方式不会走缓存

@Override
public String toString() {// 创建一个新的字符串,并将当前 StringBuilder 的字符数组复制到字符串中return new String(value, 0, count);
}

最后学习.intern(),判断s==s2吗?答案是true

String s = new String("111") + new String("222");
s.intern();
String s2 = "111222";

intern本质就是将s指向的string走一遍缓存,即如果s指向的string不在缓存中则将其放入缓存,即如下图:
在这里插入图片描述

JVM的组成

在这里插入图片描述

JVM栈帧

在这里插入图片描述
栈帧中储存着局部变量表、操作栈、动态链接、返回地址以及一些额外的附加信息。每个方法从调用到执行完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程

操作数栈

i++就是先load i到操作数栈再对局部变量表中的i进行+1操作

而++i反过来

所以有以下
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述 iconst_0 // 将常量 0 压入操作数栈
istore_1 // 将栈顶的值(0)存储到局部变量表索引 1 的位置
iinc 1, 1 // 将局部变量表索引 1 的值增加 1

动态链接

每个栈帧都保存了 当前方法所在类的运行时常量池的指针,
这样如果当前方法中需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接(每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它)

然而,我们必须知道解析(即把运行时常量池中的符号引用转为直接引用)分为两个阶段,

第一阶段(连接时解析)

第一阶段发生在类的连接阶段(具体是其中的解析阶段),这一阶段负责静态解析(早期绑定),对于静态方法(类名.)、私有private方法、final方法、实例构造器、父类方法super.,这些方法都属于编译成字节码后完全可以确定的方法,因为这些方法都不可被子类重写或者指向明确,所以必然能确定为具体属于哪个类的方法。此时经历了第一阶段后,运行时常量池处于部分常量被解析为直接引用的状态,还有另一部分仍然是符号引用,需要在第二阶段进行解析,

第二阶段(运行时解析)

当前类执行到涉及到常量池的字节码(比如invokestatic #?)此时发现该静态方法的符号引用还没有被解析为Method对象的内存地址,那么就会加载该?静态方法所属的类及其变量和方法到方法区,并将当前类的运行时常量池处于部分常量解析为直接引用,即把这里的#?变成位于方法区中t方法的Method对象的内存地址,第二阶段是多态实现的基础,即具体运行时才能知道最终调用哪个类的方法

在这里插入图片描述

类的生命周期

在这里插入图片描述

加载阶段

类加载发生时机

JVM加载类是懒加载

  1. new、getstatic、putstatic、invokestatic
  2. 反射
  3. 初始化一个类的子类会去加载其父类
  4. 启动类(main函数所在类)

可以总结为:第一次使用到涉及该类的内容都会导致类的加载
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
堆区的Class对象就是我们常用的反射的那个.class

在这里插入图片描述

双亲委派(自底向上查缓存,自上而下加载类)

在这里插入图片描述自底向上查缓存,缓存放的是已经加载过的类,有就直接返回,反之继续向上,到顶为止,开始向下,根据各类加载器的加载范围判定能否加载该类

系统类加载器就是应用类加载器即ApplicationClassLoader

类加载源码分析

在顶层类加载器类Classloader中提供了loadClass函数(主要是双亲委派的逻辑),以及findClass函数(双亲委派的查缓存阶段结束后仍未找到该类时开始根据类加载器路径范围寻找查找该类,找到该类的字节码文件后读入到byte数组,由jvm提供的native方法defineClass来具体根据类的字节码转化为java的Class对象),然而Classloader中的findClass函数约等于没实现,他就是简单地返回一个ClassNotFound异常,所以几乎具体的一个类加载器都必须重写findClass,以指示该类加载器如何找到一个类,从而才能通过IO流读到byte数组

调用关系是loadClass->findClass->defineClass

下面就是Classloader中提供的loadClass函数(主要是双亲委派的逻辑):

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}

具体java的launcher类中的内部类AppClassLoader和ExtClassLoader如何实现只负责加载自己url范围内的类呢?

源码中他们两都继承自URLClassLoader(该类间接继承Classloader)因为他们都是根据url或者文件路径加载类,URLClassLoader里面有根据url加载类的实现即重写了findClass,源码如下:

protected Class<?> findClass(final String name)throws ClassNotFoundException
{final Class<?> result;try {result = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {public Class<?> run() throws ClassNotFoundException {String path = name.replace('.', '/').concat(".class");Resource res = ucp.getResource(path, false);if (res != null) {try {return defineClass(name, res);} catch (IOException e) {throw new ClassNotFoundException(name, e);}} else {return null;}}}, acc);} catch (java.security.PrivilegedActionException pae) {throw (ClassNotFoundException) pae.getException();}if (result == null) {throw new ClassNotFoundException(name);}return result;
}

注意到上面有Resource res = ucp.getResource(path, false),ucp用于记录当前类加载器的类搜索路径,如下所示:

public class URLClassLoader extends SecureClassLoader implements Closeable {/* The search path for classes and resources */private final URLClassPath ucp;

然后创建AppClassLoader和ExtClassLoader实例时分别都会利用System.getProperty(“java.class.path”);和System.getProperty(“java.ext.dirs”);拿到他们对应的加载范围,然后存入到ucp中,这样调用AppClassLoader和ExtClassLoader实例的loadClass函数时即调用URLClassLoader的loadClass函数,这时就会根据他们具体设置的ucp,在其中尝试查找该类即ucp.getResource(path, false);

自定义类加载器

自定义类加载器的父类加载器默认会被自动设置为系统类加载器。
一般就是重写来自ClassLoader类的findClass方法,因为如上节所述,ClassLoader类的findClass方法只是简单地返回未找到类异常,因此这里我们填入自定义的类查找(体现自定义的地方)的具体逻辑(即想办法拿到类字节码文件并放入字节数组),比如可以用网络中获得,又或者指定目录下获得,最后记得调用一下jvm(native方法,c++层面实现)提供的defineClass(将你提供的字节码文件的byte数组转化为Class对象)即可,最后会返回一个Class的对象

public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 读取类文件的字节码内容(一般用IO流)byte[] classData = Files.readAllBytes(new File("/sb" + name + ".class").toPath());//可以实现自定义需求/* 将字节码内容转换为Class对象,这个是Classloader类提供的,具体内部实现是jvm提供的即native修饰*/return defineClass(name, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException("Class not found: " + name, e);}}
}
loadClass、findClass、defineClass什么关系

loadClass调用findClass调用defineClass

loadClass 是类加载最顶层的调用方法,它是通过类名来加载类,主要的代码是遵循双亲委派模型以及调用 loadClass,最终返回的是类的Class对象。(想打破双亲委派就重写这个方法)

如果 loadClass 没有找到类,它会 调用 当前类加载器的findClass 来查找类并返回Class对象。

findClass 通常由每个类加载器自己实现,它负责实际最终查找类以及拿到类的字节码字节数组,然后调用defineClass加载类的字节码。(需要双亲委派并且也需要自定义加载的路径可以重写这个方法,比如自己给定一个文件路径前缀,然后拼接上输入的类全限定名,就实现了在自定义的路径下加载给定类名的类)

defineClass 是jvm提供的api用来将字节码转化为 Class 对象并注册到 JVM 中并返回该Class对象。

无法实现加载java.lang.String

这是因为双亲委派的机制决定的,所以即使你在你的项目下定义了java.lang包然后定义String类是没用的,

如果你自定义类加载器来加载也是不行的,因为在调用defineClass的时候也会抛出异常,其内部会先检查,不允许加载自定义java.开头的类,而我们不可能自行重写defineClass的,

所以就是不能

为什么tomcat的实现必须要自定义类加载器

1. Web 应用的隔离性

Tomcat 为每个 Web 应用创建独立的自定义类加载器实例即WebappClassLoader对象。

不同的 Web 应用可能依赖相同库的不同版本,这些库的类名相同。如果使用同一个类加载器,按照双亲委派原则,先部署的应用的类会首先加载并被缓存,后部署的应用会在查找类时使用缓存中的旧版本类(因为类相同决定的原则,新版本类被认为和旧版本是同一个类),导致类冲突和不兼容问题。而如果每个 Web 应用使用独立的类加载器,即使类的全限定名相同,它们也不会被视为相同的类,避免了版本冲突

类加载器实例+类全限定名 唯一决定一个类

即使类的全限定名相同,如果是不同的类加载器实例加载的,那么你用java检查类是否相同的函数时都会告诉你两个类不同。这里我还强调了是类加载器实例,即比如你自定义了一个类加载器的类,它的不同实例对象加载同一个类也算作不同类。

2. 热部署功能

tomcat的热部署意思是开发人员能够在不重启tomcat服务器的情况下,动态加载或更新应用程序

比如我先前已经放了一个war包在tomcat的webapp目录下了,现在我修改了一下代码重新打了一个新war包替换webapp目录下那个旧的,Tomcat就会自动检测到新的.war文件(本质是tomcat会启动一个后台线程周期性检查war包是否被修改过,其实就是文件头的lastmodified字段是否与上次比较发生了改变),并创建一个新的类加载器实例来加载这个更新后的应用。旧的类加载器会被卸载,新的类加载器重新加载新war包中所有类,也就是我只需做替换war包,访问的时候能立马返回新的处理结果,tomcat服务器也没有关闭

需要注意,当我们打war包的时候并不会把核心类库也打进去,即扩展类加载器加载路径和启动类加载器加载路径下的jar包都不会打进去,所以tomcat不同版本会对应不同运行要求即jre不同,即tomcat本身也需要jre因为tomcat也是java写的,然后如果我们写网站的jdk版本过高,也要注意配置tomcat指向的jre。

tomcat本质就是一个基于socket做的http服务器,最主要的是用一个Map记录我们的war包中的web.xml
配置文件中Servlet 的定义和 URL 映射,tomcat根据用户请求tomcat的路径执行对应的Servlet仅此而已

连接阶段

验证

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
jdk8对应的主版本号是52,副版本号在这时候刚出,所以暂时只能为0
在这里插入图片描述

准备

在这里插入图片描述
在这里插入图片描述

final修饰意味着不会改变,所以直接在编译阶段就可以确定值了
在这里插入图片描述

解析

在这里插入图片描述

初始化阶段

其中init是当前类默认自动生成的无参构造方法,clinit即classinit负责静态代码块的执行和静态变量的赋值
在这里插入图片描述

putstatic指令将操作数栈的值弹到常量池的静态变量中,其中#2就是常量池中的第二个变量咯
在这里插入图片描述
在这里插入图片描述
如果static代码块和静态变量初始化对调,那么执行顺序也是对调,所以就是自上而下按顺序执行静态的东西

在这里插入图片描述
注意,Class.forname有其他重载的方法可以选择是否触发类的初始化

实战分析

在这里插入图片描述
拿到System.out静态变量,然后把将要输出的字符串放操作数栈顶,最后调用out的println方法把字符串输出到屏幕

垃圾回收

判断是否为垃圾

计数器算法

有人引用就+1,不引用-1,为0就是垃圾,但存在垃圾之间循环依赖问题

可达性分析算法(实际使用)

以gcroot对象为根,他引用或间接引用的都不是垃圾,其他都是垃圾

gcroot对象

  1. 栈中变量引用的对象
  2. 类静态变量引用的对象
  3. native方法引用的对象
  4. 常量引用的对象
public class Example {public static void main(String[] args) {// 栈中局部变量a指向的A对象是GC RootA a = new A();a.b = new B();a.b.c = new C();// 此时,a、a.b、a.b.c 都是存活的System.out.println(a.b.c.value);// 方法结束后,a 不再被引用,a、a.b、a.b.c 都会被回收}
}class A {B b;
}class B {C c;
}class C {int value = 10;
}

三色标记算法

是对可达性分析的实现!!!
cms和g1垃圾回收器都用它来实现并发标记,在此之前所有的垃圾回收器都是STW只有垃圾回收线程自己干活(且对可达性分析的实现是双色标记法),然后执行可达性分析标记出垃圾或非垃圾,也就是从gcroot开始一次dfs/bfs遍历,然后清除的时候再遍历一次把标记为垃圾的清理掉

然而随着发展内存变大堆变大,垃圾回收线程要执行很久,这意味着STW更久更无法接受,于是cms和g1开发出基于三色标记法实现并发的可达性分析,实现了垃圾回收线程(标记垃圾)和用户线程并发执行并且减少STW,

然而

引用

引用能力逐个下降:

  1. 强引用:无论如何oom都不回收
  2. 软引用:gc后内存仍然不足才会回收(可配合引用队列)
  3. 弱引用:只要gc就回收(可配合引用队列)
  4. 虚引用:随时都可能回收相当于没引用,必须配合引用队列使用,get重写为return null所以无法拿到从虚引用对象。一个对象是否有虚引用不会对其生存时间产生影响

因为各种引用本身就是个对象比如软弱虚,只有强引用不需要专门创建一个 引用对象引用 对象。

如下:
强:Object o=new Object();//所以正常使用的都是,通过当前虚拟机栈里的引用变量指向一个对象

软:SoftReference<Object> softRef = new SoftReference<>(new Object());

弱:WeakReference<Object> weakRef = new WeakReference<>(new Object());

虚:PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());

引用队列

当被引用的对象(obj)被gc回收,引用对象本身(比如new PhantomReference对象本身)会被回调放入你给的引用队列中,并且自动解除weakRef、phantomRef这些变量对引用对象本身的强引用,你可以通过检查引用队列的元素拿到引用对象本身

三种垃圾回收器

垃圾回收发生时,会让所有用户线程停下,等垃圾回收器执行完才能继续,因为期间会涉及到对象的整理,地址会发生改变,如果不停下用户线程,可能拿不到原来的对象或者拿错

串行

单核适用,单线程进行垃圾回收

吞吐量优先

多核适用,多线程
目标是让stop the word总时长最短,这样整体stw占用时长比例最小
0.2+0.2=0.4

响应时间优先

多核适用,多线程
支持用户线程和垃圾回收线程并发
目标是让每次stop the word时长都是最短,但总时长和次数可能变长 0.1+0.1+0.1+0.1+0.1=0.5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/26964.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

idea + Docker + 阿里镜像服务打包部署

一、下载docker desktop软件 官网下载docker desktop&#xff0c;需要结合wsl使用 启动成功的画面(如果不是这个画面例如一直处理start或者是stop需要重新启动&#xff0c;不行就重启电脑) 打包成功的镜像在这里&#xff0c;如果频繁打包会导致磁盘空间被占满&#xff0c;需…

探索Spring Cloud Config:构建高可用的配置中心

目录 认识Spring Cloud ConfigConfig Server读取配置文件步骤1&#xff1a; &#xff08;1&#xff09;创建config-server项目&#xff08;2&#xff09;在config-server中开启Config Server功能&#xff08;3&#xff09;在config-server配置文件进行相关配置&#xff08;4&a…

CSDN博客导出设置介绍

在CSDN编辑博客时&#xff0c;如果想导出保存到本地&#xff0c;可以选择导出为Markdown或者HTML格式。其中导出为HTML时有这几种选项&#xff1a;jekyll site&#xff0c;plain html&#xff0c;plain text&#xff0c;styled html&#xff0c;styled html with toc。分别是什…

代理对象中使用this

一、问题引出 业务逻辑层代码 Service public class DemoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements DemoService, ApplicationContextAware {// 用于从Spring容器中获取指定Bean的对象private ApplicationContext applicationContext;// 通过Appl…

视觉图像坐标转换

1. 透镜成像 相机的镜头系统将三维场景中的光线聚焦到一个平面&#xff08;即传感器&#xff09;。这个过程可以用小孔成像模型来近似描述&#xff0c;尽管实际相机使用复杂的透镜系统来减少畸变和提高成像质量。 小孔成像模型&#xff1a; 假设有一个理想的小孔&#xff0c;…

Hadoop之01:HDFS分布式文件系统

HDFS分布式文件系统 1.目标 理解分布式思想学会使用HDFS的常用命令掌握如何使用java api操作HDFS能独立描述HDFS三大组件namenode、secondarynamenode、datanode的作用理解并独立描述HDFS读写流程HDFS如何解决大量小文件存储问题 2. HDFS 2.1 HDFS是什么 HDFS是Hadoop中的一…

C语言(18)------------>函数(1)

本文介绍C语言函数的定义、标准库和库函数、自定义函数、函数中形式参数和实际参数。通过举例子和画图的方式分解每一个知识点&#xff0c;并结合生活案例和已知知识来解释函数知识。从而使得读者对C语言的函数理解更加深入&#xff0c;学习到C语言开发软件的一些实用技巧。 一…

apload-lab打靶场

1.提示显示所以关闭js 上传<?php phpinfo(); ?>的png形式 抓包&#xff0c;将png改为php 然后放包上传成功 2.提示说检查数据类型 抓包 将数据类型改成 image/jpeg 上传成功 3.提示 可以用phtml&#xff0c;php5&#xff0c;php3 4.先上传.htaccess文件&#xff0…

【Linux】TCP协议

文章目录 &#x1f449;TCP协议&#x1f448;TCP协议段格式确认应答机制窗口大小六个标记位连接管理机制三次握手四次挥手超时重传流量控制滑动窗口拥塞控制延迟应答捎带应答面向字节流粘包问题TCP异常情况TCP小结基于TCP应用层协议TCP与UDP的对比用UDP实现可靠传输 &#x1f4…

《HelloGitHub》第 107 期

兴趣是最好的老师&#xff0c;HelloGitHub 让你对编程感兴趣&#xff01; 简介 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。 github.com/521xueweihan/HelloGitHub 这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等&#xff0c;涵盖多种编程语言 Python、…

商城系统单商户开源版源码

环境配置 1.软件安装 宝塔安装系统软件:Nginx、MySQL5.6、PHP( PHP用7.1-7.4版本)、phpMyAdmin(Web端MySQL管理工具)。 2.配置mysql 设置mysql&#xff0c;在已安装的软件里面找到 mysql点击进行设置 3.修改sql-mode 选择左侧配置修改&#xff0c;找到里面的sql-mode&…

DeepSeek 与云原生后端:AI 赋能现代应用架构

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 在当今快速发展的互联网时代&#xff0c;云原生&#xff08;Cloud Native&#xff09;架构已成为后端开发的主流趋势。云…

Kafka面试题及原理

1. 消息可靠性&#xff08;不丢失&#xff09; 使用Kafka在消息的收发过程都会出现消息丢失&#xff0c;Kafka分别给出了解决方案 生产者发送消息到Brocker丢失消息在Brocker中存储丢失消费者从Brocker 幂等方案&#xff1a;【分布式锁、数据库锁&#xff08;悲观锁、乐观锁…

Python学习第十八天之深度学习之Tensorboard

Tensorboard 1.TensorBoard详解2.安装3.使用4.图像数据格式的一些理解 后续会陆续在词博客上更新Tensorboard相关知识 1.TensorBoard详解 TensorBoard是一个可视化的模块&#xff0c;该模块功能强大&#xff0c;可用于深度学习网络模型训练查看模型结构和训练效果&#xff08;…

idea生成自定义Maven原型(archetype)项目工程模板

一、什么是Maven原型&#xff08;Maven archetype&#xff09; 引自官网的介绍如下&#xff1a; Maven原型插件官网地址 这里采用DeepSeek助手翻译如下&#xff1a; Maven 原型 什么是原型&#xff1f; 简而言之&#xff0c;原型是一个 Maven 项目模板工具包。原型被定义为一…

Hadoop架构详解

Hadoop 是一个开源的分布式计算系统&#xff0c;用于存储和处理大规模数据集。Hadoop 主要由HDFS&#xff08;Hadoop Distributed File System&#xff09;、MapReduce、Yarn&#xff08;Jobtracker&#xff0c;TaskTracker&#xff09;三大核心组件组成。其中HDFS是分布式文件…

计算机毕设JAVA——某高校宿舍管理系统(基于SpringBoot+Vue前后端分离的项目)

文章目录 概要项目演示图片系统架构技术运行环境系统功能简介 概要 网络上许多计算机毕设项目开发前端界面设计复杂、不美观&#xff0c;而且功能结构十分单一&#xff0c;存在很多雷同的项目&#xff1a;不同的项目基本上就是套用固定模板&#xff0c;换个颜色、改个文字&…

利用 LangChain 和一个大语言模型(LLM)构建一个链条,自动从用户输入的问题中提取相关的 SQL 表信息,再生成对应的 SQL 查询

示例代码&#xff1a; from langchain_core.runnables import RunnablePassthrough from langchain.chains import create_sql_query_chain from operator import itemgetter from langchain.chains.openai_tools import create_extraction_chain_pydantic# 系统消息&#xff…

DeepSeek 助力 Vue 开发:打造丝滑的表单验证(Form Validation)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

云服务培训五-数据库服务

如上图所圈&#xff0c;这次主要学习云数据库RDS for MySQL、GaussDB和GeminiDB相关内容。 一、结构化数据、非结构化数据 选择数据库时&#xff0c;首先从模型上分析&#xff0c;是否涉及事务处理、复杂的查询关联&#xff0c;还是数据量大、有并发访问需求。同理&#xff0c;…