java(5)-深入理解虚拟机JVM

   引言,是否可以回答下面的问题?如果能回答,此文忽略。

  • 说一下 JVM 运行时数据区吧,都有哪些区?分别是干什么的?
  • Java 8 的内存分代改进
  • 举例栈溢出的情况?
  • 调整栈大小,就能保存不出现溢出吗?
  • 分配的栈内存越大越好吗?
  • 垃圾回收是否会涉及到虚拟机栈?
  • 方法中定义的局部变量是否线程安全

 一、JVM是什么


 1、JVM是什么

       在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(ByteCode)(class文件的内容),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。

JVM是运行Java程序的基础软件的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。它可以安装在几种不同的操作系统上,包括Windows,OS X和Linux,JVM允许Java 应用程序在所有计算机上运行。

JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。

因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言( "semi-interpreted" language)。

2、Java跨平台的特性是由JVM来实现

java程序的执行必须经过编写 、javac编译 、jvm运行 三个步骤:

     

1)编译过程:创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用,这个有点象make。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话报“cant find symbol”的错误。

编译后的字节码文件格式主要分为两部分:常量池方法字节码。常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等)

2)jvm运行:java类运行的过程大概可分为两个过程:1、类的加载  2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

下面通过以下这个java程序,来说明java程序从编译到最后运行的整个流程。代码如下:

#MainApp.java
public class MainApp {  

public static void main(String[] args) {  
    Animal animal =new Animal("Puppy");  
        animal.printName();  
    }  
}  

//Animal.java  
publi cclass Animal {  
  public String name;  

  public Animal(String name) {  
     this.name = name;  
  }  

  public void printName() {  
       System.out.println("Animal ["+name+"]");  
   }  

}

1. 编译:在编译MainApp.java程序得到MainApp.class文件。

2、启动jvm进程加载类:在命令行上敲java AppMain,系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。

3. 执行:

    1)然后JVM找到AppMain的主函数入口,开始执行main函数。

    2)main函数的第一条命令是Animal  animal = new Animal("Puppy");就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。

    3)加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用

    4).当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。

    5).开始运行printName()函数。

          Java跨平台的特性更准确地说,是Sun利用JVM在不同平台上的实现帮我们把平台相关性的问题给解决了,这就好比是HTML语言可以在不同厂商的浏览器上呈现元素(虽然某些浏览器在对W3C标准的支持上还有一些问题)。同时,Java语言支持通过JNI(Java Native Interface)来实现本地方法的调用,但是需要注意到,如果你在Java程序用调用了本地方法,那么你的程序就很可能不再具有跨平台性,即本地方法会破坏平台无关性。

      另外需要注意的是,虽然平时我们用的大多是Sun(现已被Oracle收购)JDK提供的JVM,但是JVM本身是一个规范,所以可以有多种实现,除了Hotspot外,还有诸如Oracle的JRockit、IBM的J9也都是非常有名的JVM。

3、JVM、JRE和JDK的关系

    JVM:Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。

    JRE:
    Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包

    如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

    JDK:
    Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等

    JVM&JRE&JDK关系图

 二、JVM主要组成


下图展示了JVM的主要结构:

         可以看出,JVM主要由类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈组成。

java程序从源文件到最终执行到详细过程:

java源码(*.java) --->java编译器 --->字节码文件(*.class)

类加载器 --->字节码校验器 ---> 解释器(翻译)  ---> 机器码  ---> 交由execution engin执行

1、类加载器子系统

每一个Java虚拟机都由一个类加载器子系统(class loader subsystem)。类加载器子系统负责加载编译好的.class字节码文件,并装入内存(方法区),使JVM可以实例化或以其它方式使用加载后的类。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Classs文件中常量池部分的内存映射)。JVM的类加载子系统支持在运行时的动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。

2、执行引擎

每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。

主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行其中解释属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式 。

自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。

3、运行时数据区(内存空间)

     运行时数据区是JVM从操作系统申请来的堆空间和操作系统给JVM分配的栈空间的总称。JVM为了运行Java程序,又进一步对运行时数据区进行了划分:划分为方法区、堆、Java栈、PC寄存器、本地方法栈组成。

     其中方法区和堆是所有Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每个线程私有。

三、Class Loader类加载器系统


       类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM可以实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。

1、ClassLoader的分类:

类加载器被组织成一种层级结构关系,也就是父子关系。其中,Bootstrap是所有类加载器的父亲。如下图所示:

1).启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中所有的Java类,即Java的核心类都是由该ClassLoader加载。它加载一些基本的java API,包括Object这个类。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。

2) .扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包类。

3). 系统类加载器(System Class Loader):负责加载加载应用程序中的类(即启动参数中指定的Classpath中的jar包及目录),通常我们自己写的Java类也是由该ClassLoader加载。在Sun JDK中,系统类加载器的名字叫AppClassLoader。

4). 用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

2、ClassLoader加载器的工作流程 

类加载分为装载、链接、初始化三步。类似《程序如何运行:编译、链接、装入》

第一步 装载Load

        通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。

        在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区。

        装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model)”的方式,当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader,所以称为父ClassLoader可能更容易理解)加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。

      上面的层次结构,当JVM加载一个类的时候,下层的加载器会将将任务委托给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查完了之后,按照相反的顺序进行加载,如果Bootstrap加载器找不到这个类,则往下委托,直到找到类文件。对于某个特定的类加载器来说,一个Java类只能被载入一次,也就是说在Java虚拟机中,类的完整标识是(classLoader,package,className)。一个类可以被不同的类加载器加载。

        举个具体的例子来说明,现在加入我有一个自己定义的类MyClass需要加载,如果不指定的话,一般交App(System)加载。接到任务后,System检查自己的库里是否已经有这个类,发现没有之后委托给Extension,Extension进行同样的检查,发现还是没有继续往上委托,最顶层的Bootstrap发现自己库里也没有,于是根据它的路径(Java 核心类库,如java.lang)尝试去加载,没找到这个MaClass类,于是只好(人家看好你,交给你完成,你无能为力,只好交给别人啦)往下委托给Extension,Extension到自己的路径(JAVA_HOME/jre/lib/ext)是找,还是没找到,继续往下,此时System加载器到classpath路径寻找,找到了,于是加载到Java虚拟机。
        现在假设我们将这个类放到JAVA_HOME/jre/lib/ext这个路径中去(相当于交给Extension加载器加载),按照同样的规则,最后由Extension加载器加载MyClass类,看到了吧,统一各类被两次加载到JVM,但是每次都是由不同的ClassLoader完成。

       

       双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危害。

      由此也可见,如果用户自定义了类加载器,那就必须自己保障类加载过程中的安全。

