JVM学习

一.JVM
jvm定义:Java Virtual Machine,java程序的运行环境(java二进制字节码),java源代码j经过javaC编译成class字节码,class字节码使用java程序加载到JVM运行。
好处:

  1. 一次编写到处运行的基石。jvm屏蔽了字节码与底层操作系统(windows,linux等)之间的差异。
  2. 自动内存管理机制,提供垃圾回收功能。
  3. 数组下标越界检查,C语言没有越界检查,可能导致数组新元素覆盖程序的其他部分,覆盖其他代码的内存。
  4. 多态,面向对象编程的基石,代码扩展性得到极大提高,jvm在内部使用虚拟方法表的机制来实现了多态。
    比较:jvm,jdk,jre比较
    在这里插入图片描述
    在这里插入图片描述
    二.内存结构
    1.Program Counter Register程序计数器(寄存器)
    作用:记住下一条jvm指令的执行地址。它是对物理硬件的屏蔽和抽象,物理上是通过寄存器(cpu组件中读取最快的单元)实现的。
二进制字节码(JVM指令)java源代码
0: getstatic #20//PrintStream out = System.out;
3: astore_1//–
4: aload_1//out.println(1) ;
5: iconst_1//
6: invokevirtual #26//–
9: aload_1//out.println(2) ;
10: iconst_1//
11: invokevirtual #26//–
14: aload_1//out.println(3);
15: iconst_1//
16: invokevirtual #26//–
19: aload_1//out.println(4) ;
20: iconst_1//
21: invokevirtual #26//–
24: aload_1//out.println(5) ;
25: iconst_1//
26: invokevirtual #26//–
29: invokevirtual #26//–

jvm指令加载到内存结构中会分配对应的内存地址。
程序计数器取出对应地址的jvm指令,jvm指令经过解释器解释成机器码才能交给cpu去执行。

特点:

  • 线程私有。
  • 不会存在内存溢出。

多线程通过cpu的调度器分配,每个线程有不同的时间片,在一个线程的时间片用完后,程序计数器记住下一次轮到此线程执行的指令的内存地址,继续执行。

2.Java Virtual Machine Stacks (Java 虚拟机栈)
定义:

  • 每个线程运行需要的内存空间,称为虚拟机栈。
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧(线程正在执行的方法对应的栈帧),对应着每次方法调用时所占用的内存。

栈帧:每个方法(包括方法内的参数,局部变量,返回地址)运行时需要的内存。
图解定义:
在这里插入图片描述
问题辨析

1.垃圾回收是否涉及栈内存?
栈内存:一次次方法调用所产生的栈帧内存。栈帧内存:栈帧在每一次方法调用后都会弹出栈,也就是栈帧内存会被自动回收掉。所以不需要垃圾回收管理栈内存。
垃圾回收值会回收堆内存中的无用对象,栈内存不需要。

2.栈内存分配越大越好吗?
栈内存可以通过运行虚拟机代码时,通过一个参数指定。-Xss size

方法内的局部变量是否线程安全?
线程安全:局部变量对多个线程是共享的,还是属于某个线程私有的。
某个变量是否线程安全判断方法:1、是否是方法内的局部变量 2、是否逃离了方法的作用范围(有可能被别的线程访问到,不再线程安全)。
1.方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的。
2.局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
2.局部变量引用的是基本类型变量,不考虑线程安全。

场景1:有两个线程同时调用这个方法m1,会不会造成x值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的x变量是线程1私有的,线程2的栈帧中有对应的x变量是线程2私有的,各自加5000,互不影响。

public class Demo {
    public static void m1() {
        int x = 0;
        for (int i = 0; i< 5000;i++) {
            x++;
        }
        System.out.println(x);
    }
}

场景2:有两个线程同时调用这个方法m2,会不会造成x值得混乱呢。会:x是类变量,线程共享的,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1和线程2的栈帧中是对同一个变量x操作。

public class Demo {
    static int x = 0;
    public static void m2() {
        for (int i = 0; i< 5000;i++) {
            x++;
        }
        System.out.println(x);
    }
}

