JVM内存模型,垃圾回收等学习笔记

发布于:2023-01-04 ⋅ 阅读:(470) ⋅ 点赞:(0)

JVM内存结构

JVM的内存结构大概分为:

1.堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

2.方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。jdk1.7之前对方法区的实现为永久代(PermGen,JVM内存),jdk1.8之后对方法区的实现改为元空间(MetaSpace,直接内存),并且将常量池和字符串池放入堆中,但是运行时的常量池在元空间中。

3.虚拟机栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。

4.本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。

5.程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

1.垃圾回收

  • 垃圾回收的目标位置是堆

1.1 判断垃圾

  1. 引用计数算法

有一个引用计数器,记录了每个对象被引用的次数,为0则被回收,但是若两个对象相互引用,那么这两个对象就算不用了,也不会被回收。

  1. 可达性分析法

判断对象是否可以通过gcroot强引用,是否被gcroot关联,若没有关联或者非强软引用,那么会直接被判断为垃圾。

  1. 四种引用
  • 强引用

GCroot引用的对象成为强引用对象,不管什么情况,只要引用没有取消,都不会被回收。

  • 软引用

当其他的gcroot强引用取消之后,只剩一个软引用时,如果发生垃圾回收,并且内存满了,那么就会将软引用对象回收掉

  • 弱引用

当其他的强引用取消了之后,只剩一个弱引用,那么发生垃圾回收时,不管内存是否充足,都会回收掉弱引用对象

  • 虚引用

虚引用相当于无引用,使对象无法被引用,需要配合引用队列使用,将虚引用的对象创建并且加入到引用队列等待下次jvm垃圾回收

  • 终结器引用

用于实现对象的finalize方法,不推荐使用,因为finallize方法优先级很低,可能会导致对象被加入引用队列但是内存一直得不到释放,不推荐使用。

1.3.垃圾回收算法

1.3.1 标记清除算法

将没有被gcroot关联的对象标记起来,然后统一进行清除。

但是这个算法有一个缺点,若有一个对象要进来,但是对象比两个存活对象之间的剩余空间大,那么就会往后寻找能容纳的碎片空间,长久这样,会存在很多很小的内存碎片,就会导致空间“碎片化”,缩小存储空间。

1.3.2 标记整理算法

此算法首先标记,然后将可回收对象清除之后,对碎片空间进行整理,将存货对象向前移动,这样就不存在碎片空间了,不会造成内存空间碎片化。

但是这个算法的效率会低很多,因为整理阶段牵扯到内存区块的移动,垃圾多且分散的时候速度就会变得很慢。

1.3.3 标记复制算法

这个算法将内存分为两个区域,一个是From区域,一个是To区域,但是to区域必须保持为空。

第一步也是将垃圾对象标记,然后将存活对象复制到to区域,对from区域进行清除,然后将to作为下一个from区域,将from作为下一个to区域,这样使得to区域一直为空。这个方法是以空间换取时间,也不会产生碎片化空间,但是可用的内存空间就减半了,在内存比较小的情况下效率很低。

1.3.4 总结

标记清除算法:速度快,但是会造成内存碎片

标记整理:不会造成内存碎片,但是效率较低

标记复制:不会造成内存碎片,速度比较快,但是可用空间减半

1.4 分代回收

上面讲完了垃圾回收的算法,那么这些在jvm是怎么实现的呢,分代回收就是目前主流的HotSpot的jvm对垃圾回收的实现。

他将内存区域分为两部分:

  • 新生代:伊甸园Eden,幸存区From,幸存区To
  • 老年代OldGen

朝生夕死的放在新生代,长久存在的放在老年代。并且针对不同区域采用不同算法。

当一个新对象诞生时,首先放在伊甸园Eden区域,

当伊甸园满时,触发一次垃圾回收MinorGCroot,对eden中的垃圾进行标记,然后采用标记复制算法,将eden中的存活对象复制进to区域,并且寿命+1,

然后清除eden区域,交换from和to区域。

当第二次eden满时,触发第二次MinorGC,要在eden和幸存区From中寻找垃圾进行标记,然后将存活对象复制到To区域,并且寿命+1.