第二步 链接Linking

      链接的任务是把二进制的类型信息合并到JVM运行时状态中去。 链接分为以下三步:

1).验证Verifying:校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM使用。
2).准备Preparing:为类分配内存,同时初始化类中的静态变量赋值为默认值。
3).解析Resolving(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步可以在用到相应的引用时再解析。

第三步 初始化Initialing       

       初始化类中的静态变量,并执行类中的static代码、构造函数。
       JVM规范严格定义了何时需要对类进行初始化:

  1. 通过new关键字、反射、clone、反序列化机制实例化对象时。
  2. 调用类的静态方法时。
  3. 使用类的静态字段或对其赋值时。
  4. 通过反射调用类的方法时。
  5. 初始化该类的子类时(初始化子类前其父类必须已经被初始化)。
  6. JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。

static块、构造块、构造函数的初始化执行顺序:

父类静态变量-> 父类静态块 -> 子类静态变量 -> 子类静态块 -> 父类成员变量 -> 父类构造块 -> 父类构造函数 -> 子类成员变量 -> 子类构造块 -> 子类构造函数  

那么,Class Loader在加载类的时候,究竟做了些什么工作呢?要了解这其中的细节,必须得先详细介绍一下运行数据区域

四、java内存模型:运行数据区域Runtime Data Areas


        Runtime Data Areas:当运行一个JVM示例时,系统将分配给它一块内存区域(这块内存区域的大小可以设置的),这一内存区域由JVM自己来管理。从这一块内存中分出一块用来存储一些运行数据,例如创建的对象,传递给方法的参数,局部变量,返回值等等。分出来的这一块就称为运行数据区域。

        根据 JVM 规范,JVM 内存(运行数据区)可以划分为5大块Java栈、程序计数寄存器(PC寄存器)、本地方法栈(Native Method Stack)、Java堆、方法区(包含运行常量池Runtime Constant Pool)。运行常量池本应该属于方法区,但是由于其重要性,JVM规范将其独立出来说明。

        其中前面3各区域(PC寄存器、Java栈、本地方法栈)是每个线程独自拥有的,后三者则是整个JVM实例中的所有线程共有的。

线程私有区域:
     PC寄存器(Program Counter, PC)
     Java虚拟机栈(Java Virtual Machine Stack)
     操作数栈(本地方法栈)


线程共享区域:
     堆(Heap区)
     方法区
     运行常量池(Runtime Constant Pool)

这六大块如下图所示:

4.1、PC程序计数器:指令地址

4.1.1、概述:

PC程序计数寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,叫程序计数器(或PC计数器或指令计数器)会更加贴切,并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。
 

4.1.2、作用:

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

      1)、线程私有:每一个线程都拥有一个PC计数器,各个线程之间的计数器相互独立,即该内存区域为“线程私有”内存,它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
      2)、JVM指令:当线程启动(start)时,PC计数器被创建,这个计数器存放当前正在被执行的字节码指令(JVM指令)的地址。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 natice 方法,则是未指定值(undefined)
     3)它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
    4)它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域。

4.1.3、运行原理:

      Java天生支持多线程,多线程会有线程切换的问题,当一个线程从可运行状态得到CPU调度进入运行状态,CPU需要知道从哪里开始执行,并且Java是一种基于栈的执行架构(区别于基于寄存器的架构)。当执行一个Java方法时,PC会指向下一条指令的位置。执行native方法时,PC是未定义。操作指令可能会有0个或多个操作数。JVM的执行流程大概可以描述为:
while(true) {
    opcode = code[pc];
    oprand = getOperan(opcode);
    pc = code[pc + len(oprand)];
    execute(opcode, oprand);
}

进入class文件所在目录,执行javap -v xx.class反解析(或者通过IDEA插件Jclasslib直接查看,下图),可以看到当前类对应的Code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等信息。)

4.2、JAVA 栈:执行的是Java方法

4.2.1、概述:

Java 虚拟机栈(Java Virtual Machine Stacks),早期也叫 Java 栈。

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

4.2.2、作用:

保存方法的局部变量、部分结果,并参与方法的调用和返回。

   1)java栈的内存也是线程私有,访问速度仅次于程序计数器,栈不存在垃圾回收问题。

   2)java描述的是java方法执行的内存模型:java方法被执行的时候都会同时创建栈帧(Stack Frame).用于存储局部变量表、操作数栈、动态链接,方法出口等信息。方法被调用到执行完成对的过程,就是相应对于栈帧在JVM从入栈到出栈的过程。当线程请求的栈深度大于虚拟机所允许的深度是出现错误:StackOverflowError。

  3)栈中可能出现的异常:

    如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
    如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常

可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

    我们通过递归方法来测试栈的深度和栈溢出:

public class StackOverflowError {
    //使用计数器计算栈的深度
    private static int index = 1;

    //没有结束条件的递归导致死递归
    public void recursiveCall(){
        index++;
        recursiveCall();
    }

    public static void main(String[] args) {
        StackOverflowError stackOverflowError = new StackOverflowError();
        try {
            stackOverflowError.recursiveCall();
        }catch (Throwable e){
            System.out.println("Stack deep : "+index);
            e.printStackTrace();
        }
    }

}

指定线程栈大小为-Xss128k,运行结果:

  如果调整线程栈大小-Xss256k, 深度也是不一样的:

4.2.4、运行原理:

      Java栈由栈帧(Stack Frame)组成,一个帧对应一个方法调用。

       虚拟机栈是一个LIFO(后进先出)的栈: 调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。

    1)JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈和出栈,遵循“先进后出/后进先出”原则
    2)在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
    3)执行引擎运行的所有字节码指令只针对当前栈帧进行操作
    4)如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
    5)不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
    6)如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
    Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出

IDEA 在 debug 时候,可以在 debug 窗口看到 Frames 中各种方法的压栈和出栈情况。

4.2.5、 栈帧的基本结构

一个栈帧对应一个方法,栈帧分为三部分:局部变量区、操作数栈、帧数据区(动态连接和返回地址)。

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或称为表达式栈)
  • 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  • 方法返回地址(Return Address):方法正常退出或异常退出的地址
  • 一些附加信息

1、局部变量区:本地变量表存储方法的参数、方法内部创建的局部变量
1)局部变量表存放 了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

对象引用:

      直接使用指针访问:指向对象起始地址的引用指针:程序通过栈上的reference数据来操作堆上的具体对象。reference指向了堆中的具体对象。

     

       指向一个代表对象的句柄:句柄中包含了对象实例数据与类型数据各自的具体地址信息。

      

2)部变量区是以字节为单位的数组,在这里byte、short、char类型会被转换成int类型存储,除了long和double类型占两个字长以外,其余类型都只占用一个字节。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。

3)局部变量区包含了方法参数和局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。

2、操作数栈

后进先出(Last-In-First-Out)的操作数栈,也可以称为表达式栈(Expression Stack)。

作用和概述:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,此时这个方法的操作数栈是空的。

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性的 max_stack 数据项中

运行原理:操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。

        在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop)。Java方法中的代码逻辑就是通过操作数栈来实现的。在进行计算时,操作数被弹出栈,计算完毕后再入栈。

下面用一个简单的例子展示一下。

这个实例方法经过Java编译器编译后生成的字节码

3、帧数据区

帧数据区的作用主要有:

  1.  记录指向类的常量池的指针(动态链接),以便于解析。
  2.  帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中。
  3.  记录异常表,发生异常时将控制权交由对应异常的catch子句,如果没有找到对应的catch子句,会恢复调用方法的栈帧并重新抛出异常。

动态链接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用来支持方法调用过程中的动态链接;

          在 Java 源文件被编译到字节码Class文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 Class 文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是通过两种方式将这些符号引用转换为调用方法的直接引用:

  • 静态解析:一部分在类加载阶段或者第一次使用的时候转换为直接引用;
  • 动态链接:在运行期间转换为直接引用;

JVM 是如何执行方法调用的

方法调用不同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class 文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在 Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至到运行期才能确定目标方法的直接引用。

    【这一块内容,除了方法调用,还包括解析、分派(静态分派、动态分派、单分派与多分派),这里先不介绍,后续再挖】

在 JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关

    静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
    动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

    早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
    晚期绑定:如果被调用的方法在编译器无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式就被称为晚期绑定。

虚方法和非虚方法

    如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法,比如静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
    其他方法称为虚方法

虚方法表

在面向对象编程中,会频繁的使用到动态分派,如果每次动态分派都要重新在类的方法元数据中搜索合适的目标有可能会影响到执行效率。为了提高性能,JVM 采用在类的方法区建立一个虚方法表(virtual method table),使用索引表来代替查找。非虚方法不会出现在表中。

每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。
 

返回地址用来存放调用该方法的 PC 寄存器的值。一个方法的结束,有两种方式

  • 正常执行完成
  • 出现未处理的异常,非正常退出

在方法退出之后,需要返回到方法被调用的位置继续执行;退出时可能需要执行的操作:

  • 恢复上层方法局部变量表和操作数栈;
  • 如有有返回值,把该值压入调用者栈帧的操作数栈中;
  • 调整PC计数器的值以指向方法调用指令的下一条指令地址。

局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种方式可以退出这个方法:

     1)return正常返回:执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口

    一个方法的正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定

    在字节码指令中,返回指令包含 ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个 return 指令供声明为 void 的方法、实例初始化方法、类和接口的初始化方法使用。

    2)异常返回:在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口

    方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。


4、附加信息

栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现。

 

4.3、本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。在Sun JDK中,本地方法栈和Java栈是同一个。当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就根据本地方法的语言类型建立相应的栈。

4.4、 Method Area 方法区

在不同的JDK版本中,方法区中存储的数据是不一样的。

在JDK1.6及之前:运行时常量池是方法区的一个部分,同时方法区里面存储了类的元数据信息、静态变量、即时编译器编译后的代码(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)等。
运行时常量池是方法区的一部分:用于存放编译器生成的各种字面量(static final)和符号引用。
方法区包含class文件信息,对于每个类class文件包含存储了以下信息:

  1. 类及其父类的全限定名(java.lang.Object没有父类)
  2. 类的类型(Class or Interface)
  3. 访问修饰符(public, abstract, final)
  4. 实现的接口的全限定名的列表
  5. 常量池
  6. 字段信息
  7. 方法信息
  8. 静态变量
  9. ClassLoader引用
  10. Class引用

在JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。

一个class文件对应一个常量池,为静态常量池。JDK运行时,会根据静态常量池生成对应的运行时常量池。
JDK7之前运行时常量池逻辑包含字符串常量池,此时hotspot虚拟机对方法区的实现为永久代PermGen;
JDK7字符串常量池从方法区移到了堆中,这里只是把字符串常量池单独拿到了堆中,运行时常量池剩下的东西还在方法区,也就是hotspot的永久代PermGen;
JDK8中 hotspot移除了永久代PermGen,用元空间Metaspace取代,此时字符串常量池还在堆中,运行时常量池还在方法区,只不过方法区的实现从永久代PermGen变成了元空间Metaspace。
JDK8的元空间不在JVM运行时数据区,而在本地内存,此时本地内存包括:元数据区(元空间)和直接内存。

       类的所有信息都存储在方法区中,而方法区又是所有线程共享的,所以必须保证线程安全,举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。

此外为了加快调用方法的速度,通常还会为每个非抽象类创建私有的方法表,方法表是一个数组,存放了实例可能被调用的实例方法的直接引用。