场景3:针对下面程序Demo1,有两个线程同时调用这个方法m1,会不会造成sb值得混乱呢。不会:x是方法内的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,线程1的栈帧中有对应的sb变量是线程1私有的,线程2的栈帧中有对应的sb变量是线程2私有的,互不影响。

针对方法m2, sb作为参数传入,其他的线程就有可能访问到它,sb不再是线程私有的,是线程共享的。如下示例:

 public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(new Runnable() {
            @Override
            public void run() {
                m2(sb);
            }
        }).start();
    }

主线程中创建了一个StringBuilder对象sb,又在新线程里面把sb作为参数传给了m2方法,主线程和新线程都在修改sb,sb是主线程和新线程共享的,线程不安全。

场景4:m3方法中的sb对象是否线程安全呢?答:不安全。
sb虽然是方法内的局部变量,但是作为方法的返回值返回了,这样其他的线程有可能拿到sb的引用对象,并发的修改它。

public class Demo1 {

    public static void m1() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }
    public static void m2(StringBuilder sb) {
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }

}

2.2 栈内存溢出

  • 栈帧过多导致栈内存溢出
    两个类循环引用导致栈内存溢出
  • 栈帧过大导致栈内存溢出

2.3线程运行中断
案例1:cpu占用过多
定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep pid (用ps命令进一步定位是哪个线程引起的cpu占用过高)输出的线程编号是10进制的
  • jstack pid 将此进程下jvm使用的线程和用户定义的线程都列出详情, 输出的线程id是十六进制的,可以将上一步得到的十进制线程id进行转换,对应后,可以根据线程id找到有问题的线程,进一步找到引起问题的代码行数

案列2:程序运行很长时间没有结果

  • jstack pid 输出的最后有 Found one Java-level deadlock:…
  • 定位到发生死锁的位置

3.本地方法栈
jvm调用本地方法时,给本地方法分配的内存。
本地方法:不是由java代码编写的方法,是有此C或者C++语言编写的代码。java方法不方便与底层操作系统打交道,而是通过调用本地方法与底层操作系统打交道。所有对象的父类Object类里面的方法声明就是用native声明的,是没有方法实现的,java通过native方法接口间接调用C或者C++的接口实现。

从下面说道的堆和方法区是线程共享的
4. Heap 堆
通过new关键字,创建对象都会使用堆内存
特点:
1)它是线程共享的,堆中对象都需要考虑线程安全问题 2)有垃圾回收
4.2 堆内存溢出
OutOfMemoryError Java heap space
堆空间参数:-Xmx

4.3堆内存诊断
1.jps工具
查看当前系统中有哪些java进程
2.jmap工具
查看堆内存使用情况 jmap -head pid
3.jconsole工具
图形界面的,多功能检测工具,可以连续检测

案例:垃圾回收后,内存占用仍然很高

  1. 方法区

定义
组成
5.3 方法区内存溢出
1.8以前会导致永久代内存溢出
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m
1.8以后会导致元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspace=8m

类加载器:用来加载类的二进制字节码
ClassWriter 用来生成类的二进制字节码
场景:动态产生class,并加载这些类的场景是很多的
Spring:用到字节码技术,都会用到Cglib,生成的代理类,是spring中aop的核心。
myBatis:用到字节码技术,都会用到Cglib。 产生map接口的实现类。

5.4 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

5.5 StringTable
特性

  • 常量池中的字符串仅是符号,第一次用到时才变成变量
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    1)1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
    2)1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,放入串池,会把串池中的对象返回。

5.6 StringTable的位置
1.6方法区中永久代中,常量池的一部分
1.7-8 堆中
永久代回收效率很低,只有fullGC才会回收,等到老年代空间不足时,才会触发FullGC。java应用程序中大量的字符串常量,会分配到StringTable中,如果StringTable回收效率不高,会占用大量的内存。
基于这样的缺点,StringTable被转移到堆中。MinorGC就可以触发,大大减轻对内存的压力。

5.7 StringTable 垃圾回收

当内存空间不足时,StringTable中没有被引用到的字符串常量仍然会被垃圾回收。

5.8 StringTable 性能调优

  • 调整-XX:StringTableSize=桶个数 hash查找会变快
  • 考虑将字符串对象是否入池

6.直接内存 Directory Memory

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不受JVM内存回收管理
    在这里插入图片描述

