JVM第三节
反射
委派模式
- 本地实现 当调用次数比较少时
- 动态实现 调用次数较多时
public static void hello(int num){
System.out.println(" # "+num);
new Exception(" ---- ").printStackTrace();
}
public static void main(String[] args) throws Exception{
Class<?> stu = Student.class;
Method hello = stu.getMethod("hello",int.class);
hello.invoke(null,0);
}
之所以采用,委派模式,为了在本地实现与动态实现之间进行转换.
存在一个阈值(15) 当大于阈值是就采用 动态实现.
public static void main(String[] args) throws Exception{
Class<?> stu = Student.class;
Method hello = stu.getMethod("hello",int.class);
for (int i = 0; i < 20; i++) {
hello.invoke(null,i);
}
}
执行一次
执行多次
那为啥不直接使用 动态实现呢?
因为,动态实现是通过字节码实现的.前期的准备时间长.
所以在调用次数比较少时,采用本地实现.
(有点像快速排序和插入排序,在10以内插入排序要比快速排序快)
反射的消耗
反射是很消耗内存的
- 先取得 Class
- 遍历所有的方法得要 Method
- 调用 invoke()
主要开销
- 变长参数方法的Object数组
- 基本类型的装箱拆箱
- 方法内联
(方法内联指的是编译器在编译一个方法时,将某个方法调用的目标方法也纳入编译范围内,并用其返回值替代原方法调用这么个过程。)
Java内存对象的布局
当我们new 一个对象时,回去调用它的构造器.(如果没有显式声明,则会创建一个空参构造器)
而子类构造器又会去加载父类的构造器.
所以虽然父类中存在一些不能访问的字段(private),但仍然为它分配了内存.
这样做是很正常的,子类的方法调用父类的方法中有存在对该字段的访问.
每个对象都有一个对象头
对象头 = 标记字段 + 类型指针
在64位系统中,标记字段 , 类型指针都占 64bit 也就是需要 16B
像 int 它只占 4B ,如果用Integer 需要额外加上 16B
这里存在一个 优化
压缩指针
将 类型指针从 8B->4B ,从而将对象头 16B -> 12B
可以将 两个单元 看成 一个单元
例如 0 1 2 3 4 5
0 1 看成一个整体,如果要取 值的话 直接乘2 即可
内存对齐
内存模型
下面的代码中,将方法1,2 各执行一次
多线程中局部变量 (r1,r2) 可能存在多个结果
int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
}
public void method2() {
int r1 = b;
a = 2;
}
结果有 4 种
(1,0) (0,2) (0,0) (1,2)
其中 (1,2) 是很让我迷惑的,造成这种结果的原因
- 即时编译器的重排序 (遵循 as-if-serial)
- 处理器的乱序执行
- 内存系统的重排序
as-if-serial 在单线程程序中,要保证重排序的结果要与顺序执行的一致,
即给人一种程序顺序执行的错觉
这里先说一下,即时编译器的重排序.
它是编译器对代码的优化. 犹记当初学习编译原理时,各种啥的啥的…忘记了
有点对编译器优化的印象.将一些代码进行重新排列,然后还会对压根没有使用到的
对象去除.
总之,最后代码的真正执行顺序是与 正常的顺序执行 不大相同的
当然,在单线程因为遵循 as-if-serial 规则,不会出现 (1,2)这种情况
而多线程,可能会存在数据竞争(data race),Java语言则归咎于没有处理好并发
Java内存模型与 happens-before
为了避免数据竞争造成的影响,Java5 提出了 happens-before
如果说操作 X happens-before Y ,
那么 X 对于 Y 来讲是可见的
但是,在程序中,如果 X 对于 Y 没有影响,那么 X 与 Y 的执行顺序是不一定的
内存屏障 阻止指令的重排序,导致缓存的刷新
锁,volatile,final
Java 中的锁,可以避免多线程中对共享变量数据不一致的问题.
在解锁时,会清除缓存,让当前线程修改的数据对其它线程可见.
JIN 存在逃逸分析,如果JIN 发现有锁没锁都一样,那么它就会取消锁
volatile 也算是一种轻量级锁,它可以保证 该对象的可见性,但是保证不了原子性.
同时,频繁地访问,会造成很多次缓存的清除,从而降低性能
final 字段,因为是不可变的.所以会有一个 写屏障,防止对其值的修改.