4.5、堆(Heap)

       堆用于存储对象实例以及数组值。堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。

       在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。这是基于这样一个类似28原理的统计,90%多的对象都是很快成为垃圾的对象。所以化为成两个区域,分别使用不同的收集算法,年轻代的收集频率更高,所需空间也相对较小。

1)、新生代(New Generation)

大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制。 

内存分配时,多个线程会有并发问题,主要通过两种方式解决:
1).CAS加上失败重试分配内存地址。
2). TLAB, 即Thread Local Allocation Buffer, 为每个线程分配一块缓冲区域进行对象分配。

JVM在Eden Space中会开辟一小块独立的TLAB区域用于更高效的内存分配,我们知道在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。

年轻代还可以分为两个大小相等的Survivor和一个Eden区域。

对象在几种情况下会进入老年代:

  •      1. 大对象,超过Eden大小或者PretenureSizeThreshold. 
  •      2. 在年轻代的年龄(经历的GC次数)超过设定的值的时候 
  •      3. To Survivor存放不下的对象

2)、旧生代(Old Generation/Tenuring Generation)

      在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。

4.6、常量池

在Java的内存分配中,总共3种常量池:

1. 字符串常量池(也叫全局字符串池、string pool、string literal pool)

  • 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
  • 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK7.0中,StringTable的长度可以通过参数指定:-XX:StringTableSize=66666
  • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;可以运行如下代码,会报异常信息:java.lang.OutOfMemoryError:PermGen space
  • 在JDK7.0版本,字符串常量池被移到了堆中了。可以运行如下代码,会报异常信息:java.lang.OutOfMemoryError: Java heap space
  • 在JDK8.0版本,字符串常量池放到元空间,可以运行如下代码,会报异常信息:java.lang.OutOfMemoryError: Java heap space ,实际还是在元空间。

public class StringConstantPoolTest {
    public static void main(String[] args) {
        List<String> list = Lists.newArrayList();
        while (true) {
            list.add(String.valueOf(System.currentTimeMillis()).intern());
        }
    }
}
 

2.class常量池(Class Constant Pool):

  • Java类编译后就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
  • 每个class文件都有一个class常量池。

什么是字面量和符号引用:

  • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  • 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。




3.运行时常量池(Runtime Constant Pool):

  • 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用
  • JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中。由此可知,运行时常量池也是每个类都有一个。
  • 在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
public class HelloWorld {
        public static void main(String []args) {
            String str1 = "abc";
            String str2 = new String("def");
            String str3 = "abc";
            String str4 = str2.intern();
            String str5 = "def";
            System.out.println(str1 == str3);//true
            System.out.println(str2 == str4);//false
            System.out.println(str4 == str5);//true

        }

 }

程序的内存分配过程:

首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值,最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

方法区的Class信息,又称为永久代,是否属于Java堆?

方法区(method area)只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。永久代Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。

在Java 6中,方法区中包含的数据,除了JIT编译生成的代码存放在native memory的CodeCache区域,其他都存放在永久代;
在Java 7中,Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内);
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。
Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

以上描述截取自:
《深入理解Java虚拟机:JVM高级特性与最佳实践》 作者: 周志明

五、Java 8: 从永久代(PermGen)到元空间(Metaspace)


1、 从永久代(PermGen)到元空间(Metaspace)

     是Oracle从JDK7发布以来就一直宣称的要完全移除永久代空间。例如,字符串内部池,已经在JDK7中从永久代中移除。

       在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误:java.lang.OutOfMemoryError: PermGen

      JDK8的JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的非堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。

       类的元数据信息转移到Metaspace的原因:

       1)、PermGen调整比较麻烦:PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
       2)、在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。

2、Metaspace 内存分配和垃圾回收

  1)、jdk8的PermGen 空间的状况

  • 这部分内存空间将全部移除。
  • JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。




  2)、Metaspace内存分配模型

  • 大部分类元数据都在本地内存中分配。
  • 用于描述类元数据的“klasses”已经被移除。

 3)、Metaspace 容量

  • 默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。
  • 新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
  • 如果设置-XX:MaxMetaspaceSize,当,Metaspace被耗尽;与JDK1.7运行时非常相似,报错:ERROR: java.lang.OutOfMemoryError: Metadata space  

      还是上面例子com.demo.test.web.PermGenOom,我们使用java8,并且设置-XX:MaxMetaspaceSize=1m




4)、Metaspace 垃圾回收

  • 对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。
  • 适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。




5)、Java 堆内存的影响

  • 一些杂项数据已经移到Java堆空间中。升级到JDK8之后,会发现Java堆 空间有所增长。

六、执行引擎Execution  Engine


     类加载器将字节码载入内存之后,执行引擎以Java 字节码指令为单元,读取Java字节码。问题是现在的java字节码机器是读不懂的,因此还必须想办法将字节码转化成平台相关的机器码。这个过程可以由解释器来执行,也可以有即时编译器(JIT Compiler)来完成。

     

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。

JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。注意这里是“通常”情况,之后会讲到由于优化导致的例外。

1、解释执行

和一些动态语言类似,JVM可以解释执行字节码。Sun JDK采用了token-threading的方式,感兴趣的同学可以深入了解一下。

解释执行中有几种优化方式:

a.栈顶缓存

将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数站。这样便减少了寄存器和内存的交换开销。

b.部分栈帧共享

被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。

c.执行机器指令

在一些特殊情况下,JVM会执行机器指令以提高速度。

2、编译执行

为了提升执行速度,Sun JDK提供了将字节码编译为机器指令的支持,主要利用了JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,之后就可以重复利用。Oracle JRockit采用的是完全的编译执行。

3、自适应优化执行

自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,所以通过将那少部分代码编译为优化过的机器码就可以大大提升执行效率。自适应优化的典型代表是Sun的Hotspot VM,正如其名,JVM会监测代码的执行情况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该方法的字节码编译为极度优化的、静态链接的C++代码。当方法不再是热区时,则会取消编译过的代码,重新进行解释执行。