java代码不能运行系统缓存区,而是在堆里面分配一块java缓冲区,将数据从系统缓存区读到java缓冲区。

在这里插入图片描述
ByteBuffer.allocateDirect(_1Mb);在系统内存分配一块直接内存,java代码可以直接访问。java代码和系统都可直接访问。少了一次缓冲区的复制操作,速度提升一倍。

直接内存溢出
OutOfMemory: Direct buffer memory

6.3 直接内存分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferrenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存

直接内存禁用显示回收对直接内存的影响。

垃圾回收
1.如何判断对象可以回收
1.1引用计数法,循环引用导致垃圾回收
1.2可达性分析算法
确定一系列根对象,根对象:肯定不能被当成垃圾回收的对象。
垃圾回收前,对堆内存中的所有对象进行一遍扫描,确定每一个对象是不是被根对象做直接或者间接的引用,如果是,此对象就不能被垃圾回收,否则,就被作为垃圾将来被回收。

  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。

  • 扫描堆中的对象,看是否能够沿着GC Root对象为七点的引用链找到该对象,找不到表示可以回收。

  • 哪些对象可以作为GC Root?

  • Memory Analyzer: 快速的多功能的java堆分析工具.
    -先用jmap获取堆内存快照。jmap -dump:format=b,live,file=1.bin pid
    1)System Class(系统类):启动类加载器加载的类,是一些核心类,代码运行过程中都会用到的
    2)Native Stack jvm执行操作时候会调用操作系统的方法,操作系统在执行时引用的java对象也是可以作为根对象。
    3)Thread 活动线程中使用的对象不能被当成垃圾回收,线程运行时由一次次的方法调用组成,每次方法调用都会产生栈帧,栈帧内使用的对象(局部变量引用的对象,方法参数引用的对象)被作为根对象。
    4)Busy Monitor synchronized正在加锁的对象不能被回收,因为后面还要释放锁。
    1.3 五种引用
    1)强引用–只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
    new 一个对象通过复制运算符赋值给了一个变量,此变量就强引用了创建的对象。只要沿着GC Root链能够找到此对象,就不会被垃圾回收。
    2)软引用(SoftReference) --仅有软引用引用该对象时,在垃圾回收后,内存仍不足会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身。
    当没有强引用引用对象,只有软引用引用对象,当发生垃圾回收,并且内存不够时候,会将软引用引用的对象回收掉。
    应用:不重要的资源可以使用软引用
    3)弱引用(WeakReference)–
    当没有强引用引用对象,只有弱引用引用对象,当发生垃圾回收,不管内存是否充足时候,会将弱引用引用的对象回收掉。可以配合引用队列来释放弱引用自身。
    软弱引用配合引用队列进行工作:软弱引用自身也是一个对象,如果在创建软弱引用时给它们分配了队列,当软弱引用引用的对象被回收的时候,软弱引用就会进入对应的队列。
    软弱引用被回收时,需要通过引用队列找到它们,再进行处理。
    4)虚引用
    必须配合引用队列使用,创建ByteBuffer的实现类对象时,就会创建一个Cleaner的虚引用对象,ByteBuffer会分配一个直接内存,然后将直接内存地址给虚引用对象。当ByteBuffer被回收时,锁使用的直接内存不会被回收,让虚引用对象进入引用队列,虚引用所在的引用队列由一个叫ReferenceHandler的线程定时寻找是否有新入队的Cleaner,如果有就会调用Cleaner的虚方法。虚方法根据前面记录的直接内存地址,调用Unsafe.freeMemory将直接内存释放掉。
    虚引用引用的对象被垃圾回收时,虚引用对象本身就会被放入引用队列,从而间接的用一个线程来调用虚引用对象的方法,然后调用Unsafe.freeMemory将直接内存释放掉。
    5)终结器引用
    当对象重写了终结方法finalize,并且没有强引用引用它时,就可以被垃圾回收。当没有强引用引用对象时,虚拟机为我们创建对象对应的终结器引用,当对象没有被强引用引用时,终结器引用对象也加入一个引用队列,由优先级很低的一个线程finalizeHandler,查看队列中队列中是否有终结器引用,如果有就找到对应的对象并调用其finalize方法,调用完后,等真正发生垃圾回收,就可以将此对象占用的内存回收掉。效率很低,线程有限地低

  • 垃圾回收算法
    标记清除算法(Mark Sweep):速度较快,会造成内存碎片。
    标记整理:速度慢,没有内存碎片。
    复制:不会有内存碎片,需要占用双倍内存空间。

  • 分代垃圾回收
    堆内存将内存区域分成新生代(朝生夕死)和老年代(长时间存活的对象)。
    新生代又分成伊甸园、缓存区From和幸存区To。
    新生代垃圾回收:Minor GC
    老年代垃圾回收:Full GC
    对象首先分配在伊甸园区域
    1)新生代空间不足时,触发Minor GC,伊甸园和缓存区From存活的对象使用复制算法复制到缓存区To中,存活的对象年龄加一,并交换From和To。
    2)Minor GC会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
    3)当对象寿命超过阈值时,会晋升到老年代,最大寿命是15(4bit)
    4)当老年代空间不足,会先触发Monir GC后,空间仍然不足,会触发Full GC (STW时间更长)
    3.1 相关VM参数
    堆初始大小:-Xms
    堆最大大小:-Xmx或-XX:MaxHeapSize
    新生代大小:-Xmn或-XX:NewSize=size+ -XX:MaxNewSize=size
    幸存区比例(动态):-XX:InitialSurvivorRatio=ratio 和-XX:+UseAdptiveSizePolicy
    幸存区比例:-XX:SurvivorRatio=ratio
    晋升阈值:-XX:MaxTenuringDistribute=threshold
    晋升详情:-XX:+PrintTenuringDistribution
    GC详情:-XX:+PrintGCDetails -verbose:gc
    FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

  • 垃圾回收器
    4.1串行 -XX:+UseSerialGC=Seria(工作在新生代采用复制算法垃圾回收)l + SerialOld Serial (工作在老年代采用标记整理算法垃圾回收)
    1)单线程 2)堆内存较小,适合个人电脑
    4.2吞吐量优先 -XX:+UseParallelGC(新生代的垃圾回收器,复制算法) ~ -XX:+UseParallelOldGC(工作在老年代采用标记整理算法垃圾回收)
    1)多线程 2)堆内存较大,多核CPU 3)让单位时间内的STW的时间最短
    4.3响应时间优先 -XX:+UseConcMarkSweepGC(标记清楚算法的垃圾回收器)~ -XX:+UseParNewGC ~ SerialOld
    1)多线程 2)堆内存较大,多核CPU 3)尽可能让单次STW时间最短
    4.4 G1
    jdk9开始作为默认的垃圾回收器取代了CMS
    适用场景
    同时注重吞吐量和低延迟,默认的暂停目标是200ms
    超大堆内存,会将堆划分为多个大小相等的region
    整体上是标记+整理算法,两个区域之间是复制算法
    1)垃圾回收阶段
    在这里插入图片描述
    2)Young Collection
    会STW
    Eden区总内存占满后,会使用复制算法垃圾回收将存活对象复制到suvivor区,suvivor区总内存占满后,会使用复制算法垃圾回收将年龄满的对象复制到old区,年龄比满的复制到新的survior区。
    3)Young Collection + CM(concunrent Marking)
    在Young GC时会进行GC Root的初始标记
    老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
    -xx:InitiatingHeapOccupancyPercent=percent(默认45%)
    4)Mixed Collection
    会对E、S、O进行全面垃圾回收
    最终标记(Remark)会STW,并发标记的时候一些工作线程可能又产生了新的垃圾,所以需要最终标记。
    拷贝存活(Evacuation)会STW,不是所有的老年代都会拷贝,会根据最大暂停时间(-:MaxGCPauseMillis=ms)有选择的将回收价值最高的老年代进行垃圾回收(如果堆内存太大,老年代垃圾回收占用时间过多),时间足够会全部进行垃圾回收。
    5)Full GC辨析
    SerialGC
    新生代内存不足发生的垃圾收集-minor gc
    老年代内存不足发生的垃圾收集-full gc
    ParallelGC
    新生代内存不足发生的垃圾收集-minor gc
    老年代内存不足发生的垃圾收集-full gc
    CMS
    新生代内存不足发生的垃圾收集-minor gc
    老年代内存不足
    G1
    新生代内存不足发生的垃圾收集-minor gc
    老年代内存不足
    CMS/G1老年代内存不足引发的垃圾收集分两种情况:
    1.老年代内存占堆内存比例达到45%,触发并发标记和混合收集。垃圾回收速度高于垃圾产生速度,不是full gc,属于并发垃圾收集阶段,暂停时间短。
    2.垃圾回收速度跟不上垃圾产生速度,并发收集退化为串行收集。响应时间变长。
    也就是并发收集失败后才进入full gc,GC日志打印有full gc字样。
    6)Young Collection跨代引用
    找到根对象,再可达性分析,再找到存活对象,复制到幸存区。
    根对象部分存在于老年代,遍历蒸饭柜老年代效率低,采用卡表技术把老年代的区域再进行划分。分成
    一个个大小为512k的card,如果老年代有个对象引用了新生代对象,对应的卡标记为脏卡,这样做GC root遍历的时候就不用遍历整个老年代,而是遍历脏卡,减少搜索范围,提高效率。

  • 卡表与RememberedSet

  • 在引用变更时通过post-write barrier + dirty card queue

  • concurent refinement threads更新Remembered Set