当幸存区From中的对象达到一定的寿命时(默认是15),将会进入老年代。

当有一个新对象进来,新生代的空间不足,并且MinorGC之后空间也不足时,会将From区中寿命最长的对象提前放入老年代。

当老年代的空间也不足时,将会触发FullGC,进行全面的垃圾回收,在调优时应尽量避免FullGC,而是将垃圾在MinorGC的时候就回收掉。

若FullGCroot之后空间任然不足,那么会抛出内存空间溢出异常java.lang.OutOfMemoryError: Java heap space

  • MinorGC会引发Stop the world,会暂停其他线程的工作,因为在进行复制的时候涉及指针转移等操作,必须暂停其他各线程工作,垃圾回收结束,用户线程才能恢复运行。
  • 对象的最大寿命是15(4bit),超过阈值会晋升至老年代。
  • 当老年代空间不足触发FullGC也会stop the world,并且暂停时间更长。

1.5 垃圾回收器

1.5.1 串行

  • 单线程
  • 堆内存较小,cpu线程比较少
  • 使用的是标记整理方法
  • -XX:+UseSerialGC=Serial+SerialOld

1.5.2 吞吐量优先

  • 多线程
  • 堆内存较大,需要多核cpu支持
  • 让单位时间内,STW暂停时间最短
  • 使用的是标记整理算法
  • -XX:+UseParallelGC~-XX:+UseParallelOldGC 开启其中一个即可全部开启
  • -XX:ParallelGCThreads=n 指定垃圾回收线程数
  • -XX:UseAdaptiveSizePolicy 动态调整eden和幸存者区的比例
  • -XX:GCTimeRatio=ratio 指定垃圾回收占用时间比例,1/(1+ratio)默认为99,也就是1%
  • -XX:MaxGCPauseMillis=ms 指定STW最大暂停毫秒数,默认200ms

1.5.3 响应时间优先

  • 多线程
  • 堆内存较大,多核cpu
  • 实现标记清除算法
  • 尽可能让单次STW暂停时间最短
  • -XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~SerialOld 并发标记整理(CMS),不同于并行:并发是一个处理器处理多个任务,并行是多个处理器处理多个任务;并行在同一时间可以同时执行多个任务,并发在同一时间只能执行一个任务
  • -XX:ConcGCTreads=threads threads建议设置为线程数的1/4
  • -XX:CMSInitiatingOccupancyFraction=percent ,堆内存占用达到percent时,进行CMS垃圾回收
  • -XX:+CMSScanvengeBeforeRemark 重新标记之前进行一次新生代的垃圾回收+表示打开

过程如下:

  1. 初始标记只需要标记表层的垃圾,不需要管他们的引用与否,所以时间很短,STW的时间也很短;
  2. 并发标记阶段需要从GCRoot向下溯源,标记深层的垃圾,耗时长;
  3. 然后是并发预处理阶段,这个阶段的目的是减少下一个阶段重新标记的时间,由于并发标记阶段用户线程没有挂起,所以标记完之后可能有新生代晋升到了老年代,或者有新生代引用了老年代等情况,这个阶段就是对这些dirty对象进行处理,并且还可能会进行minorGC,减小重新标记阶段遍历新生代的时间;
  4. 重新标记阶段对预处理之后的垃圾进行标记,该阶段会STW,时间长短取决于并发预处理阶段;
  5. 最后的并发清理阶段对上面几个阶段标记的垃圾进行清理,会产生碎片空间,并且因为是并发清理,所以在清理过程中会产生新的垃圾,被称为“浮动垃圾”,只有等下一次GC去清理。全部结束后会重置算法内部的相关数据,为下一次GC做准备。

缺点:1.因为是标记清理算法,所以会产生很多碎片化空间,如果碎片过多,又可能因为空间不足触发FullGC,这样会导致STW耗时进一步加长;

2.CMS需要预留空间,保证在CMS回收时用户有足够的空间使用,如果CMS预留空间不够,CMS会引发SerialOld回收老年代垃圾,会导致停顿时间很长。