自适应优化不仅通过利用小部分的编译时间获得大部分的效率提升,而且由于在执行过程中时刻监测,对内联代码等优化也起到了很大的作用。由于面向对象的多态性,一个方法可能对应了很多种不同实现,自适应优化就可以通过监测只内联那些用到的代码,大大减少了内联函数的大小。

Sun JDK在编译上采用了两种模式:Client和Server模式。前者较为轻量级,占用内存较少。后者的优化程序更高,占用内存更多。

在Server模式中会进行对象的逃逸分析,即方法中的对象是否会在方法外使用,如果被其它方法使用了,则该对象是逃逸的。对于非逃逸对象,JVM会在栈上直接分配对象(所以对象不一定是在堆上分配的),线程获取对象会更加快速,同时当方法返回时,由于栈帧被抛弃,也有利于对象的垃圾收集。Server模式还会通过分析去除一些不必要的同步,感兴趣的同学可以研究一下Sun JDK 6引入的Biased Locking机制。

此外,执行引擎也必须保证线程安全性,因而JMM(Java Memory Model)也是由执行引擎确保的。

七、Class文件结构


Java编译器将Java代码编译成class文件格式。 其中步骤包括了我们熟悉的词法分析将源文件转换成token流。语法分析将token流转换成抽象语法树(AST)。语义分析分析语义是否正确。源代码优化。目标代码生成和目标代码优化等步骤。最终得到了class文件。之后在虚拟机中,class文件可以通过解释器解释执行和通过即时编译器(JIT-just in time)编译成native代码执行两种方式执行。 class文件是有严格定义的。符合定义的class文件才能够被JVM加载、验证、初始化、执行。 我们通过javap可以查看一个class文件的内容。 Class文件可以分为以下几个部分:
● Magic Number (0xCAFEBABY)
● minor version, major version 如 0x0033 代表 00,51, 是java8版本
● constant pool 常量池,常量池中包括了字段、方法、类的名称的符号引用,符号引用会在运行时经过链接转换为直接引用。
● access flags 类的private、public等修饰词
● this class 表明当前类的名称
● super class 父类
● interfaces 实现的接口列表
● fields class中定义的字段,每个field又是一个结构体
● methods 方法,包括MaxLocal, Max Stack,方法名,signature,access flags等。 代码保存在方法的名称为Code的属性中。
● attributes
下面以一个简单的类


看一下它的class文件,通过vim打开,在Normal模式下,按: 输入%!xxd,即可转换成16进制表示。然后可以通过%!xxd -r转换回来


通过javap来看一下它的结构

八、垃圾收集(Garbage Collect)


具体看《java(9)-深入浅出GC垃圾回收机制》

Java中不需要对内存进行手动释放,JVM中的垃圾回收器帮助我们回收内存。
● 何时进行收集
一般来说,当某个区域内存不够的时候就会进行垃圾收集。如当Eden区域分配不下对象时,就会进行年轻代的收集。还有其他的情况,如使用CMS收集器时配置CMSInitalize
● 如何判断一块内存是垃圾

即判断一个对象不再使用,不再使用可以是没有有效的引用。 一般来说,主要有两种判断方式
 1:引用计数: 当有对象引用自身时,就会计数器加1,删除一个引用就减一,当计数为0时即可判断为垃圾。python等语言使用引用计数。引用计数存在循环引用问题,如两个落单的A和B互相引用,但是没有其他对象指向它们这种情况
2)可达性分析:通过一些根节点开始,分析引用链,没有被引用的对象都可以被标记为垃圾对象。根节点是方法栈中的引用、常量等。

1、垃圾收集算法

● 标记清除(Mark Sweep)  :对非垃圾对象进行标记都,清除其他的对象。这种方式对对内存空间造成空隙,即内存碎片,最终导致有空余空间,但没有连续的足够大小的空间分配内存。
● 标记整理(Mark Compact) :标记非垃圾对象后,将这些对象整理好,排列到内存的开始位置。这样内存就是整齐的了。但是因为会造成对象移动,所以效率会有降低
● 标记清除整理(Mark Sweep Compact) :即组合两种方式,在若干次清除后进行一次整理。
● 复制(Copy) :划分成两个相同大小的区域,收集时,将第一个区域的活对象复制到另一个区域,这样不会有内存碎片问题。但是最多只能存放一半内存。

2、垃圾收集器
垃圾收集器就是垃圾收集算法的相应实现。
● Serial New :新生代单线程的收集器,是Client模式默认的垃圾收集器
● Parallel New :Serial New的多线程版本。ParNew常和CMS拉配使用。这里说明一些Parallel和Concurrent即并行和并发在垃圾收集这里的表示的不同,并行表示有多个线程同时进行垃圾收集,并发是指垃圾收集线程和应用线程可以并发执行。
● Parallel Scanvenge :PS收集器是注重吞吐量(ThroughPut)的收集器。
● Serial Old:老年代的单线程收集器
● Parallel Old:Serial Old的多线程版本,由于Parallel Scavenge不能和CMS搭配使用,所以会是使用PS时的一种选择。
● CMS (Concurrent Mark Sweep):注重延迟latency的收集器,在交互式应用中,如面向用户的web应用,需要尽可能减少垃圾收集造成的停顿时间。在总的统计上,吞吐量可能没有PS收集器高。 细分上,CMS还分为4个阶段:
1.初始标记,标记GC Root可以直达的对象。STW
2.并发标记,从第一步标记的对象开始,进行可达性分析遍历,和应用线程并发执行。
3.重新标记,SWT,修正上一阶段并发执行造成的引用变化。
4.并发清除,并发的清除垃圾 CMS使用标记清除算法,所以有内存碎片问题,可能设置参数在进行若干次不带整理的收集后进行一次带整理(compact)的收集。另外,因为垃圾收集是和应用线程并发执行的,在收集的同时可能还会有垃圾不断产生,即产生了浮动垃圾。另外还需要预留出一定空间,到达这个值后进行收集,但是还会有收集速度赶不上生产的速度,这时就会出现Concurrent Mode Failure,CMS会退化成Serial Old进行GC。
● G1 (Garbage First)具有大内存收集和目标效率时间等控制能力,目标是代替CMS。G1通过将内存划分成不同的区域(Region),并对不同区域计算分数,分析那个Region最具有收集价值。