7)Remark
pre-wrire barrier + satb_mark_queue
当对象的引用发生改变时,jvm给其加入一个写屏障。经对象加入一个队列中,变成灰色,表示还没有处理完,等并发标记结束后,进入最终标记,从队列中取出再进行一次检查,再变成黑色。

8)JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-xx:+UseStringDeduplication
String s1 = new String(“hello”);
String s2 = new String(“hello”);

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复。
  • 如果它们值一样,让它们引用同一个char[]
  • 注意,与String.intern()不一样
    -String.intern()关注的是字符串对象
    -而字符串去重关注的是char[]
    -在jvm内部,使用了不同的字符串表

9)JDK 8u40并发标记类卸载
所有对象经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它它所加载的所有类
-xx:+classUnloadingWithConcurentMark默认启用
很多框架程序都使用了自定义的类加载器,很有用,jdk的启动、扩展、应用程序类加载器不会使用的
10)DK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象。
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

11)JDK 9并发标记起始时间的调整

  • 并发标必须在堆空间占满前完成,否则退化为FullGC
  • JDK 9之前需要使用 -xx:InitiatingHeapOccupancyPercent
  • jdk 9可以动态调整
    -xx:InitiatingHeapOccupancyPercent用来设置初始值
    进行数据采样并动态调整-xx:InitiatingHeapOccupancyPercent=percent
    总会添加一个安全的空档空间
    减少并发垃圾回收退化为fullGC