优点:只有两个STW,并且暂停时间很短,其他标记清理都是并发进行的,不影响程序运行,注重响应速度

1.5.4 G1(Garbage First)

jdk9默认垃圾回收器,对cms

  • 适用场景:
    • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
    • 超大堆内存,会将堆划分 为多个大小相等的Region
    • 整体是标记整理算法,区域之间是复制算法
  • 相关JVM参数
    • -XX:+UseG1GC 使用G1垃圾收集器
    • -XX:G1HeapRegionSize=size 指定区域(Region)的大小
    • -XX:MaxGCPauseMillis=time 最大暂停时间

G1的分区思想

  • 每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代Old。存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。如上图所示,区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

G1的三种回收阶段(Oracle工程师提出)

  • Young Collection,YoungCollection,MixedCollection三个阶段循环
  1. YoungCollection

  • 与分代回收的MinorGC相同,会STW
  • 当Eden区域满时触发YoungCollection,以拷贝的算法将对象放入Survivor区
  • 将S区年龄到阈值的对象存入Old区
  1. YoungCollection+ConcurrentMarking
  • 在YoungGC时会进行GCRoot的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由JVM参数-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
  1. MixedCollection
  • 会对E、S、O区域进行全面垃圾回收
  • 首先用YoungCollection将E区的存活对象拷贝到S区中,将S区中没达到年龄阈值的整理到一个或多个S区消除碎片空间,S区达到年龄阈值的复制到O区
  • 然后G1会根据用户设置的MaxGCPauseMillis,G1会在所有O区中挑出那些回收价值最高的区域,即耗时短,回收垃圾多的区域,将存活对象复制进一个O区
  • 最后将没复制的回收过得区域进行清理

回收的四个阶段

1)G1执行的第一阶段:初始标记(Initial Marking )这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。

2)G1执行的第二阶段:并发标记从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。当并发标记完成后,开始最终标记(Final Marking )阶段

3)最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)

4)筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)

缺点:与CMS一样需要预留空间,若空间不足或者说垃圾产生的速度比垃圾回收的速度要快,那么也会使用SerialGC回收老年代垃圾,会触发FullGC

  1. Young Collection跨代引用
  • 新生代回收的跨代引用问题(老年代对新生代的引用)
  • 在回收新生代对象时,为了确认新生代对象是否存活,除了遍历GCRoots之外,往往需要遍历整个老年代查看是否有引用,而被引用的新生代对象只有极少数,为了极少数对象遍历整个老年代,造成了很大的性能浪费
  • Remembered Set记忆集,记录哪些新生代被哪些O区对象引用
  • HotSpot使用CardTable卡表实现记忆集,若O区域某个GCRoot对象引用了新生代对象,那么会在CardTable中标记为脏卡,这个新生代对象就变成了脏对象
  • 这样在新生代垃圾回收时就不必去遍历老年代,而是遍历GCROOTs和卡表,就可以判断出存活对象,节省了性能和时间
  1. JDK8u20字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用了cpu时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

String s1 = new String("hello");//char[]{'h','e,'l,'l,'o}
String s2 = new String("hello");//char[]{'h','e,'l,'l,'o}指向同一个数组
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果他们值一样,让它们引用同一个数组
  • 注意,与String.intern()不一样
    • String.intern()关注的是字符串池中的字符串对象
    • 而字符串去重关注的是队列中的char[]
    • 在JVM内部,使用了不同的字符串表
  1. JDK8u40并发标记类卸载

所有对象都经过并发标记后,就知道哪些类不再使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类(无用类的卸载,可以理解为方法区的垃圾清理)

-XX:+ClassUnloadingWithConcurrentMark默认启用

  1. JDK8u60回收巨型对象
  • 一个对象大于Region的一半,称之为巨型对象,放入H区域
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • 如果卡表中不存在对巨型对象的引用,那么会在新生代GC被回收
  1. JDK9并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent
  • JKD9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

1.6 三色标记算法

简介:

三色标记算法是一种标记算法,标记阶段在垃圾回收中是很重要的,很多垃圾回收算法有标记阶段,如标记清除,标记整理,复制算法,标记一般是通过三色标记算法实现的。