3.一些JVM的GC参数
常用的参数设置有:
● -Xms=4g -Xmx=4g 设置Java堆的初始大小和最大大小均为4g,即避免了堆大小调整
● -Xmn=1g 设置年轻代的总大小为1g
● -SurvivorRatio=8, 设置Eden和一个Survivor的比例为8:1
● -XX:+PringGCDetails

八、jvm监控工具(Garbage Collect)


监控工具帮助我们在运行时或问题发生后分析现场,分析内存分布状态,哪里导致内存泄漏等(本该被释放的对象仍然被引用)。

1、命令行工具:

JDK内置工具使用,在bin目录下:

一、javah命令(C Header and Stub File Generator)
二、jps命令(Java Virtual Machine Process Status Tool)
三、jstack命令(Java Stack Trace)
四、jstat命令(Java Virtual Machine Statistics Monitoring Tool)
五、jmap命令(Java Memory Map)
六、jinfo命令(Java Configuration Info)
七、jconsole命令(Java Monitoring and Management Console)
八、jvisualvm命令(Java Virtual Machine Monitoring, Troubleshooting, and Profiling Tool)
九、jhat命令(Java Heap Analyse Tool)
十、Jdb命令(The Java Debugger)
 十一、Jstatd命令(Java Statistics Monitoring Daemon)


● jps:即java版的ps,可以查看当前用户启动了哪些java进程。jps -l 或者jps -lv
● jstat:jstat是一个多种用途的工具,更多需要man jstat或直接输入jstat查看提示。利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。查看 GC操作的信息,类装载操作的信息以及运行时编译器操作的信息

             jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] (如 jstat  -gcutil pid  100 10) pid指jps命令查看的java进程号。

            假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是 :jstat-gc 2764 250 20 或者
            

●jinfo:Java配置信息工具

  jinfo pid

 可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来:jinfo -sysprops pid

● jmap:jmap可以查看内存状况

jmap常用参数:

       -heap:打印jvm heap的情况
  -histo:打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。
  -histo:live :同上,但是只打印存活对象的情况。
  -permstat:打印permanent generation heap情况。



● jstack 查看Java线程状况

  jstack  -l pid
● javap (Java Printer)
javap 可以用可读的方法查看class文件内容,在遇到线上class文件问题,如NoSucheMethodError发生时,可以快速进行判断分析。如分析一个A.class文件,查看它的私有方法和字段。
javap  -p -c  -v  A.class
 

2、可视化工具
● JVisualVM: $JAVA_HOME/bin/jvisualvm
● JMC:$JAVA_HOME/bin/jmc
● JConsole: $JAVA_HOME/bin/jconsole

十、虚拟机参数说明


堆(Heap)和非堆(Non-heap)内存 
  按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。 

堆内存分配 -Xms和-Xmx 
  JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;

       JVM最大分配的内存由-Xmx 指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到- Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。 
非堆内存分配 -XX:PermSize
  JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;

      XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。 
JVM内存限制(最大值) 
  首先JVM内存限制于实际的最大物理内存(废话!呵呵),假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然 可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统 下为2G-3G),而64bit以上的处理器就不会有限制了。 

  通常,我们为了避免内存溢出等问题,需要设置环境变量 
   JAVA_OPTS    -Xms256M -Xmx512M 等,【对于服务器,一般都设置成一样的】 
  但是有的时候可能这样的设置还会不行(比如,当Server应用程序加载较多类时,即jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小,为了避免调整),你可以使用更多的参数配置, 
  如: java -Xms512m -Xmx512m -XX:PermSize=64m -XX:MaxPermSize=128m 
  其中,使用   -XX:MaxPerSize标志来增加永久域的大小,-XX:PerSize标志设置初始值 

-XX 
  基于 Sun 的 Java 2 Standard Edition(J2SE)5 有生成垃圾回收功能,这允许分隔内存池以包含不同时效的对象。垃圾回收循环根据时效收集与其他对象彼此独立的对象。使用其他参数,您可以单独设置内存池的 大小。为了实现更好的性能,您应该对包含短期存活对象的池的大小进行设置,以使该池中的对象的存活时间不会超过一个垃圾回收循环。新生成的池的大小由 NewSize 和 MaxNewSize 参数确定。 

  第一次垃圾回收循环中存活下来的对象转移到另一个池中。生还者池的大小由参数 SurvivorRatio 确定。 如果垃圾回收变成了瓶颈,您可以尝试定制生成池设置。要监控垃圾回收统计信息,使用 Tivoli Performance Viewer 中的对象统计信息或 verbose:gc 配置设置。 输入下列值: 
  -XX:NewSize (lower bound)-XX:MaxNewSize (upper bound) -XX:SurvivorRatio=NewRatioSize -XX:NewSize 320m 此值设大可调大新对象区,减少Full GC次数-XX:userParNewGC 可用来设置并行收集【多CPU】-XX:ParallelGCThreads 可用来增加并行度【多CPU】-XXUseParallelGC 设置后可以使用并行清除收集器【多CPU】下面的命令把整个堆设置成128m,新域比率设置成3,即新域与旧域比例为1:3,新域为堆的1/4或32M:java –Xms128m –Xmx128m –XX:NewRatio =3 

  缺省值为:NewSize=2m MaxNewSize=32m SurvivorRatio=2。但是,如果 JVM 的堆大小大于 1GB,则应该使用值:-XX:newSize=640m -XX:MaxNewSize=640m -XX:SurvivorRatio=16,或者将堆的总大小的 50% 到 60% 分配给新生成的池。 

  举例:永久域默认大小为4m。运行程序时,jvm会调整永久域的大小以满足需要。每次调整时,jvm会对堆进行一次完全的垃圾收集。 
   使用-XX:MaxPerSize标志来增加永久域的大小。在WebLogic Server应用程序加载较多类时,经常需要增加永久域的最大值。当jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小。为了避免 调整,可使用-XX:PerSize标志设置初始值。 
   下面把永久域初始值设置成32m,最大值设置成64m。 
   java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m 

  默认状态下,HotSpot在新域中使用复制收集器 。该域一般分为三个部分。第一部分为Eden,用于生成新的对象。另两部分称为救助空间,当Eden充 满时,收集器停止应用程序,把所有可到达对象复制到当前的from救助空间,一旦当前的from救助空间充满,收集器则把可到达对象复制到当前的to救助 空间。From和to救助空间互换角色。维持活动的对象将在救助空间不断复制,直到它们获得使用期并转入旧域。使用-XX:SurvivorRatio可 控制新域子空间的大小。 

