上一篇说了类加载器、双亲委派机制、自定义类加载器
一、问题 ask
1. 自定义类加载器的上一层也就是父类加载器是谁
System.out.println(new MyClassLoader().getParent());
输入后果:sun.misc.Launcher$AppClassLoader@18b4aac2
2. 我没有指定 parent 呀 为什么不是 null 呢
咱们自定义类加载器继承了 ClassLoader,new MyClassLoader()的时候会先走类加载器的结构
// 无参结构 调用了 2 个参数的结构
protected ClassLoader() {this(checkCreateClassLoader(), getSystemClassLoader());
}
// 这里指定了 parent parent 从哪儿来 看 getSystemClassLoader()
private ClassLoader(Void unused, ClassLoader parent) {
// 指定 parent
this.parent = parent;
// 其余操作
if (ParallelLoaders.isRegistered(this.getClass())) {parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
// 返回的 scl 看 scl 怎么初始化的
initSystemClassLoader();
if (scl == null) {return null;}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
private static synchronized void initSystemClassLoader() {if (!sclSet) {if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 获取 classLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {oops = pae.getCause();
if (oops instanceof InvocationTargetException) {oops = oops.getCause();
}
}
if (oops != null) {if (oops instanceof Error) {throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
// 间接返回了 loader loader 是怎么来的
public ClassLoader getClassLoader() {return this.loader;}
// Launcher 类初始化的时候 构造方法里初始化了 load 默认是 appclassloader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
3. 间接获取零碎类加载器
ClassLoader.getSystemClassLoader(); -> appClassLoader
4. 能够本人指定 parent
// 写一个有参的结构 传入一个你想认的爹 而后调用 super 把 parent 传进去就行了
public MyClassLoader(ClassLoader parent) {super(parent);
}
5. 突破双亲委派?看一眼 不了解没关系 我也不了解 从别的中央抄过来的
- 重写 LoadClass 办法
因为双亲委派是在 loadClass 里边的逻辑指定的 -
什么时候突破?
- JDK1.2 之前 没有 findClass 必须重写 loadClass
- ThreadCotextClassLoader 能够实现根底类调用实现类代码,通过 thread.setContentClassLoader 指定
- 热启动 热部署
osgi、tomcat 都有本人的模块指定 classloader (能够加载同一类库不同版本)
比方两个 WebApplication 加载不同版本的同一个类
二、Linking
- verification
对文件格式进行校验 - preparation
给动态变量赋默认值 -
resolution
-
将类、办法、属性等符号援用解析为间接援用
常量池中的各种符号援用解析为指针、偏移量等内存地址的间接援用。
比方 java.lang.Object 他是个符号援用
如果想找他真是的内存数据 须要依据 java.lang.Object 先去常量池找见这个符号,而后再依据符号找对应的类型,这个就太绕了,间接把符号援用解析为间接援用的话 java.lang.Object 就变为 0x00012 内存地址,间接依据这个地址找类型就能够了
-
三、Initializing
调用初始化代码 <clinit> 给动态成员赋初始值
1. 面试题 输入后果是多少
/**
* @author 木子的昼夜
*/
public class Mr {public static void main(String[] args) {System.out.println(T.count);
}
}
class T{
// 成员变量
public static int count = 10;
public static T t = new T();
// 结构
private T(){count++;}
}
后果:11
如果赋值和 new 对象 换一下地位呢
/**
* @author 木子的昼夜
*/
public class Mr {public static void main(String[] args) {System.out.println(T.count);
}
}
class T{
// 成员变量
public static T t = new T();
public static int count = 10;
// 结构
private T(){count++;}
}
后果:10
本人想下这个过程 想不通能够公众号留言 我再进行解答 应该都能够想的通。。
2. 也就是
- 动态属性:load-> 默认值 -> 初始值
- 成员属性:new -> 申请内存 -> 默认值 -> 初始值
3. 这里有个面试题 单例 双重校验
/**
* @author 木子的昼夜
*/
public class Sig {
private static T03 t03;
public static T03 getInstance(){
// 先校验是否是 null
if (t03 == null) {
// 等锁
synchronized (T03.class){
// 接着校验是否是 null 因为可能多集体等锁
if (t03 == null){t03 = new T03();
}
}
}
return t03;
}
}
class T03{
}
这个单例模式有什么问题吗?
面试官会疯狂的暗示你 加 volatile .
接着会问 volatile 的作用:禁止指令重排 保障可见性
这里就是因为 咱们说的 new T03() 的时候 先分配内存 再赋初始值 再赋默认值
如果内存调配好了 另一个线程 if(t03 == null) 就是 false 了
而后就返回了 如果用 t03.count 那他还是 0 呢
当然 概率很低 然而这是会呈现的
让咱们看一下 T03 t03 = new T03(); 的过程
public class T03 {public int count =8;}
public class Test {public static void main(String[] args) {T03 t03 = new T03();
}
}
留神:这里须要应用 idea 的一个工具 ->jclasslib ByteCode Viewer 间接搜寻装置即可
- 先运行一下 main 办法 生成 class 文件
- 选中 Test 文件
-
view 视图 找 Show ByteCode By jclasslib
- 看生成过程
0 new #2 <T03> //(1)这句话就是在内存开拓一块空间 count = 0
3 dup
4 invokespecial #3 <T03.<init>> //(2)这句话就是初始化 count 值 count = 8
7 astore_1 //(3)这句话 就是把内存空间 地址援用 赋值给 t03 变量
8 retur
失常状况下 依照 (1) (2) (3) 的程序执行 是没有任何问题的 然而指令可能重排
可能会呈现 (1) (3)(2) 这种状况 就是咱们上边说的呈现问题的状况 所以要禁止指令重排 volatile
4. JMM 不是接妹妹 是 Java Memory Model
1. 先来一个存储器的层次结构图 来开开胃
2. 为什么会呈现数据不统一?
假如线程 1 应用 cpu1 把数据 x 读到了 L0、L1、L2 中的任何一个中央 这是 cpu 独享的
线程 2 应用 cpu2 把数据 x 也读到了 cpu2 的 L0、L1、L2 的任何一个中央
这时候就是一个数据 在内存中存储着 2 份了 其中一份批改了 那另一份没改 是不是就有问题了
3. 硬件层面怎么来解决这个问题 — 总线锁
在 cpu 读取数据 L3–>L2 都要过总线
在 cpu1 读取 x 的时候 给总线上一把锁 这时候 cpu2 不容许读
毛病:总线锁是锁总线,也就是我 cpu2 不拜访 x 我 cpu2 去拜访 y 也不能拜访 这样不是很正当吧
大家去洗脚了,你找了小丽,而后在门口上了一把锁,凭什么不让我去找小兰。。。
4. 硬件层面怎么来解决这个问题 — 一致性协定(各种各样)MESI、MSI MOSI、Synaose、Firefly、Dragon 等
个别大家聊的时候 是 MESI — intel CPU 实现协定
what is MESI ? is this !
- 数据存储在缓存行上 缓存行用额定两位 two bit 来标记状态,这里须要留神,如果数据夸缓存行了,那就很难用这种形式标记了,就须要应用总线锁了,呀呼嘿嘿
-
这个很难表白 我试着说一下子
1. 我是 cpu1, 我从主从读取了 x , 这时候只有我读没有其余 cpu 读,我会标记位 Exclusive
- 如果我读的时候,还有别的 cpu 在读,那我就标记位 Shared
- 如果我读回来,我做了批改,那我就标记位 Modified,这个时候他人就会变成 Invalid
- 如果我读回来,别的 cpu 不要脸的进行了批改(为啥我批改就不是不要懒 哈哈),那我就标记为 Invalid,这时候如果我要用这个数计算的时候,我会从新从内存读取一下
至于这些状态都是在什么时候变动的,这个学识就大了去了,主板上各种逻辑单元,我也不晓得是什么高科技实现的。
5. 再叙 – 缓存行
上边说了 缓存行的 2bit 标记状态 那什么是缓存行呢?
cpu 这个家伙呀,在读取数据的时候,是以缓存行为最小单位读取的
比方 int x =666;cpu 在读取 x 的时候不会只读取这四个字节,他会读取 x 及 x 当前的 N 个字节
这些个字节总的就叫缓存行,个别缓存行是 64 字节
缓存行问题:
我是 cpu1, 我读取 x 的时候,会把整个缓存行读取了
我批改了 x,我把缓存行状态改为 invalid,其实我没有
批改 y z w j 然而如果别的 cpu 在应用 y z w j 的话
就须要从新加载一遍
这个问题叫:伪共享:位于同一缓存行的两个不同数据被两个 CPU 锁定,产生相互影响。
这里有一个缓存行对齐的例子:
public class CacheLineTest01 {static T[] arr = new T[2];
static{arr[0] = new T();
arr[1] = new T();}
public static void main(String[] args) throws InterruptedException {final CountDownLatch cdl = new CountDownLatch(2);
final long count = 1_0000_0000L;
long start = System.currentTimeMillis();
// 起两个线程 别离批改 arr[0] arr[1] 对应对象 T 的属性
// 这个 arr 很大概率上会在一个缓存行 因为就 2 个 T 对象 每个对象就一个 Long 类型属性 总共不够 64 字节
new Thread(()->{for (long i = 0; i <count; i++) {arr[0].x = i;
}
cdl.countDown();}).start();
new Thread(()->{for (int i = 0; i < count; i++) {arr[1].x = i;
}
cdl.countDown();}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
class T{public volatile long x=0L;}
执行屡次输入后果:30、29、23、26、27、30
public class CacheLineTest02 {static T006[] arr = new T006[2];
static{arr[0] = new T006();
arr[1] = new T006();}
public static void main(String[] args) throws InterruptedException {final CountDownLatch cdl = new CountDownLatch(2);
long start = System.currentTimeMillis();
final long count = 1_0000_0000L;
new Thread(()->{for (long i = 0; i < count; i++) {arr[0].x = i;
}
cdl.countDown();}).start();
new Thread(()->{for (int i = 0; i <count; i++) {arr[1].x = i;
}
cdl.countDown();}).start();
cdl.await();
long end = System.currentTimeMillis();
System.out.println((end-start)/100);
}
}
// 加了一个对齐 也就是 Padding 这样 new2 个 T006 之后 相对不在一个缓存行
// 所以两个 cpu 批改属性 不会相互影响
class T006 extends Padding{public volatile long x=0L;}
class Padding{long a,b,c,d,e,f,g;}
执行屡次后果:14、16、16、14、17、14、15
很显著,第二段代码的执行工夫更快 这就是缓存行对齐对程序效率晋升的作用
能够看图:第一段代码 会走 invalid 每次都会去内存拿数据 再进行批改,而第二段代码会走 Modified 不须要去内存再一次拿数据
6. 乱序执行 01
用一句话总结:cpu 为了进步执行效率,会在一条指令筹备数据过程中,执行另一条不依赖于前一条指令的指令
能够看一个例子:cpu 在执行指令 1 的时候,指令 1 须要去内存拿数据,大家晓得内存读取数据耗时至多是 cpu 的 100 倍起步,这个工夫 cpu 等着吗?不能呀!那你电脑不卡成狗了吗。
这个工夫 cpu 会接着去判断下一条指令 2,看指令 2 是否依赖指令 1 的执行后果,如果依赖,接着看指令 3,如果不依赖就执行,顺次往下执行,直到指令 1 拿回来数据为止
举个例子:
小强做饭,第一道菜是土豆炖牛腩,第二道菜是拍黄瓜
如果是你,你会怎么做?
最容易些想到的是这样:
筹备土豆 -> 筹备牛腩 -> 放锅里 -> 看着它炖熟了 -> 盛进去 -> 筹备黄瓜 -> 拍黄瓜 -> 倒酱汁 -> 拍黄瓜做好了
然而咱们个别不会这么做,咱们跟 cpu 一样聪慧:
咱们会这样做:
筹备土豆 -> 筹备牛腩 -> 放锅里 -> 判断拍黄瓜这道菜要不要等土豆牛腩好了能力做?-> 不是 -> 筹备黄瓜 -> 拍黄瓜 -> 倒酱汁 -> 拍黄瓜做好了 -> 在做拍黄瓜的过程中你必定会看着土豆牛腩,避免干锅,如果拍黄瓜过程中土豆牛腩好了,你会先进行拍黄瓜,先去把牛腩捞进去(不然土豆块成土豆汤了),而后再去拍黄瓜
7. 乱序执行 02
合并写的概念:
拿生存中的例子就是,小强的土豆炖牛肉好了,能够放上桌让他人吃了,然而他感觉,这顿饭拍黄瓜跟土豆炖牛肉一起吃能力称之为“一顿饭”,留神这里一顿饭在 cpu 中能够对应一个数据。而后他就俩都做好了,拿一个大托盘,把 2 道菜合成了“一顿饭”放上桌,大家吃的不可开交。
学术上的概念大略意思就是:多个程序对同一个数据 x 进行操作,cpu 执行 x =x+1; 筹备把后果写回 L3 内存,然而他“自作聪明”的发现,后边如同还有一句 x = x+10; 所以他就等着 x =x+10; 这句执行完之后 再把一个最终后果写回 L3 内存,而不是写 2 次。
合并写的缓冲区 WCbuffer 很小很小 只有 4 个字节
8. 乱序执行 证实小程序
import java.util.concurrent.CountDownLatch;
public class TestOrder {
private static int a=0,b=0,x=0,y=0;
public static void main(String[] args) throws InterruptedException {
long count = 0;
for (;;){
count++;
CountDownLatch cdl = new CountDownLatch(1);
CountDownLatch cdlres = new CountDownLatch(2);
// 默认值
a=0;b=0;x=0;y=0;
new Thread(()->{
try {cdl.await();
a = 1;
x = b;
} catch (InterruptedException e) { }finally {cdlres.countDown();
}
}).start();
new Thread(()->{
try {cdl.await();
b = 1;
y = a;
} catch (InterruptedException e) { }finally {cdlres.countDown();
}
}).start();
cdl.countDown();
cdlres.await();
if (x==0&&y==0){System.out.println("存在乱序"+",一共执行:"+count+ "次");
break;
}
}
}
}
如果不重排呈现的后果应该是:
如果呈现 x ==0 && y == 0 的状况 阐明指令重拍了
想要证实,你就拿着这个程序,跑吧,跑一会儿,要有急躁
看看我执行的次数:40 多万次
9. 有序性保障
待续。。
有问题欢送 留言,可也在公众号留言(想赢快):