2.JVM内存区域

程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池、直接内存

(经常有人把Java内存划分为栈内存和堆内存,实际的内存区域比这更复杂,,间接说明了程序员最关注的、与对象内存分配关系最密切的区域是“堆“和”栈“,“栈”通常就是指这里讲的虚拟机栈,或者更多情况下是指虚拟机栈中的局部变量表部分)

java的编译器和解释器浅析
https://www.jianshu.com/p/fc73b8aac5b7

 

1.1程序计数器

  程序计数器是一个记录着当前线程所执行的字节码的行号指示器,字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
  

  JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
  从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。

1.2程序计数器的特点
(1)程序计数器具有线程隔离性(线程私有、生命周期随着线程,线程启动而产生,线程结束而消亡)
(2)程序计数器占用的内存空间非常小,可以忽略不计
(3)程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
(4)程序执行的时候,程序计数器是有值的,其记录的是程序正在执行的字节码的地址
(5)执行native本地方法时,程序计数器的值为空。原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计
         原文链接:https://blog.csdn.net/sunhuiliang85/article/details/90718251

 

2.1Java虚拟机栈

  特点

  (1)Java 虚拟机栈线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
  (2)描述的是Java方法执行的内存模型,用于存储栈帧,每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程。
  (3)使用的内存不需要保证是连续的
  (4)Java 虚拟机规范即允许 Java 虚拟机栈被实现成固定大小(-Xss),也允许通过计算结果动态来扩容和收缩大小。如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候就已经确定。

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

2.2Java虚拟机栈栈帧

  概述

  栈帧是 Java 虚拟机栈中的单位元素,每个线程中调用同一个方法或者不同的方法,都会创建不同的栈帧(可以简单理解为,一个线程调用一个方法创建一个栈帧),所以,调用的方法链越多,创建的栈帧越多(代表作:递归)。在 Running 的线程,只有当前栈帧有效(Java 虚拟机栈中栈顶的栈帧),与当前栈帧相关联的方法称为当前方法。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。

  


  局部变量表(Local Variable Table) 变量槽(Variable Slot)为最小单位
  (1)存放方法参数和方法内部定义的局部变量在 Java 程序编译成 Class 文件时,在 Class 文件格式属性表中 Code 属性的 max_locals 数据项中确定了需要分配的局部变量表的最大容量。

  (2)每个 Slot 都应该存放的8种类型: byte、short、int、float、char、boolean、reference 这8种类型的数据,都可以使用32位或者更小的空间去存储对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。即 long 和 double 两种类型。

  (3)通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的 Slot 数量。如果是32位数据类型的数据,索引 n 就表示使用第 n 个 Slot,如果是64位数据类型的变量,则说明要使用第 n 和第 n+1 两个 Slot。

  (4)局部变量表中的第0位索引的 Slot 默认是用来传递方法所属对象实例的引用,在方法中可以通过关键字 this 来访问这个隐含的参数。其余参数按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。

  (5)局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为

  public static void main(String[]args)(){
    {
      byte[] placeholder=new byte[64*1024*1024];
    }
    int a=0;
    System.gc();
  }
  运行一下程序,却发现这次内存真的被正确回收了。
  [GC 66401K->65778K(125632K),0.0035471 secs]
  [Full GC 65778K->218K(125632K),0.0140596 secs]
  placeholder能否被回收的根本原因是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作(即没有int a=0这段代码),placeholder原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量表Slot清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下的“奇技”来使用。

  (6)关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在“准备阶段”。我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。
  public static void main(String[]args){

    int a;

    System.out.println(a);

  }

 

 

  操作数栈(Operand Stack)
  (1)操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。
  (2)同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。
  (3)操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。
  (4)当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。

  (5)在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机实现会做一些优化,令两个栈帧出现一部分重叠。让下面的栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无需进行额外的参数复制传递。

  (6)Java 虚拟机的解释执行引擎称为 “基于栈的执行引擎”,其中所指的 “栈” 就是操作数栈。

 

  动态连接(Dynamic Linking)

 

 

  方法返回地址
  (1)当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:areturn),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
  

  (2)无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。

  (3)概述:虚拟机会使用针对每种返回类型的操作来返回,返回值将从操作数栈出栈并且入栈到调用方法的方法栈帧中,当前栈帧出栈,被调用方法的栈帧变成当前栈帧,程序计数器将重置为调用这个方法的指令的下一条指令。

  

 

  附加信息
  虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

 

3.1 本地方法栈

  特点

  (1)本地方法栈是一个后入先出(Last In First Out)栈。

  (2)由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。

  (3)本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常

  

  概述

  本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。  

  Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

 

 