注:命令窗口,输入java -X,可看到全部参数:

JVM配置参数中文说明:

-----------------------------------------------------------------------

1、-Xmixed           mixed mode execution (default) 混合模式执行

2、-Xint             interpreted mode execution only解释模式执行

3、-Xbootclasspath:<directories and zip/jar files separated by ;>

      set search path for bootstrap classes and resources 设置zip/jar资源或者类(.class文件)存放目录路径

3、-Xbootclasspath/a:<directories and zip/jar files separated by ;>

   append to end of bootstrap class path追加zip/jar资源或者类(.class文件)存放目录路径

4、-Xbootclasspath/p:<directories and zip/jar files separated by ;>

      prepend in front of bootstrap class path 预先加载zip/jar资源或者类(.class文件)存放目录路径

5、-Xnoclassgc       disable class garbage collection 关闭类垃圾回收功能

6、-Xincgc           enable incremental garbage collection 开启类的垃圾回收功能

7、-Xloggc:<file>    log GC status to a file with time stamps 记录垃圾回日志到一个文件。

8、-Xbatch           disable background compilation关闭后台编译

9、-Xms<size>        set initial Java heap size设置JVM初始化堆内存大小

10、-Xmx<size>        set maximum Java heap size设置JVM最大的堆内存大小

11、-Xss<size>        set java thread stack size 设置JVM栈内存大小

12、-Xprof            output cpu profiling data 输入CPU概要表数据

13、-Xfuture          enable strictest checks, anticipating future default执行严格的代码检查,预测可能出现的情况

14、-Xrs              reduce use of OS signals by Java/VM (see documentation)通过JVM还原操作系统信号

15、-Xcheck:jni       perform additional checks for JNI functions 对JNI函数执行检查

16、-Xshare:off       do not attempt to use shared class data尽可能不去使用共享类的数据

17、-Xshare:auto      use shared class data if possible (default)尽可能的使用共享类的数据

18、-Xshare:on       require using shared class data, otherwise fail.尽可能的使用共享类的数据,否则运行失败

基本参数说明: 

-client,-server 
这两个参数用于设置虚拟机使用何种运行模式,client模式启动比较快,但运行时性能和内存管理效率不如server模式,通常用于客户端应用程序。相反,server模式启动比client慢,但可获得更高的运行性能。 
在windows上,缺省的虚拟机类型为client模式,如果要使用server模式,就需要在启动虚拟机时加-server参数,以获得更高性能,对服务器端应用,推荐采用server模式,尤其是多个CPU的系统。在Linux,Solaris上缺省采用server模式。 

-hotspot 
含义与client相同,jdk1.4以前使用的参数,jdk1.4开始不再使用,代之以client。 