垃圾回收调优

类加载与字节码技术

  1. 类文件结构
    在这里插入图片描述
    1.1 魔数
    在这里插入图片描述
    1.2 版本
    在这里插入图片描述
    1.3 常量池
    在这里插入图片描述
    在这里插入图片描述
    图解方法执行流程
    1)原始java代码
    2)编译后的字节码文件
    javap -v demo.class
    3)常量池载入运行时常量池(是方法区的一部分)
    把class文件中的常量池信息载入运行时常量池,将来用到方法引用、成员变量引用,具体数值时候去运行时常量池找到。
    4)方法的字节码载入方法区
    5)main线程开始运行,分配栈帧内存
    局部变量表,操作数栈决定了需要的栈帧内存大小
    6)执行引擎读取方法内的数据

    多态原理小结
    当执行invokevirtual指令时,
    (1).先通过栈帧中的对象引用找到对象
    (2). 分析对象头,找到对象的实际Class
    (3). Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
    (4).查表得到方法的具体地址
    (5).执行方法的字节码

  2. 字节码指令

  3. 编译器处理
    javac编译器在编译期间对字节码的优化和处理
    语法糖,就是指java编译器把.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的额外福利。为了便于阅读几下给出的是几乎等价的java源码形式,并不是编译器还会转换出中间的java源码,切记。
    3.1默认构造器
    在这里插入图片描述
    3.2自动拆装箱
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    3.3 泛型集合取值
    泛型也是在jdk1.5开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当做了object类型来处理:
    在这里插入图片描述
    所以在取值时,编译器真正生成的字节码文件中,还要额外做一个类型转换的操作;
    在这里插入图片描述

