精华内容
下载资源
问答
  • 根据使用情况,JVM运行时数据区域可分为六个区域:程序计数器虚拟机栈本地方法栈堆方法区运行时常量池如上所述,这些存储区域可以分为两类:线程私有(程序计数器,虚拟机栈,本机方法栈) 线程共享(堆,方法区,运行...

    Java虚拟机(JVM)定义了在程序执行期间使用的各种运行时数据区域,这些JVM数据区域中的某些区域是按线程创建的,其他区域则是在JVM启动时创建的,并且内存区域在线程之间共享。

    a28386fcf669cdea34995b17992c7883.png

    根据使用情况,JVM运行时数据区域可分为六个区域:

    程序计数器

    虚拟机栈

    本地方法栈

    方法区

    运行时常量池

    c27eeea2cb657ee915ff2658cc611b29.png

    如上所述,这些存储区域可以分为两类:

    线程私有(程序计数器,虚拟机栈,本机方法栈) 线程共享(堆,方法区,运行时常量池) 程序计数器

    在JVM中,在任何给定时间,可能正在执行许多线程。每个执行线程都有自己的PC寄存器。

    如果JVM线程执行的方法是JAVA方法,则PC寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程正在执行本机方法,则Java虚拟机的pc寄存器的值未定义。

    虚拟机栈

    每个JVM线程都有自己的JVM堆栈,该堆栈在线程启动时创建。JVM堆栈存储被推入堆栈并从堆栈弹出的框架,而JVM堆栈永远不会被直接操纵。在发生任何异常时,您都可以通过此堆栈跟踪获取每个元素代表一个堆栈框架的位置。

    与Java虚拟机堆栈相关的特殊条件:

    如果线程中的计算需要比允许的Java虚拟机更大的堆栈,则Java虚拟机将抛出StackOverflowError。 如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展,但是可以提供足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,则Java虚拟机机器抛出OutOfMemoryError。

    虚拟机栈中的框架

    调用方法时会创建一个新框架,然后将该框架推入该线程的JVM堆栈中。当框架的方法调用完成时,该框架将被销毁。

    每个框架都有自己的局部变量数组,自己的操作数堆栈以及对当前方法类的运行时常量池的引用。局部变量数组和操作数堆栈的大小在编译时确定,并与与帧关联的方法的代码一起提供。

    在任何时候,只有一个帧处于活动状态,这是执行方法的帧。该帧称为当前帧,其方法称为当前方法。定义当前方法的类是当前类。

    请注意,由线程创建的框架在该线程本地,并且不能被任何其他线程引用。

    局部变量 -创建并添加到JVM堆栈的每个框架都包含一个称为其局部变量的变量数组。局部变量数组的长度在编译时自行确定,并以类或接口的二进制表示形式以及与框架关联的方法的代码提供。 操作数堆栈 –每个帧都包含一个称为帧的操作数堆栈的后进先出(LIFO)堆栈。操作数堆栈的最大深度称为编译时间本身,并与与帧关联的方法的代码一起提供。 执行动态链接 -在已编译的.class文件中,方法的代码是指要调用的方法和要通过符号引用访问的变量。这些符号方法引用通过动态链接转换为具体的方法引用,并根据需要加载类以解析符号在那个时候还没有定义。   本地方法栈

    JVM也可以使用常规堆栈来支持本地方法,本地方法是用Java编程语言以外的其他语言编写的方法。创建每个线程时,将为每个线程分配本地方法栈。

    以下异常条件与本机方法堆栈相关联:

    如果线程中的计算所需的本机方法堆栈超出允许的范围,则Java虚拟机将引发StackOverflowError。 如果可以动态扩展本机方法堆栈并尝试进行本机方法堆栈扩展,但可以提供足够的内存,或者可以提供足够的内存来为新线程创建初始本机方法堆栈,则Java虚拟机将引发OutOfMemoryError。

    堆是JVM运行时数据区域,从中可以将内存分配给对象,实例变量和数组。堆是在JVM启动时创建的,并在所有Java虚拟机线程之间共享。一旦存储在堆中的对象没有任何引用,该对象的内存就会被垃圾收集器回收,该垃圾收集器是一种自动存储管理系统。对象永远不会显式释放。

    以下异常情况与堆相关联:

    如果计算需要的堆多于自动存储管理系统可以提供的堆,则Java虚拟机将引发OutOfMemoryError。

    方法区

    JVM具有在所有JVM线程之间共享的方法区域。方法区域存储有关已加载的类和接口的元数据。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码。

    对于JVM加载的每种类型,存储在方法区域中的类型信息如下-

    类/接口的全限定名称。

    任何直接超类的完全限定名称。

    使用的修饰符。

    任何扩展超级接口的全限定名称。

    区分加载类型是类还是接口的信息。

    除类型信息方法区域外,还存储:

    运行时常量池。

    字段信息,包括字段名称,类型,修饰符。

    方法信息,包括方法名称,修饰符,返回类型,参数。

    静态(类)变量。

    方法代码,包含字节码,局部变量大小,操作数堆栈大小。

    方法区域通常是非堆存储器的一部分,以前被称为PermGen空间。注意, PermGen Space已从Java 8更改为MetaSpace。

    以下异常条件与方法区域相关联:

    如果无法使方法区域中的内存可用以满足分配请求,则Java虚拟机将抛出OutOfMemoryError。   运行时常量池

    运行时常量池是该类的constant_pool表的每个类或每个接口存储。Constant_pool包含在编译时已知的常量(字符串文字,数字文字),它还存储必须在运行时解析的方法和字段引用。

    运行时常量池在线程之间共享,并从JVM的方法区域分配。

    7e6ac99f06f6d76cf6f76841749072c1.png

    不是将所有内容存储在字节码中,而是为该类维护单独的常量池,并且字节码包含对常量池的引用。这些符号参考通过动态链接转换为具体的方法参考。

    展开全文
  • 原标题:初级Java程序员必备知识点:线程私有区域线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否...

    原标题:初级Java程序员必备知识点:线程私有区域

    线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死)。

    b70fabbd07467a1affd67e3fff4d1349.png

    1.Native Method Stack本地方法栈

    是在Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

    2. PC Register程序计数器

    每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

    作用是当前线程所执行字节码的行号指示器(类似于传统CPU模型中的PC), PC在每次指令执行后自增, 维护下一个将要执行指令的地址. 在JVM模型中, 字节码解释器就是通过改变PC值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC完成(仅限于Java方法, Native方法该计数器值为undefined).

    不同于OS以进程为单位调度, JVM中的并发是通过线程切换并分配时间片执行来实现的. 在任何一个时刻, 一个处理器内核只会执行一条线程中的指令. 因此, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 这类内存被称为“线程私有”内存.

    3. Java Stack(虚拟机栈)

    栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。

    基本类型的变量和对象的引用变量都是在函数的栈内存中分配。

    栈帧中主要保存3类数据:

    本地变量(Local Variables):输入参数和输出参数以及方法内的变量;

    栈操作(Operand Stack):记录出栈、入栈的操作;

    栈帧数据(Frame Data):包括类文件、方法等等。

    栈运行原理

    栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。

    遵循“先进后出”/“后进先出”原则。返回搜狐,查看更多

    责任编辑:

    展开全文
  • 器,这类内存也称为“线程私有” 的内存。 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是 Native 方法,则为空。 这个内存区域是唯一一个在虚拟机中没有规定任何 ...

    一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数
    器,这类内存也称为“线程私有” 的内存。
    正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址) 。如果还是
    Native 方法,则为空。
    这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

    展开全文
  • 操作系统负责线程调度到CPU,本地线程初始化成功,就会调用Java线程的run方法。 JVM的系统线程: 虚拟机线程: 周期任务线程 GC线程 编译线程:将字节码编译成本地代码 信号调度线程 程序计数器(Program Counter ...

    概述

    上一篇学习了JVM的类加载子系统,这次就到JVM的核心内容之一——运行时数据区。因其内容较多,本篇先学习线程私有区部分。上图:
    在这里插入图片描述
    运行时数据区可分为两部分:线程私有区和线程共享区。线程私有区主要包含三个区域:虚拟机栈本地方法栈程序计数器。线程共享区主要包含:堆区,方法区。这次主要学习线程私有区,再上图看下线程私有区的内容。

    在这里插入图片描述

    图中基本解释了线程私有区的划分和作用,接下来就通过问题的方式对每个区域进行深入的了解。

    问题

    1.为什么会有运行时数据区的概念呢?

    JVM为Java程序提供了内存管理的功能:当我们启动一个Java程序,或者说JVM程序时,就开辟了运行时数据区这一块内存空间(逻辑上),当我们new一个对象时,所有申请内存,分配,赋值等操作JVM都帮我们完成了。为了更好的管理Java程序的内存空间,JVM会把这一块内存空间划分了不同区域和制定了相应的规则,所以就有了运行时数据区。

    在Java程序中可以通过Runtime实例与JVM沟通。

    /**
     * Every Java application has a single instance of class
     * <code>Runtime</code> that allows the application to interface with
     * the environment in which the application is running. The current
     * runtime can be obtained from the <code>getRuntime</code> method.
     * <p>
     * An application cannot create its own instance of this class.
     *
     * @author  unascribed
     * @see     java.lang.Runtime#getRuntime()
     * @since   JDK1.0
     */
    public class Runtime {
    }
    

    PS: JVM提供了内存管理的功能,无需像C语言一样申请,释放内存空间,减少了开发人员额外的工作量。但是呢,我们更需要对运行时数据区有更深的了解,否则在遇到问题的时候我们一无所知,就很难定位问题原因。

    2. 为什么需要程序计数器(program counter register)?

    1). 程序计数器是存储下一条指令的地址,分支,循环,跳转,异常处理,线程恢复等基础功能都是依赖它完成的。

    2). 程序计数器在多线程并发的场景里十分必要,在CPU不断切换线程执行时,切换到当前线程的话,CPU就可以通过程序计数器存储的地址读取到下一条要执行的指令。这也是为什么程序计数器是线程私有的,可以保证在CPU不断切换过程中,保证每个线程的执行不会出错。

    示例:

        public int testPcCount() {
            int a = 1;
            int b = 2;
            int c = 0;
            if (a == 1) {
                c = a + b;
            } else {
                c = b - a;
            }
            return c;
        }
    
    
    
    public int testPcCount();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: iconst_1
             1: istore_1
             2: iconst_2
             3: istore_2
             4: iconst_0
             5: istore_3
             6: iload_1
             7: iconst_1
             8: if_icmpne     18
            11: iload_1
            12: iload_2
            13: iadd
            14: istore_3
            15: goto          22
            18: iload_2
            19: iload_1
            20: isub
            21: istore_3
            22: iload_3
            23: ireturn
    
    

    代码中写了个简单的if-else判断逻辑,可以看到指令前面的1,2,3,4…就是指令地址,而在第8条指令if_icmpne时,后面带了一个18的指令地址,也就是说如果判断条件不符合的话,程序计数器的值就会变成18,执行引擎也就会去执行第18行的指令。第15行的goto语句也是类似。PS:如果是native方法的话,会变成undefined。

    总结:程序计数器是对物理PC寄存器的抽象模拟,所以其空间很小,但运行速度最快,他也是唯一没有OutOfMemory的区域。

    3. StackOverflow是什么?

    首先,我们得明确StackOverflow中的stack其实就是虚拟机栈,StackOverflow也就是虚拟机栈空间不足溢出。虚拟机栈中保存的是和方法一一对应的栈帧,执行一个方法即对应一个栈帧的压栈,方法执行完毕对应栈帧的出栈。那么就可以知道StackOverflow出现的原因就是栈帧太多,导致内存不足。最容易想到的场景那就是递归了,递归没有跳出语句时,就会出现StackOverflow。那么如何解决呢?

    解决方案:当然就是扩大虚拟机栈大小。如何扩?JVM规范允许虚拟机栈的大小是动态的或固定不变的。

    固定大小:可以在Java程序启动时,设置虚拟机栈大小。使用 -Xss 设置, 作为Java后端开发者对-Xss应该都很熟悉。现在应该会更清楚-Xss的意思。-Xss 512k扩展:-Xmx -Xms是经常与-Xss一起出现的两个参数,后续也会学习。

    动态扩展:在内存不足时,可以尝试进行扩展。如果在尝试扩展时发现无法申请到足够内存,则也会报出OutOfMemory。

    4. 栈中可以存储对象嘛?

    这好像是以前面试常问到的问题。栈其实是泛指,实际指的应该是局部变量表中可以存储对象嘛?答案是不可以,局部变量表是一个数字数组,存储方法参数和定义在方法内的局部变量。包括8种基本数据类型和对象引用,以及returnAddress类型。这也就是常说的栈存的是引用,堆存的是对象。

    示例:

        public void testLocalVariables(){
            int a = 3;
            boolean b = true;
            byte c = 10;
            double d = 1.00;
            long e = 5;
            Book book = new Book();
        }
    
    
      public void testLocalVariables();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=9, args_size=1
             0: iconst_3
             1: istore_1
             2: iconst_1
             3: istore_2
             4: bipush        10
             6: istore_3
             7: dconst_1
             8: dstore        4
            10: ldc2_w        #37                 // long 5l
            13: lstore        6
            15: new           #39                 // class com/yu/other/TestJvm$Book
            18: dup
            19: aload_0
            20: invokespecial #40                 // Method com/yu/other/TestJvm$Book."<init>":(Lcom/yu/other/TestJvm;)V
            23: astore        8
            25: return
          LineNumberTable:
            line 83: 0
            line 84: 2
            line 85: 4
            line 86: 7
            line 87: 10
            line 88: 15
            line 89: 25
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      26     0  this   Lcom/yu/other/TestJvm;
                2      24     1     a   I
                4      22     2     b   Z
                7      19     3     c   B
               10      16     4     d   D
               15      11     6     e   J
               25       1     8  book   Lcom/yu/other/TestJvm$Book;
    
    
    
    

    testLocalVariables是我写的方法,编译后对class进行javap -v,就可以看到LocalVariableTable。知识点来了。

    知识点1: 表头的含义

    Start和Length: Start是指该变量的起始位置,Length可以理解为作用长度,就是说这个变量可以在多长的范围内使用。

    Slot:中文翻译:槽,是局部变量填充的基本单位。

    Name:很好理解,就是局部变量的名字

    Signature: 以前还叫descriptor,就是变量类型的标记

    变量类型Signature
    intI
    booleanZ
    shortS
    byteB
    floatF
    charC
    doubleD
    longJ
    referenceL+类的全路径名(eg: Lcom/yu/other/TestJvm)

    知识点2: this

    局部变量表中第一位是this,当前方法是构造方法或实例方法的话,局部变量表中的对象就是this引用,其实也很好理解,我们经常在实例方法中通过this获取当前类的实例变量,所以this引用要放在当前方法中。

    知识点3: Start&Length

    我们通过this来进一步了解下Start和Length,this初始位置是0,作用域长度是26。我们最后一条指令地址是25,所以总长度就是26,this从初始位置到方法结束都可以使用,那么作用域也就是从0开始,长度为26。再看一个作用域的例子

        public void testLocalStartAndLength(){
            {
                int a = 3;
                int b = 5;
                int c = a + b;
                System.out.println(c);
            }
            int d = 10;
        }
        
         public void testLocalStartAndLength();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: iconst_3
             1: istore_1
             2: iconst_5
             3: istore_2
             4: iload_1
             5: iload_2
             6: iadd
             7: istore_3
             8: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
            11: iload_3
            12: invokevirtual #42                 // Method java/io/PrintStream.println:(I)V
            15: bipush        10
            17: istore_1
            18: return
          LineNumberTable:
            line 96: 0
            line 97: 2
            line 98: 4
            line 99: 8
            line 101: 15
            line 102: 18
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                2      13     1     a   I
                4      11     2     b   I
                8       7     3     c   I
                0      19     0  this   Lcom/yu/other/TestJvm;
               18       1     1     d   I
    }
    
    

    从上面的例子就能更清楚的理解作用域的含义,a,b,c三个字段是无法在括号之外使用的,所以作用域都是在指令地址15之前可用。

    知识点4 : Slot的重复利用

    根据知识点3可知,局部变量是有作用域的,出了作用域,局部变量即失效。而Slot是用来存储局部变量的,那么出了作用域的局部变量就可以废弃了,也就引出了Slot的重复利用。当局部变量失效后,所占的Slot也就空闲了,后续声明的变量就可以使用之前的Slot了。看知识点3在括号外,声明的int d变量,可以看到所占Slot是1,而1之前是变量a的Slot。这也就是Slot的重复利用。

    另外,这里也看到this所在位置并不是第一位,但是呢,可以看到,其实际位置,作用域,Slot是放在初始位置的。为什么不把this放到第一位呢?我们再看个例子,我也是在测试中发现这个原因的。

    首先普及一下,一个Slot大小是32位,基本数据类型中double,long是64位,占2个Slot,其余都是32位,占1一个Slot。那么问题来了,当一个int值出了作用域之后,声明了一个long值,slot会复用嘛?JVM很巧妙的解决了这个问题。我们举个例子

        public void testLocalStartAndLengthV2(){
            int b;
            int c = 2;
            {
                int a = 1;
                b = 3;
                c = a + b;
            }
            long d = 10;
        }
    
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                4       6     3     a   I
                0      15     0  this   Lcom/yu/other/TestJvm;
                6       9     1     b   I
                2      13     2     c   I
               14       1     3     d   J
    
    
    

    作用域小的a被放在了局部变量表第一位,同时被赋予了最后一个Slot。这样后续声明long值时,就可以复用a的slot加上后面的slot。

    故:可以理解到JVM会优先把作用域小的变量优先处理,并置于最后的Slot,以便后续Slot的复用。

    知识点5: locals=9

    在知识点1中,code下面有这么一段语句:stack=3, locals=9, args_size=1。

    这里locals就是局部变量表的大小,也就是slot的数量。可以理解到局部变量表的大小在编译时就已经确定!

    知识点6:局部变量表会被GC嘛?

    局部变量表是线程私有的,和线程的生命周期保持一致。所以不会被GC。但局部变量表是GC的重要根节点。GC的引用计数法,就是根据局部变量表中的引用是否还存在,此外这里还涉及到四大引用,强引用,软引用,弱引用,虚引用。也是GC的重要依据,后续会再做进一步的解释。

    4. 操作数栈是如何运行的?

    直接上代码:

        public void testOperandStack() {
            int a = 3;
            int b = 5;
            int d = 6;
            int c = a * (b + d);
        }
    
      public void testOperandStack();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=5, args_size=1
             0: iconst_3             //声明int变量3
             1: istore_1             //存储到局部变量表位置1中
             2: iconst_5           
             3: istore_2
             4: bipush        6      //声明int变量6
             6: istore_3             //存储到局部变量表位置3中
             7: iload_1              //从局部变量表位置1的变量加载到操作数栈
             8: iload_2              //从局部变量表位置1的变量加载到操作数栈 
             9: iload_3
            10: iadd                 //栈顶两元素相加,放在栈顶
            11: imul                 //栈顶两元素相乘,放在栈顶    
            12: istore        4      //将栈顶元素存到局部变量表位置4中。 
            14: return
    
    

    从字节码指令的注释中,就可以看到iload操作就是把需要进行运算的值加载到操作数栈。iadd,imul就是对操作数栈中的值进行运算。并把临时运算结果存储在操作数栈中。

    ps:这里有个小知识点,细心的朋友可能会看到,声明d变量的时候用的是bipush,而不是iconst,这是因为JVM对于int不同范围的取值使用了不同的指令:当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令。有兴趣的可以研究下。

    5.栈顶缓存技术

    操作数都是存储在内存中的,因此频繁地执行内存读写操作必然会影响执行速度,Hotspot的设计者提出了栈顶缓存:将栈顶元素全部缓存在物理CPU的寄存器中,降低对内存的读写次数,提高执行引擎的执行效率。

    6. 方法的重写和重载的区别

    方法调用示例

    了解重写和重载之前,先从JVM角度看下方法的实质,看下常见方法调用的字节码:

        private void testPrivate() {
        }
    
        public void testPublic() {
        }
    
        private static void testStatic() {
        }
    
        public final void testFinal() {
        }
    
    
        public void testMethod() {
            testPrivate();
            testPublic();
            testStatic();
            testFinal();
        }
    
    public void testMethod();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #2                  // Method testPrivate:()V
             4: aload_0
             5: invokevirtual #3                  // Method testPublic:()V
             8: invokestatic  #4                  // Method testStatic:()V
            11: aload_0
            12: invokevirtual #5                  // Method testFinal:()V
            15: return
    
    

    从字节码指令能看到调用方法时,有以下几个指令:invokespecialinvokevirtualinvokestatic,指令后面跟着#2,#3等序号。所以基本可以确定字节码里方法调用分为两部分:指令+序号。我们先深入了解下这两块。

    知识点1 调用方法的字节码指令

    调用方法的字节码指令分为下面5个指令,6种情况。其中invokedynamic是在jdk1.7时才出现。

    指令描述备注
    invokespecial实例构造器,私有实例方法,父类实例方法静态解析
    invokestatic静态方法静态解析
    invokevirtual非私有实例方法 final修饰静态解析
    invokevirtual非私有实例方法 非final修饰动态分派
    invokeinterface接口方法动态分派
    invokedynamic动态方法动态分派

    现在我们先总结出一点:不同类型的方法在调用时,对应JVM不同的字节码指令

    知识点2 符号引用

    字节码指令后面都会跟上一个地址:#2,#3,#4…很明显这是个索引序号。从动态链接里,我们知道Java文件在编译成class文件时,会把所有的成员变量和方法转做符号引用存储在class文件的常量池,在class文件加载时,会把这部分信息加载到运行时常量池中。

    那就先到常量池中看下索引序号的信息,如下(javap反编译后的字节码文件的最上方就是常量池信息):

    Constant pool:
       #1 = Methodref          #23.#68        // java/lang/Object."<init>":()V
       #2 = Methodref          #22.#69        // com/yu/other/TestMethod.testPrivate:()V
       #3 = Methodref          #22.#70        // com/yu/other/TestMethod.testPublic:()V
       #4 = Methodref          #22.#71        // com/yu/other/TestMethod.testStatic:()V
       #5 = Methodref          #22.#72        // com/yu/other/TestMethod.testFinal:()V
    
    

    我们之前的序号是从2开始的,看常量池就明白了原来类的构造函数方法排在第一位。

    序号后面是Methodref,表示是方法引用,指向的就是对应的方法。当然,并没这么简单。我们先来剖析一下后面注释的内容: com/yu/other/TestMethod.testPrivate:()V,这个在JVM中就叫做符号引用。存储在常量池中的也就是这个内容。那我们来看下符号引用的组成结构:
    在这里插入图片描述
    符号引用,顾名思义仅仅是个符号,那么调用方法时,如何找到实际方法呢?再次回顾下动态链接的作用:把符号引用转为调用方法的直接引用。那就续上了。小结:方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)

    知识点3 符号引用到直接引用

    方法调用根据调用方法能否在编译期间确定分为两种方式:静态解析,动态分派。下面是一些概念,主要记住静态解析,动态分派;非虚方法和虚方法。

    调用方法可以在编译期间确定调用方法无法在编译期间确定
    方法调用静态解析动态分派
    符号引用转为直接引用静态链接动态链接
    方法的绑定机制早期绑定晚期绑定
    方法类型非虚方法虚方法

    符号引用转为直接引用会因为调用方法是否可以在编译期间确定而有不同。回看知识点1中字节码指令表格内的备注,不同的字节码指令就已经确定了是静态链接还是动态链接,除了invokevirtual,这个方法需要区分是否方法是否是final方法来确定是静态链接还是动态链接。

    静态链接对应的方法:static,private,构造函数,final,super。

    静态链接加载过程

    类加载解析阶段:如果对类的加载还有印象的话,记得在链接的解析的作用就是将常量池的符号引用转换为直接引用。过程:根据符号引用中的类名,找到类中简单方法名和方法描述符一致的方法,找到的话就转为直接引用。否则依次向上到父类中查找。

    调用阶段:已转为直接引用,直接调用即可。invokestatic,invokespecial的区别在于:invokespecial调用前,需要将实例对象加载到操作数栈,invokestatic不用。

    invokevirtual过程:

    类加载解析阶段:解析类的继承关系,生成类的虚方法表。

    虚方法表:类中声明的虚方法的入口地址会按固定顺序存放在虚方法表中。虚方法表会继承父类的虚方法表,顺序与父类保持一致,子类新增的方法按顺序添加到虚方法末尾,子类重写的父类方法,则重写方法位置的入口地址修改为子类实现。

    调用阶段: 动态分派,根据变量的实际类型,查找实际类的虚方法表,并且根据索引找到对应方法的实际地址。

    invokeinterface与invokevirtual类似,但因为Java接口是可以多继承的,虚拟机提供了itable(interface method table)来支持多接口,itable由偏移量表offset table与方法表method table两部分组成。当需要调用某个接口方法时,虚拟机会在offset table查找对应的method table,随后在该method table上查找方法。

    知识点4 重载和重写

    现在我们再回头看看重载和重写,先从表现角度看看:

    重载:相同的函数名,不同的参数类型或个数

    重写:子类集成父类,重新父类的方法,函数名相同,参数类型和个数均一致。

    重载方法在编译成class文件生成符号引用时,会因为方法描述符的不同而区分为不同的符号引用,从JVM的角度来看就是不同的方法,和其他方法无任何区别。

    重写方法:在加载解析阶段,生成虚方法表或接口方法表;在调用阶段,查询虚方法表或者接口方法表才能取得方法的入口地址。

    7. 方法返回地址的作用

    方法返回地址:存储调用该方法的程序计数器的值。

    作用:方法的调用和完成从JVM来看就是栈帧的压栈和出栈。在出栈时,需要恢复调用该方法的上层方法的运行信息。

    1.将程序计数器的值设置为方法返回地址,以便上层方法继续执行;

    2.若当前方法有返回值,会将返回值压入上层方法的操作数栈

    上述都是在方法正常直接结束的情况,那如果抛了异常呢?当Java代码被编译成class文件时,会生成一个异常表。

    当方法出现异常时,会遍历异常表中的所有条目,当触发异常的字节码的索引值在某个异常表条目的监控范围内,JVM会判断抛出的异常和想要捕获的异常进行匹配,若匹配,则将程序计数器的值转为该条目的target的值。反之,则继续查找,直到结束。

    若结束也没有匹配上的话,当前栈帧出栈时,不会给上层方法传递相关值,而上层方法会重复前面的步骤,去做异常的匹配。最坏的情况,JVM会遍历当前线程所有栈帧的异常表。

    异常表示例如下:方法声明中throws的异常是不在异常表的。

        public void testException() throws IndexOutOfBoundsException, FileNotFoundException {
            try {
                System.out.print("1111");
            } catch (NumberFormatException e) {
                System.out.print("2222");
            }
    
            try {
                System.out.print("1111");
            } catch (ArithmeticException e) {
                System.out.print("2222");
            }
        }
        
        
         public void testException() throws java.lang.IndexOutOfBoundsException, java.io.FileNotFoundException;
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String 1111
             5: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
             8: goto          20
            11: astore_1
            12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            15: ldc           #6                  // String 2222
            17: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
            20: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            23: ldc           #3                  // String 1111
            25: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
            28: goto          40
            31: astore_1
            32: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            35: ldc           #6                  // String 2222
            37: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
            40: return
          Exception table:
             from    to  target type
                 0     8    11   Class java/lang/NumberFormatException
                20    28    31   Class java/lang/ArithmeticException
    
        
    

    异常表中from,to就是异常捕获的字节码指令地址范围,target就是捕获到异常后跳转到的指令地址。

    总结

    JVM运行时数据区分为线程私有区和共享区,线程私有区的重点就是我们常说虚拟机栈。当然其中细节很多,也很复杂,妄图通过一篇文章学完也是不现实的事情,至少可以记得其大概组成结构,每个结构的大概作用。在和其他程序员交谈时,总不会显得自己还是个不懂JVM的初级程序员吧!

    在这里插入图片描述

    参考链接:
    https://www.jianshu.com/p/b5b919f24f82

    展开全文
  • 转载自http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html1. 概述多任务和高并发是衡量一台计算机处理器的能力重要指标之一。一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions ...
  • 下面是Java线程相关的热门面试题,你可以用它来好好准备面试。1) 什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以...
  • Java线程安全示例

    2021-03-04 05:36:43
    基础知识根据前面学到的Java内存模型理论知识,我们来... 根据Java内存运行时数据分配,静态变量存于方法区中,实例对象存于堆中,此二区域为线程共享,而方法中的变量存于虚拟机栈,为线程私有。对象的状态可能包...
  • 网上搜索了一下,关于java的线程栈:JDK...程序计数器每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令。2.线程栈线程的每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量...
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的...
  • Thread类中有个变量threadLocals,这个类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。 Thread中的成员变量threadLocals: /* ...
  • 目录:缓存线程安全问题的本质出现线程安全的问题本质是由于:安全主内存和工做内存数据不一致性以及编译器重...并发CPU的运算速度是很是快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据...
  • 原标题:技术分享:JAVA线程安全之内存模型一、java内存模型1、简介 各个区域的解释和功能方法区(Method Area):方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、...
  • Java内存区域划分

    2021-02-28 12:54:56
    ☝️ 灰色部分(Java栈,本地方法栈和程序计数器)是线程私有,不存在线程安全问题,橙色部分(方法区和堆)为线程共享区。2. 类加载器类加载器(Class Loader)负责加载class文件,class文件在文件开头有特定的文件标识,...
  • 一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁);如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程...
  • Java线程并发:知识点

    2021-03-21 10:24:03
    Java线程并发:知识点发布:一个对象是使它能够被当前范围之外的代码所引用:常见形式:将对象的的引用存储到公共静态域;非私有方法中返回引用;发布内部类实例,包含引用。逃逸:在对象尚未准备好时就将其发布。...
  • 栈是线程私有的,每个线程都是自己的栈,每个线程中的每个方法在执行的同时会创建一个栈帧用于存局部变量表、操作数栈、动态链接、方法返回地址等信息。每一个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机...
  • Java线程之内存模型

    2021-02-27 16:55:17
    目录多线程需要解决的问题线程之间的通信线程之间的同步Java内存模型内存间的交互操作指令屏障happens-before规则指令重排序从源程序到字节指令的重排序as-if-serial语义程序顺序规则顺序一致性模型顺序一致性模型...
  • 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,...
  • JAVA线程大总结篇

    2021-02-26 18:49:00
    线程一:java语言中,实现线程的两种方式java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。//定义线程类public class ...
  • Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程Java 线程结束,原生线程随之被回收。操作...
  • Java线程面试题

    2021-03-16 16:34:48
    1.什么是线程线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需...
  • 1、java线程模型

    2021-03-06 21:42:35
    要了解多线程,先需要把java线程模型搞清楚,否则有时候很难理清楚一个问题。硬件多线程:物理机硬件的并发问题跟jvm中的情况有不少相似之处,物理机的并发处理方案对于虚拟机也有相当大的参考意义。在买电脑或者...
  • 直接上图:从图中看到,JVM内存分为两个主要区域,一个是所有线程共享的数据区,一个是线程隔离数据区线程私有)线程隔离数据区程序计数器(ProgramCounterRegister):一小块内存空间,单前线程所执行的字节码行号指示...
  • 2019独角兽企业重金招聘... 一天,程序崩溃:## There is insufficient memory for the Java Runtime Environment to continue.# Native memory allocation (malloc) failed to allocate 32756 bytes for ChunkPo...
  • Java 线程实现原理

    2021-04-07 18:20:56
    Linux 操作系统中创建线程的方式 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 参数名 参数定义 参数解释 pthread_t *thread 传出...
  • java线程基础一

    2021-01-06 22:51:24
    什么是线程 在讨论什么是线程前有必要先说一下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行...在Java中,当我们启动main函数时其实就启..
  • 相信大多使用Java的同学曾经都遇到过因为写多线程代码引起的一些不正常现象,所以在使用需要多线程的地方会很小心或者刻意避免使用多线程。但是另一方面,它能够显著提高应用性能、改善应用结构、增加处理器执行效率...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 56,006
精华内容 22,402
关键字:

java线程私有的区域

java 订阅