-classpath,-cp 
虚拟机在运行一个类时,需要将其装入内存,虚拟机搜索类的方式和顺序如下: 
Bootstrap classes,Extension classes,User classes。 
Bootstrap 中的路径是虚拟机自带的jar或zip文件,虚拟机首先搜索这些包文件,用System.getProperty("sun.boot.class.path")可得到虚拟机搜索的包名。 
Extension是位于jre\lib\ext目录下的jar文件,虚拟机在搜索完Bootstrap后就搜索该目录下的jar文件。用System. getProperty("java.ext.dirs”)可得到虚拟机使用Extension搜索路径。 
User classes搜索顺序为当前目录、环境变量 CLASSPATH、-classpath。 

-classpath告知虚拟机搜索目录名、jar文档名、zip文档名,之间用分号.分隔。 
例如当你自己开发了公共类并包装成一个common.jar包,在使用common.jar中的类时,就需要用-classpath common.jar 告诉虚拟机从common.jar中查找该类,否则虚拟机就会抛出java.lang.NoClassDefFoundError异常,表明未找到类定义。 
在运行时可用System.getProperty(“java.class.path”)得到虚拟机查找类的路径。 
使用-classpath后虚拟机将不再使用CLASSPATH中的类搜索路径,如果-classpath和CLASSPATH都没有设置,则虚拟机使用当前路径(.)作为类搜索路径。 
推荐使用-classpath来定义虚拟机要搜索的类路径,而不要使用环境变量CLASSPATH的搜索路径,以减少多个项目同时使用CLASSPATH时存在的潜在冲突。例如应用1要使用a1.0.jar中的类G,应用2要使用a2.0.jar中的类G,a2.0.jar是a1.0.jar的升级包,当a1.0.jar,a2.0.jar都在CLASSPATH中,虚拟机搜索到第一个包中的类G时就停止搜索,如果应用1应用2的虚拟机都从CLASSPATH中搜索,就会有一个应用得不到正确版本的类G。 

-D=value 
在虚拟机的系统属性中设置属性名/值对,运行在此虚拟机之上的应用程序可用System.getProperty(“propertyName”)得到value的值。 
如果value中有空格,则需要用双引号将该值括起来,如-Dname=”space string”。 
该参数通常用于设置系统级全局变量值,如配置文件路径,应为该属性在程序中任何地方都可访问。 

-verbose[:class|gc|jni] 
在输出设备上显示虚拟机运行信息。 
verbose和verbose:class含义相同,输出虚拟机装入的类的信息,显示的信息格式如下: 
[Loaded java.io.FilePermission$1 from shared objects file] 
当虚拟机报告类找不到或类冲突时可用此参数来诊断来查看虚拟机从装入类的情况。 


-verbose:gc在虚拟机发生内存回收时在输出设备显示信息,格式如下: 
[Full GC 268K->168K(1984K), 0.0187390 secs] 
该参数用来监视虚拟机内存回收的情况。 


-verbose:jni在虚拟机调用native方法时输出设备显示信息,格式如下: 
[Dynamic-linking native method HelloNative.sum ... JNI] 
该参数用来监视虚拟机调用本地方法的情况,在发生jni错误时可为诊断提供便利。 

-version 
显示可运行的虚拟机版本信息然后退出。一台机器上装有不同版本的JDK时 

-showversion 
显示版本信息以及帮助信息。 

-ea[:...|:] 
-enableassertions[:...|:] 

从JDK1.4开始,java可支持断言机制,用于诊断运行时问题。通常在测试阶段使断言有效,在正式运行时不需要运行断言。断言后的表达式的值是一个逻辑值,为true时断言不运行,为false时断言运行,抛出java.lang.AssertionError错误。 
上述参数就用来设置虚拟机是否启动断言机制,缺省时虚拟机关闭断言机制,用-ea可打开断言机制,不加和classname时运行所有包和类中的断言,如果希望只运行某些包或类中的断言,可将包名或类名加到-ea之后。例如要启动包com.foo.util中的断言,可用命令 –ea:com.foo.util 。 

-da[:...|:] 
-disableassertions[:...|:] 
用来设置虚拟机关闭断言处理,packagename和classname的使用方法和-ea相同。 

-esa | -enablesystemassertions 
设置虚拟机显示系统类的断言。 


-dsa | -disablesystemassertions 
设置虚拟机关闭系统类的断言。 


-agentlib:[=] 
该参数是JDK5新引入的,用于虚拟机装载本地代理库。 

Libname为本地代理库文件名,虚拟机的搜索路径为环境变量PATH中的路径,options为传给本地库启动时的参数,多个参数之间用逗号分隔。在Windows平台上虚拟机搜索本地库名为libname.dll的文件,在Unix上虚拟机搜索本地库名为libname.so的文件,搜索路径环境变量在不同系统上有所不同,Linux、SunOS、IRIX上为LD_LIBRARY_PATH,AIX上为LIBPATH,HP-UX上为SHLIB_PATH。 
例如可使用-agentlib:hprof来获取虚拟机的运行情况,包括CPU、内存、线程等的运行数据,并可输出到指定文件中,可用-agentlib:hprof=help来得到使用帮助列表。在jre\bin目录下可发现hprof.dll文件。 

-agentpath:[=] 
设置虚拟机按全路径装载本地库,不再搜索PATH中的路径。其他功能和agentlib相同。 

-javaagent:[=] 
虚拟机启动时装入java语言设备代理。Jarpath文件中的mainfest文件必须有Agent-Class属性。代理类要实现public static void premain(String agentArgs, Instrumentation inst)方法。当虚拟机初始化时,将按代理类的说明顺序调用premain方法。 
参见:java.lang.instrument 



扩展参数说明 

-Xmixed 
设置-client模式虚拟机对使用频率高的方式进行Just-In-Time编译和执行,对其他方法使用解释方式执行。该方式是虚拟机缺省模式。 

-Xint 
设置-client模式下运行的虚拟机以解释方式执行类的字节码,不将字节码编译为本机码。 
-Xbootclasspath:path 
-Xbootclasspath/a:path 
-Xbootclasspath/p:path 
改变虚拟机装载缺省系统运行包rt.jar而从-Xbootclasspath中设定的搜索路径中装载系统运行类。除非你自己能写一个运行时,否则不会用到该参数。 
/a:将在缺省搜索路径后加上path 中的搜索路径。 
/p:在缺省搜索路径前先搜索path中的搜索路径。 

-Xnoclassgc 
关闭虚拟机对class的垃圾回收功能。 

-Xincgc 
启动增量垃圾收集器,缺省是关闭的。增量垃圾收集器能减少偶然发生的长时间的垃圾回收造成的暂停时间。但增量垃圾收集器和应用程序并发执行,因此会占用部分CPU在应用程序上的功能。 

-Xloggc: 
将虚拟机每次垃圾回收的信息写到日志文件中,文件名由file指定,文件格式是平文件,内容和-verbose:gc输出内容相同。 

-Xbatch 
虚拟机的缺省运行方式是在后台编译类代码,然后在前台执行代码,使用-Xbatch参数将关闭虚拟机后台编译,在前台编译完成后再执行。 

-Xms 
设置虚拟机可用内存堆的初始大小,缺省单位为字节,该大小为1024的整数倍并且要大于1MB,可用k(K)或m(M)为单位来设置较大的内存数。初始堆大小为2MB。 
例如:-Xms6400K,-Xms256M 

-Xmx 
设置虚拟机内存堆的最大可用大小,缺省单位为字节。该值必须为1024整数倍,并且要大于2MB。可用k(K)或m(M)为单位来设置较大的内存数。缺省堆最大值为64MB。 

例如:-Xmx81920K,-Xmx80M 

当应用程序申请了大内存运行时虚拟机抛出java.lang.OutOfMemoryError: Java heap space错误,就需要使用-Xmx设置较大的可用内存堆。 

-Xss 
设置线程栈的大小,缺省单位为字节。与-Xmx类似,也可用K或M来设置较大的值。通常操作系统分配给线程栈的缺省大小为1MB。 
另外也可在java中创建线程对象时设置栈的大小,构造函数原型为Thread(ThreadGroup group, Runnable target, String name, long stackSize)。 

-Xprof 
输出CPU运行时的诊断信息。 

-Xfuture 
对类文件进行严格格式检查,以保证类代码符合类代码规范。为保持向后兼容,虚拟机缺省不进行严格的格式检查。 

-Xrs 
减少虚拟机中操作系统的信号(singals)的使用。该参数通常用在虚拟机以后台服务方式运行时使用(如Servlet)。 

-Xcheck:jni 
调用JNI函数时进行附加的检查,特别地虚拟机将校验传递给JNI函数参数的合法性,在本地代码中遇到非法数据时,虚拟机将报一个致命错误而终止。使用该参数后将造成性能下降。

本文是网上内容学习总结。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值