三色标记法中用三个颜色块标记对象:

  • 白色:尚未被GC访问过的对象,如果全部标记已完成依旧为白色的,称为不可达对象,既垃圾对象。
  • 黑色:本对象已经被GC访问过,且本对象的子引用对象也已经被访问过了(本对象的孩子节点也都被访问过)。
  • 灰色:本对象已访问过,但是本对象的子引用对象还没有被访问过,全部访问完会变成黑色,属于中间态(本对象的孩子节点还没有访问)。

基本流程:

1.初始时,所有对象都在 【白色集合】中;

2.将GC Roots 直接引用到的对象 挪到 【灰色集合】中;

3.从灰色集合中获取对象:

3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;

3.2. 将本对象 挪到 【黑色集合】里面。

重复步骤3,直至【灰色集合】为空时结束。结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收

存在的问题:

  1. 产生浮动垃圾:由于标记的时候用户线程也在工作,当一个被访问过的黑对象断开对白色对象的引用时,白色对象就变成垃圾了,但是因为黑色对象被访问过了不会再被访问,所以这一部分白色垃圾就变成了浮动垃圾,不会被本次GC回收,须等待下次GC回收
  2. 漏标:当一个灰色对象断开对白色对象的引用,并且此时黑色对象引用了该对象,该对象就不会被标记为灰色,一直是白色,最终被当成垃圾回收,影响了程序的正确性

解决方案:

写屏障 + 增量更新,当对象D的成员变量的引用发生变化时,我们可以利用写屏障,当D是黑色G是白色的话将D标为灰色,等待遍历,即增量更新(Incremental Update)。

CMS(ConcurrentMarkSweep)垃圾收集器就实现了这一种解决方案,它的重新标记阶段就是一个增量更新的过程,增量标记那些在并发标记中产生的浮动垃圾,减少浮动垃圾的存活率。

2. 类加载与字节码

2.1 类文件结构

2.1.1 magic魔数

0-3字节,表示它是否是class类型的文件

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

JAVA的魔数:ca fe ba be

2.1.2 版本

4-7字节,表示类的版本,0034H(52)表示的是java8

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

2.1.3 常量池

8-9字节.表示常量池长度,0023表示35,表示常量池由#1-#34,0不计入,也没有值

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#1项0a表示一个Method信息,0006和0015表示它引用了常量池中#6和#21项来获得这个方法的所属类和方法名

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

第#2项09表示一个Field,后面的四个字节表示了它的所属类和成员变量名

总之就是说开头一个字节表示他是什么,通过查表可以知道是method信息还是变量还是类型等等

2.2 字节码指令

2.2.1 入门

  1. public HelloWorld();构造方法的字节码指令

2a b7 00 01 b1

  • 2a=>aload_0加载slot0的局部变量,即this,作为下面invokespecial构造方法调用的参数
  • b7=>invokespecial 预备调用构造方法,哪个方法呢?
  • 00 01引用常量池中#1项,即Object的<init>Method,即构造方法
  • b1表示返回
  1. public static void main主方法的字节码指令

b2 00 02 12 03 b6 00 04 b1

  • b2->getstatic 获取静态变量 哪个静态变量呢?
  • 00 02引用常量池中的#2项,即System.out
  • 12->ldc 加载参数,哪个参数呢?
  • 03引用常量池#3项,即String hello world
  • b6->invokevirtual预备调用成员方法,哪个方法呢
  • 00 04引用常量池中#4项,即println方法
  • b1表示返回

2.2.2 javap工具

D:\IdeaProjects\project1\test1\src\main\java>javap -v  HelloWorld1.class
Classfile /D:/IdeaProjects/project1/test1/src/main/java/HelloWorld1.class
  Last modified 2022-7-4; size 428 bytes
  MD5 checksum 61c7e3d88ff7f34652cc5bada469a776
  Compiled from "HelloWorld1.java"
public class HelloWorld1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // HelloWorld1
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               HelloWorld1.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               HelloWorld1
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public HelloWorld1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "HelloWorld1.java"