泛型擦除的是字节码上的泛型信息(方法体),可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息。
局部变量类型表:方法的参数的一些泛型信息
list变量对应LocalVariableTypeTable的字节码信息:
在这里插入图片描述
这种LocalVariableTypeTable的泛型信息虽然没有被擦除,但是不能通过反射拿到泛型信息。只有方法的参数和返回值上带有的泛型信息才能被拿到。
在这里插入图片描述
3.4 可变参数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.5 foreach循环
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.6 switch字符串
3.7switch枚举
3.7枚举类
3.8 try with resourses
3.9
3.10方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
父子类的返回值完全一致
子类返回值可以是父类返回值的子类

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
3.11匿名内部类
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4. 类加载阶段
4.1类加载
java源代码编译成字节码后,通过类加载器将类的字节码载入方法区。内部采用C++的instanceKlass描述java类,它的重要field有:
在这里插入图片描述
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的

4.2链接

  • 验证-验证字节码是否符合jvm规范,安全性检查
  • 准备:为static变量分配空间,设置默认值
    static变量在jdl1.7之前存储于instanceKlass末尾,从jdk7开始,存储于——java_mirror末尾
    static变量分配空间和赋值是两个步骤,分配空间是在准备阶段完成,赋值在初始化阶段完成
    如果static变量是final的基本类型以及字符串常量的,那么编译阶段就确定了,赋值在准备阶段完成
    在这里插入图片描述
    如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
    在这里插入图片描述
  • 解析 将常量池中的符号引用解析为直接引用
    4.3初始化
    () v方法
    初始化即调用()v,喜怒及会保证这个类的构造方法的线程安全
    发生的时机
    概括的说,类初始化是懒惰的
    main方法所在的类,总会被首先初始化
    首次访问这个类的静态变量或静态方法时
    子类初始化,如果父类还没有初始化会引发
    子类访问父类的静态变量,只会触发父类的初始化
    Class.forName
    new会导致初始化
    不会导致类初始化的情况
    访问类的static final静态常量(基本类型和字符串)不会触发初始化
    类对象.class不会触发初始化
    创建该类的数组不会触发初始化
    类加载器的loadClass方法
    Class.forName的参数2为false时
    练习
    懒惰式单例模式初始化
  1. 类加载器
    在这里插入图片描述
    5.1 启动类加载器
    通过特殊的虚拟机参数把自己编写的类交由启动类加载器加载
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    5.1 扩展类加载器
    我们自己编写的一个类是由应用程序类加载器加载的,因为启动类加载器和扩展类加载器找不到这个类。

同名的类分别放在扩展类加载器和应用程序类加载器类路径下,哪个类加载器会加载它?
答:扩展类加载器。双亲委派模型决定的
5.3双亲委派模型
就是指调用类加载器的loadClass方法时,查找类的规则
注意:这里的双亲翻译成上级似乎更合适,因为他们并没有继承关系

5.4线程上下文类加载器
我们在使用jdbc时,都需要加载Driver驱动,不知道你注意没有,不写
Class.forName(“com.mysql.jdbc.Driver”)
在这里插入图片描述
先不看别的,看看DriverManager的类加载器:
System.out.println(DriverManager.class.getClassLoader());
在这里插入图片描述

5.4自定义类加载器
什么时候需要自定义类加载器
1)想加载非classpath任意路径路径下的类文件
2)都是通过接口来实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常用于tomcat容器(同名同包的类也可以在tomcat运行)
实现自定义类加载器的步骤
1.继承ClassLoader父类
2.要遵从双亲委派机制,重写findClass方法
注意:不是重写loadClass方法,否则不会走双亲委派机制
3.读取类文件的字节码
4.调用父类的defineClass方法来加载类
5.使用者调用该类加载器的loadClss方法