4.1Java堆

  特点

  (1)Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块,也被称为 “GC堆”,是被所有线程共享的一块内存区域,在虚拟机启动时被创建。

  (2)唯一目的就是储存对象实例和数组(JDK7 已把字符串常量池和类静态变量移动到 Java 堆),几乎所有的对象实例都会存储在堆中分配。随着 JIT 编译器发展,逃逸分析、栈上分配、标量替换等优化技术导致并不是所有对象都会在堆上分配。

  (3)Java 堆是垃圾收集器管理的主要区域。堆内存分为新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。

  (4)从内存分配的角度看,线程共享的 Java 堆中可能划分出多个线程私有的线程本地分配缓存区(Thread Local Allocation Buffer,TLAB)

  (5)根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx-Xms 控制)。

  (6)如果 Java 堆可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常。
 
 
 
5.1方法区
  特点  
  (1)方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域
  (2)用于存储已被虚拟机加载的类信息、常量(包含字符串常量)、类静态变量(即静态变量)、即时编译器编译后的代码等数据。(Java7之前位于Java堆永久代,之后移除永久代,位于另一块与堆不相连的本地内存 -- 元空间  Metaspace)
  (3)Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
  (4)运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern() 方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
  (5)在HotSpot虚拟机上开发、部署程序的开发者来说,人们都愿意把方法区称为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如BEA JRockit 、IBM J9 等)是不存在永久代的概念的。
  (6) 内存回收效率低,方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效.对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载.
  (7) Java虚拟机堆方法区的要求比较宽松和堆一样,允许固定大小,也允许可扩展的大小,还允许不识闲垃圾回收.
  

  5.2方法区(Method Area)存储的类信息
  对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  (1)这个类型的完整有效名称(全名=包名.类名)

  (2)这个类型直接父类的完整有效名称( java.lang.Object除外,其他类型若没有声明父类,默认父类是Object)

  (3)这个类型的修饰符(public、abstract、final的某个子集)

  (4)这个类型直接接口的一个有序列表
  除此之外还方法区(Method Area)存储类信息还有

  (5)类型的常量池( constant pool)

  (6)域(Field)信息

  (7)方法(Method)信息

  (8)除了常量外的所有静态(static)变量

  方法区(Method Area)存储类信息请参考:参考博客https://blog.csdn.net/zzhangxiaoyun/article/details/7518917

 

  5.3 方法区(Method Area)存储的常量
  (1)static final修饰的成员变量都存储于 方法区(Method Area)中
  

  5.4 方法区(Method Area)存储的静态变量
  (1)静态变量又称为类变量,类中被static修饰的成员变量都是静态变量(类变量)

  (2)静态变量之所以又称为类变量,是因为静态变量和类关联在一起,随着类的加载而存在于方法区(而不是堆中)

  (3)八种基本数据类型(byte、short、int、long、float、double、char、boolean)的静态变量会在方法区开辟空间,并将对应的值存储在方法方法区,对于引用类型的静态变量如果未用new关键字为引用类型的静态变量分配对象(如:static Object obj;)那么对象的引用obj会存储在方法区中,并为其指定默认值null;若,对于引用类型的静态变量如果用new关键字为引用类型的静态变量分配对象(如:static Person person = new Person();),那么对象的引用person 会存储在方法区中,并且该对象在堆中的地址也会存储在方法区中(注意此时静态变量只存储了对象的堆地址,而对象本身仍在堆内存中);这个过程还涉及到静态变量初始化问题,可以参考博客:静态变量初始化相关https://blog.csdn.net/u013241673/article/details/78221857

 

  5.5 方法区(Method Area)存储的方法(Method)
  (1)程序运行时会加载类编译生成的字节码,这个过程中静态变量(类变量)和静态方法及普通方法对应的字节码加载到方法区。

  (2)但是!!!方法区中没有实例变量,这是因为,类加载先于对应类对象的产生,而实例变量是和对象关联在一起的,没有对象就不存在实例变量,类加载时没有对象,所以方法区中没有实例变量

  (3)静态变量(类变量)和静态方法及普通方法在方法区(Method Area)存储方式是有区别的
  原文链接:https://blog.csdn.net/u013241673/article/details/78574770

  类信息详解https://www.jianshu.com/p/59f98076b382

  Class实例在堆中还是方法区中https://www.cnblogs.com/xy-nb/p/6773051.html

  方法区博客1https://blog.csdn.net/u014590757/article/details/79939900 

  方法区博客2https://www.cnblogs.com/Open-ing/p/11856399.html

  关于类变量和属性

 

6.1直接内存

  特点

  (1)直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

  (2)在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

  (3)本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。

  (4)NIO的Buffer提供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuffer更快。

  (5)在java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。

  (6)我的理解是堆内存的申请是直接从已分配的堆空间中取一块出来使用,不经过内存申请系统调用,而直接内存的申请则需要本地方法通过系统调用完成。

  博客https://blog.csdn.net/towads/article/details/78763421

  简书https://www.jianshu.com/p/c1ca975d6a33

 
posted @ 2019-12-31 16:40  骑着猪猛跑  阅读(133)  评论(0编辑  收藏  举报