2.2.3 图解方法执行流程

  1. 原始java代码
package cn.itcast.jvm.t3.bytecode;


/**
* 演示 字节码指令 和 操作数栈、常量池的关系 */
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
  1. 编译后的字节码文件
D:\IdeaProjects\project1\test1\src\main\java>javap -v Demo3_1.class
Classfile /D:/IdeaProjects/project1/test1/src/main/java/Demo3_1.class
  Last modified 2022-7-4; size 432 bytes
  MD5 checksum 82b4efda4ff6e8382a2897a4da98e36c
  Compiled from "Demo3_1.java"
public class Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #20.#21        // java/io/PrintStream.println:(I)V
   #6 = Class              #22            // Demo3_1
   #7 = Class              #23            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               Demo3_1.java
  #16 = NameAndType        #8:#9          // "<init>":()V
  #17 = Utf8               java/lang/Short
  #18 = Class              #24            // java/lang/System
  #19 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(I)V
  #22 = Utf8               Demo3_1
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (I)V
{
  public Demo3_1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 3: 0
        line 4: 3
        line 5: 6
        line 6: 10
        line 7: 17
}
SourceFile: "Demo3_1.java"
  1. 常量池载入运行时常量池

238a081149b7e4796c1e613affc48b20.png

  1. 方法字节码载入方法区

  1. main线程开始运行,分配栈帧内存

stack=2(main和println方法)locals=4()

  1. 执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有 sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

2.2.4 i++和++i的区别

i++就是先iload紧接着iinc,++i是先iinc再iload,并且iinc运算不需要读取操作数而是直接在变量上完成的

2.3 多态原理

当执行invokevirtual指令时

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

2.4 语法糖

语法糖,就是指java编译器把*.java编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java给程序员的一个额外福利(给糖吃)

2.4.1 自动构造器

public class Candy1{}
//编译成class后的代码为:
public class Candy1{
    //这个无参构造是编译器为我们加上去的
    public Candy1(){
        super();//即调用父类Object的无参构造方法
    }
}

2.4.2 自动拆装箱

代码片段
public class Candy2{
    public static void main(){
        Integer x = 1;
        int y = x;
    }
}
这段代码在jdk5之前是无法编译通过的,在jdk5之后添加了这个语法糖才可以编译通过
编译后的代码为:
public class Candy2{
    public static void main(){
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}

2.4.3 泛型集合取值

List<Integer> list = new ArrayList<>();
list.add(1);//其实调用的是List.add(Object e)
Integer x = list.get(0);//实际是Object o = List.get(0)
//所以在编译的时候,会额外将取值进行转换
Integer x = (Integer)list.get(0);
//如果将Integer改为int,那么还要进行拆箱操作,所以最后的代码为:
int x = ((Integer)list.get(0)).intValue();
还好这些麻烦事不用自己做

2.4.4 可变参数

String... args会被转换为String[] args

public static void foo(String... args){
    System.out.println(args);
}
public static void main(String[] args){
    foo("hello","sugar");
}
//编译时实际会被转为
public static void foo(String[] args){
    System.out.println(args);
}
public static void main(String[] args){
    foo(new String[]{"hello","sugar"});
    //new String[]{}会返回一个空数组,而不会赋值为null
}

2.4.5 foreach循环

int[] is={1,2,3,4,5};
for(int i : is){
    System.out.println(i);
}
//会被转变为
int[] is=new int[]{1,2,3,4,5}
for(int i=0;i<is.length;i++){
    int e = is[i];
    System.out.println(e);
}

//而list集合的foreach是使用iterator实现的
Iterator itr = List.iterator();
while(itr.hesNext()){
    Integer e = (Integer)itr.next();
    System.out.println(e);

2.4.6 switch字符串

从jdk7开始,switch可作用于字符串和枚举类,但是变量不能为null,这个功能也是语法糖,例如:

switch(str){
    case "hello":{
        System.out.println("h");
        break;
    }
    case "world":{
        System.out.println("w");
        break;
    }
}
public class Candy6_1 {
    public candy6_1(){}
    public static void choose(String str) {
        byte x  -1;
        switch(str.hashCode()) {
            case 99162322: / / hello 的hashCode
                if (str.equals("hello")){
                    //hashcode可能发生碰撞,相同,所以需要进行equals判断
                x= 0;
                }
                break;
            case 113318802: i l world 的hashCode
                if (str.equals( "world") ){
                x = 1;
                }
        }
        switch(x){
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
        }
    }
}

2.4.7 switch枚举类型

enum Sex{
    MALE,FEMALE
}

public class Candy7{
    public static void foo(Sex sex){
        switch(sex){
            case MALE:
                System.out.println("男");break;
            case FEMALE:
                System.out.println("女");break;
        }
    }
}

//转换后代码为:
public class Candy7{
    /**
     *定义一个合成类仅jvm使用,对我们不可见,用来映射枚举的ordinal与数组元素的关系
     枚举的ordinal表示枚举对象的序号,从0开始
     即MALE的ordinal()=0,FEMALE的ordinal=1
     */
    static class $MAP{
        //数组大小即为枚举元素个数,里面存储case用来对比的数字
        static int[] map = new int[2];
        static{
            map[Sex.MALE.ordinal()]]=1;
            map[Sex.FEMALE.ordinal()]]=2;
        }
    }
    public static void foo(Sex sex){
        int x=$MAP.map[sex.ordinal()];
        switch(x){
            case 1:
                System.out.println("男");break;
            case 2:
                System.out.println("女");break;
        }
    }
}