6.运行期优化
6.1即时编译
分层编译
JVM将字节码执行状态分成了5个层次:
0层:解释执行(Interpreter)
字节码被加载到虚拟机后,靠解释器一个字节一个字节的解释执行,解释成一个个真正的机器码。当字节码被反复调用次数达到一定程度后,启用编译器(C1和C2编译器)对字节码进行编译执行
1层:使用C1即时编译器编译执行(不带profiling)
2层:使用C1即时编译器编译执行(带基本profiling)
3层:使用C1即时编译器编译执行(带完全的profiling)
4层:使用C2即时编译器编译执行
profiling是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等

即时编译器和和解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
解释器是将字节码解释为针对所有平台都通用的机器码
JIT会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取采用解释执行的方式运行;另一方面,对于占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下,Interpreter<C1<C2,总的目标是发现热点代码(hotspot名称的由来),优化之。
C2编译器做的优化,刚才的一种优化手段称之为【逃逸技术】,发现新建的对象是否逃逸,可以使用-xx:-DoEscapeAnalysis关闭逃逸分析。
方法内联
在这里插入图片描述
在这里插入图片描述
字段优化
6.2反射优化

内存模型
1.java内存模型
java内存结构和java内存模型是不一样的,java内存模型是Java Memory的意思。
简单的说,JMM定义了一套在多线程读写共享数据时(成员变量,数组),对数据的可见性、有序性和原子性的规则和保障。
1.1原子性
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做500次,结果是0吗?
在这里插入图片描述
1.2问题分析
以上的结果可能是正数,负数或者0。为什么呢,因为java中对静态变量的自增,自减并不是原子操作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果是单线程以上8行代码是顺序执行(不会交错),没有问题:
在这里插入图片描述
但多线程下这8行代码可能交错运行,原因:操作系统的的多线程模型是抢先式多任务,线程轮流拿到cpu的使用权。
在这里插入图片描述
在这里插入图片描述
2.可见性
2.1退不出的循环
先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
在这里插入图片描述
在这里插入图片描述
2.因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存到自己的工作内存中的告诉缓存中,减少对主存中run的访问,提高效率。。
在这里插入图片描述
3.1s后,main线程修改了run的值,并同步至主存,而t是从自己的工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
在这里插入图片描述
2.2解决方法
在这里插入图片描述
可见性,保证的是在多个线程之间,一个线程对volitile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况;
在这里插入图片描述
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是属于重量级操作,性能相对更低。
如果在前面的死循环中加入System.out.println()会发现即时不加volitile关键字修饰符,线程t也能正确看到对run变量的修改了,为什么?
在这里插入图片描述
synchronized防止当前线程从高速缓存获取run的值,破坏了JIT的优化

在这里插入图片描述
3.有序性
3.1诡异的结果
在这里插入图片描述
I_Result是一个对象,有一个属性r1用来保存结果,问,可能有几种?
在这里插入图片描述在这里插入图片描述
这种现象就做指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量的测试才能发现:java并发压测工具jcstress
3.2解决方法
volitile修饰的变量,可以禁用指令重排

3.2有序性的理解
在这里插入图片描述
在这里插入图片描述
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的double-checked locking模式实现单例

public final class Singleton {
    private Singleton() {}
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        //实例没创建,才会进入内部的synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                //也许有其他线程已经创建实例,所以再创建一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}

以上的实现特点是:
懒惰实例化
首次使用getInstance()才使用synchronized加锁,后续使用无需加锁。
但在多线程条件下,上面的代码是有问题的,INSTANCE= new Singleton()对应的字节码为:
在这里插入图片描述
其中47两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按照如下时间序列执行:
在这里插入图片描述
在这里插入图片描述
3.4 happens-before
happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结:
下面所有的变量都是指成员变量或静态成员变量。
1)线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
在这里插入图片描述
2)线程对volatile变量的写,对接下来其他线程对该变量的读可见
在这里插入图片描述
3)线程start前对变量的写,对该线程开始后对该变量的读可见
在这里插入图片描述
4)线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用t1.isAlive()或t1.join()等待它结束)
在这里插入图片描述
5)线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
在这里插入图片描述
在这里插入图片描述
6)对变量默认值(0,false,null)的写,对其他线程对该变量的读可见
7)具有传递性,如果x hb-y,且y hb->z 那么有 x hb->z

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值