2.4.8 枚举类

jdk7新增了枚举类,以前面的性别枚举为例:

enum Sex(){
    MALE,FEMALE
}
//转换后的代码
public final class Sex extends Enum<Sex>{
    public static final Sex MALE;
    public static final Sex FEMALE;
    public static final Sex[] $VALUES;
    
    static{
        MALE = new Sex("MALE",0);
        FEMALE = new Sex("FEMALE",1);
        $VALUES = new Sex[]{MALE,FEMALE};
    }
    
    private Sex(String name,int ordinal){
        super(name,ordinal);
    }
    public static Sex[] values(){
        return $VALUES.clone();
    }
    
    public static Sex valueOf(String name){
        return Enum.valueOf(Sex.class,name);
    }
}

2.4.9 带参数的try-catch

jdk7开始新增了对需要关闭的资源处理的特殊语法try(){}catch{}

其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AutoCloseable,使用try(){}语法可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9{
    public static void main(String[] args){
        try(InputStream is){
            System.out.println(is);
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

2.5 类加载阶段

2.5.1 加载

  • 将类的字节码载入方法区中,内部采用c++的instanceKlass描述java类,它的重要field有:
    • _java_mirror即java类的镜像,例如对String来说,就是String.class,String.class和instanceKlass互相拥有对方的内存指针,相当于是类的入口,将instanceKlass暴露给java。
    • _super即父类
    • _fields即成员变量
    • _methods即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意

  • instanceKlass这样的[元数据]是存储在方法区(1.8后的元空间)内的,但_java_mirror是存储在堆中
  • 可以通过HSDB工具查看

2.5.2 链接

  • 验证:验证类是否符合JVM规范,安全性检查,验证class文件的魔数格式是否正确,是否是cafebabe
  • 准备:为static变量分配空间,设置默认值
    • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在之后的初始化阶段完成
    • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析:将常量池中的符号引用解析为直接引用,这样就知道该类在内存中的位置了loadClass方法不会导致类的解析和初始化

2.5.3 初始化

<cinit>()v方法

初始化即调用<cinit>()v,虚拟机会保证这个类的【构造方法】的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName()
  • new 会导致初始化

不会导致初始化的情况

  • 访问类的static final 静态常量(基本类型和字符串String)不会触发初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2位false时

初始化的顺序:

  • 静态属性,静态方法块,
  • 普通属性,普通方法快,构造函数,普通方法

存在继承关系的类:

  • 父类的(静态属性,静态方法块),
  • 子类的(静态属性,静态方法块),
  • 父类的(普通属性,普通方法快,构造函数),
  • 子类的(普通属性,普通方法快,构造函数)。

2.6 类加载器

2.6.1 类加载器种类

类加载器的种类有:

  • BootstrapClassLoader,加载位于jre/lib/路径下的jar包,这个类加载器是C语言实现的,在java中打印不出来
  • ExtensionClassLoader,加载位于jre/lib/ext/路径下的jar包,父类为BootstrapClassLoader,显示为null
  • AppClassLoader,加载位于classpath:目录下的jar包,父类为ExtensionClassLoader
  • CustomClassLoader,通过ClassLoader的子类自定义加载Class,父类为ApplicationClassLoader

加载顺序,自顶向下加载,自底向上检查是否加载成功。

2.6.2 双亲委派模式

双亲委派就是指调用类加载器的loadClass方法时,查找类的规则

加载一个类时若有父类,自下而上验证,首先会用ApplicationClassLoader验证是否加载过父类,加载过不再加载,若没有加载过则到父类加载器去进行同样的操作,直到BootStrap类加载器,若BootStrap类加载器也没有加载过,那么从上而下检查是否可以加载,无法加载则让Extension类加载器检查是否可以加载,以此类推直到Application类加载器

总结:自下而上检查是否加载过,加载过的类不再加载;自上而下检查能否加载,能加载则由本身加载类。

2.6.3 线程上下文类加载器

线程上下文类加载器的一般使用模式:获取-使用-还原

ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
 
try {
    //设置线程上下文类加载器为自定义的加载器
    Thread.currentThread.setContextClassLoader(targetTccl);
    myMethod(); //执行自定义的方法
} finally {
    //还原线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
}

我们在使用JDBC的时候,会有一个注册驱动的代码Class.forName(com.mysql.jdbc.Driver),但是其实不写这个语句也可以正常使用JDBC,追踪源码,发现DriverManager的类加载器是BootStrapClassLoader。而DriverManager中有一个静态代码块,里面有一个loadInitialDrivers(),这个方法就是注册数据库驱动的,那么问题来了,DriverManager的类加载器是BootStrap,但是显然mysql-connector-java-5.1.47.jar包不在lib目录下,那么DriverManager是如何正确加载jar包的呢?其实在方法中使用了Class.forName(aDriver, true,

ClassLoader.getSystemClassLoader());而getSystemClassLoader返回的是ApplicationClassLoader,这样就打破了双亲委派的规则。

ContextClassLoader的作用就是为了破坏双亲委派机制。

public class DriverManager{
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    public void loadInitialDrivers(){
        //1.使用ServiceLoader机制加载驱动,即SPI
        //ServiceLoader.load就使用了线程上下文类加载器,获取了线程上下文的类加载器。
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        ....
        //2.使用jdbc.drivers定义的驱动名加载驱动
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
        .....
    }
}

2.6.4 SPI机制(Service Provider Interface)

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 一个服务(Service)通常指的是已知的接口或者抽象类,服务提供方就是对这个接口或者抽象类的实现,然后按照SPI 标准存放到资源路径META-INF/services目录下,文件的命名为该服务接口的全限定名。如有一个服务接口:

package spi;
public interface HelloService {
    public void hi();
}

以及其实现类:

package spi.impl;

import spi.HelloService;
import static utils.Utils.*;
public class HelloServiceImpl implements HelloService {
    @Override
    public void hi() {
        print("hello spi");
    }
}

那此时需要在resources的META-INF/services中创建一个名为spi.HelloService的文件,其中的内容就为该实现类的全限定名:spi.impl.HelloServiceImpl。 如果该Service有多个服务实现,则每一行写一个服务实现(#后面的内容为注释),并且该文件只能够是以UTF-8编码。 然后,我们可以通过ServiceLoader.load(Class class); 来动态加载Service的实现类了。 许多开发框架都使用了Java的SPI机制,如java.sql.Driver的SPI实现(mysql驱动、oracle驱动等)、common-logging的日志接口实现、dubbo的扩展实现等等。

SPI机制的约定

  • 在META-INF/services/目录中创建以Service接口全限定名命名的文件,该文件内容为Service接口具体实现类的全限定名,文件编码必须为UTF-8。
  • 使用ServiceLoader.load(Class class); 动态加载Service接口的实现类。
  • 如SPI的实现类为jar,则需要将其放在当前程序的classpath下。
  • Service的具体实现类必须有一个不带参数的构造方法。

测试:

public class test {
    public static void main(String[] args) {
        ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
        for (HelloService helloService : loader) {
            helloService.hi();
        }
        //也可以使用Iterator迭代器接受loader.iterator();
    }
}

ServiceLoader.load就使用了线程上下文类加载器,获取了线程上下文的类加载器。

2.6.5 自定义类加载器

什么时候需要自定义类加载器?

  • 想加载非classpath随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

步骤:

  • 继承ClassLoader父类
  • 要遵从双亲委派机制,重写findClass方法
    • 注意不是重写loadClass方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的DefineClass方法来加载类
  • 使用者调用该类加载器的loadClass方法
public class CustomClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path="D://myclasspath/"+name+".class";
        ByteArrayOutputStream os=new ByteArrayOutputStream();
        try {
            Files.copy(Paths.get(path), os);//读入类文件的字节码
            byte[] bytes=os.toByteArray();
            return defineClass(name,bytes,0,bytes.length);//定义类名对应的字节码文件
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到",e);
        }

    }
}

2.7 运行期优化

2.7.1 即时编译

分层编译

JVM将执行状态分成了5个层次:

  • 0层,解释执行(Interpreter)
  • 1层,C1即时编译器编译执行(不带profiling)
  • 2层,C1即时编译器编译执行(带基本的profiling)
  • 3层,C1即时编译器编译执行(带完全profiling)
  • 4层,用C2编译器编译执行

profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码转换为机器码,下次即使遇到相同的代码,仍会执行重复的解释
  • JIT是将一些字节码编译为机器码,并存如code cache,下次遇到相同的代码不会重复编译,直接执行
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用代码,我们不用将其编译为机器码,而是直接以解释器方式运行;另一方面,对于仅占据一小部分的热点代码,我们则可以将其交给JIT编译为机器码运行,以达到理想的运行速度。执行效率上简单比较一下Interpreter<C1<C2,总的目标是发现热点代码(hotspot的由来),优化之。

2.7.2 逃逸分析

关于 Java 逃逸分析的定义:

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

我们了解了 Java 中的逃逸分析技术,再来了解一个对象的逃逸状态。

逃逸状态:

1.全局逃逸(GlobalEscape)

即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

  • 对象是一个静态变量
  • 对象是一个已经发生逃逸的对象
  • 对象作为当前方法的返回值

2.参数逃逸(ArgEscape)

即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

3.没有逃逸

即方法中的对象没有发生逃逸。

开启和关闭逃逸分析:

  • 开启逃逸分析:-XX:+DoEscapeAnalysis
  • 关闭逃逸分析:-XX:-DoEscapeAnalysis
  • 显示分析结果:-XX:+PrintEscapeAnalysis

逃逸分析默认为启用状态。

逃逸分析优化

针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化。

  • 锁消除
    • 我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
    • 例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
    • 锁消除的 JVM 参数如下: 开启锁消除:-XX:+EliminateLocks 关闭锁消除:-XX:-EliminateLocks 锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
  • 标量替换
    • 首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。
    • 对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
    • 这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
    • 标量替换的 JVM 参数如下: 开启标量替换:-XX:+EliminateAllocations 关闭标量替换:-XX:-EliminateAllocations 显示标量替换详情:-XX:+PrintEliminateAllocations 标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。
  • 栈上分配
    • 当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。
    • 也就是说如果一个对象没有被本方法以外的方法使用,那么这个对象就会被放在栈内存而不是堆中,并且栈中只有对象包含的变量。

2.7.3 方法内联

如果一个方法是热点方法并且长度不是很长,会进行内联,就是将方法内代码拷贝到调用者的位置,如:

public int square(final int i){
    return i*i;
}

System.out.println(square(9));
//优化后
System.out.println(9*9);

还能够进行常亮折叠的优化变成:

System.out.println(81);

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到