JVM知识(下)

本次主要介绍,JVM的方法区,堆,栈。以下内容主要还是参考《Inside JVM》

方法区

在java虚拟机实例中,加载类型的信息存储在一个逻辑区域被成为方法区。当java虚拟机加载一个类型时,它使用一个类加载器去加载合适的类文件。类文件中的类加载器读取二进制数据的线性流,并将其传递到虚拟机中。虚拟机中提取信息中的二进制数据类型和存储信息的方法。内存类(静态类)中声明的变量也存在于方法区中。

Java虚拟机代表类型信息内部实现由设计师决定实现的。例如,类文件中的多字节数量存储在高位优先(首先最重要的字节)。当数据被导入到方法区,然而,虚拟机可以以任何方式存储数据。如果一个实现处于低位优先处理上,设计师也就是开发者可以通过多字节的值存储在方法区低位优先顺序上。
因为虚拟机执行应用程序托管,所以将搜索和使用类型的信息存储在方法区。开发者必须尝试设计数据结构,以促进快速执行java应用程序的执行效率,但还必须想到紧密性。如果设计一个实现,运行,在低内存的约束中,开发者可以在紧密性的因素下权衡一些执行的速度。如果设计一个实现,那它将运行在虚拟内存的系统中,另一方面,开发者可以决定将冗余信息存储在方法区来促进执行速度(如果底层主机不提供虚拟内存,但提供来一个硬盘,开发者可以创造自己的虚拟内存系统作为实现的一部分)。从优化性能上开发者可以选择任何数据结构和组织来实现上下文的需求。
所有的线程都共享同一个方法区,所以访问方法区的数据一定要考虑线程的安全性。例如两个线程都去访问一个名为Lava的类并且该类没有被加载过,一个线程被允许加载这个类时,另一个线程必须等待。
方法区的大小不需要固定的地方存储,在java程序运行的时候,虚拟机可以根据程序的需要来扩张和收缩方法区的大小。此外,方法区的内存不需要是连续的。实现可能允许用户或程序员指定一个初始大小的方法,以及最大或最小值。
方法区也可以被垃圾回收的。因为通过程序java程序可以动态扩展类加载器对象,类可以成为未引用。如果一个类没有引用到,java虚拟机可以通过GC垃圾回收来保持最少的方法占据内存的区域。没有加载过的类包括类里的条件都变为“未引用”。后续再将类的生命周期。

类型信息

虚拟机加载的每种类型信息类型,java虚拟机必须存储以下的几种信息在方法区:

  • 完全限定名称的类型;
  • 类型是superClass的完全限定名称(除非是一个接口或类类型. lang。对象,都有一个超类);
  • 类型是否是一个类或者接口;
  • 类型修饰符(公共的一些子集,abstract,final);
  • 有序列表的任何super interface的完全限定名称。
    在java class文件和java虚拟机中,类型的命名总是存储为完整的名称。在java源代码中,一个完全限定名称是一个名字是类型的包,加上一个点,加上简单类型的名字。例如,java类在包的Object对象 完整名称是java.lang.Object.在class文件中,点斜杠所取代,如java / lang /对象。在方法区,完全限定的名称可以在任何形式和数据结构设计师选择。
    除了上述列出的基本类型信息,虚拟机也为每个加载也存储类型:
  • 类型的常量池
  • 字段信息
  • 方法信息
  • 所有类(静态)中声明的变量类型
  • 引用类的类加载器
  • 引用类类

方法信息

每个方法中声明的类型,以下信息必须存储在方法区。
  • 字段名称
  • 字段类型
  • 字段是修饰符(public, private, protected, static, final, synchronized, native, abstract)

类变量

类变量可以共享访问一个类的所有实例,即使没有任何实例。这些变量与类相关联,而不是类的实例,所以它们在逻辑上是在方法区上的类中的数据。在java虚拟机使用一个类前,它必须从方法区中分配内存给每个不是final类型的变量。常量(声明为final类型的类变量)不以同样的方式对待
non-final类型的类变量。每个使用final类变量的类型都有一个常量的副本在自己的常量池里。作为常量值的一部分,final类变量存储在方法区-就像non-final类变量。
类变量共享可以访问一个类的所有实例,即使没有任何实例。这些变量与类相关联,而不是类的实例,所以他们在逻辑上是类中的数据方法的一部分地区。在Java虚拟机使用了一个类之前,它必须从方法分配内存区域中声明的每个不是final类变量。但是声明的non-final类变量作为数据类型的一部分存储,使用final类变量作为任何数据类型存储。

引用类的类加载

类加载的每种类型,java虚拟机必须跟踪该类型是否通过原始类加载或者一个类加载对象。对于那些通过类加载加载对象的类型,虚拟机必须存储类加载器的引用对象加载类型。这种数据类型信息存储在方法区。
在动态链接中虚拟机使用这些信息。一种类型关联另一种类型时,虚拟机请求相同的类加载器来加载引用类型。这个动态链接的过程也是虚拟机形成不同的命名空间的方式。能正确执行动态链接和维护多个命名空间,在方法区中虚拟机需要知道类加载器加载的每种类型。

类引用

java虚拟机加载每个类型时,该类的实例也就被创建类。在方法区,虚拟机必须以某种方式关联类实例的引用类型与类型数据。

堆(Heap)

每当在运行的java程序中创建一个类实例或者数组, 存储会从堆中分配一个新的对象。只有一个堆在java虚拟机实例中,所有的线程共享这个堆。因为一个java应用程序运行在独有的java虚拟机实例中,在运行的程序中拥有单独的堆。两个不同的java应用程序不会相互影响堆的数据。然而,同一应用程序的两个不同的线程可以互相影响堆数据。这就是为什么你必须关心的同步多线程访问Java对象(堆数据)。

GC

垃圾回收主要功能是自动回收在运行的应用程序中不再被应用的对象内存。它也能移动对象在应用程序运行时减少堆碎片。GC不再是java虚拟机所需要的严格规范。规范只需要实现管理自己的堆在某种方式。例如,一个实现可以有固定数量的可用堆空间和空间填满时抛出OutOfMemory异常。Java虚拟机规范没有说一个实现都必须使用多少内存来运行程序。它没有说如何实现必须管理堆。它只对实现设计师说,该项目将会从堆中分配内存,但不会释放它。由设计师来找出他们想怎样处理这一事实。

定义对象

给定一个对象引用,虚拟机必须能够迅速定位对象的实例数据。此外,必须有一些方法来访问一个objectis类数据(存储在方法区)给定一个对象的引用。出于这个原因,为对象分配的内存通常包括一些指针到方法区。
将堆分为两个部分:一个处理池,一个对象池。一个对象引用是一个本机指针处理池条目。处理池条目有两个组件:一个指向实例数据对象池和一个指向类的数据方法。这种方案的优点是,它使得虚拟机更容易处理堆碎片。当虚拟机移动一个对象在对象池,它只需要更新一个指针的对象是新地址:有关指针处理池中。这种方法的缺点是,每访问一个对象实例数据需要解除两个指针。

 另一个设计使一个对象引用一个原生指针包包含的数据对象实例数据和一个指向对象的指针类数据。这种方法需要非关联化只有一个指针来访问一个对象实例数据,但使得移动对象更复杂。当虚拟机移动物体打击分裂这种堆,它必须更新每一个参考对象在运行时的数据区。

虚拟机中一个对象引用对象的类数据需要几个原因。当运行的程序试图把一个对象引用另一个类型,虚拟机检查是否被实际的类引用的对象,当程序执行一个instanceof操作它必须检查。在这两种情况下,虚拟机必须引用对象的类数据。当程序调用一个实例方法时,虚拟机必须动态绑定。它必须选择要调用的方法不是基于引用的类型而是该类的对象。要做到这一点,它必须再次访问给定的类数据只有一个对象的引用。

数组引用

在Java中,数组是成熟的对象。像对象、数组总是存储在堆中。像对象一样,实现设计师可以决定他们想要如何表示数组在堆上。数组与类相关联的类实例,就像任何其他对象。所有数组相同的大小和类型有相同的类。数组的长度(或多维数组的每个维度的长度)不扮演任何的角色建立数组类。例如,三个整数的数组具有相同的类作为三百整数数组。一个数组的长度被认为是实例数据的一部分。
多维数组表示为数组的数组。整数的二维数组,例如,将由一个一维数组的引用几个一维整数数组。

必须保持在堆上的数据是每个数组的数组长度,数组数据,引用数组类的一些数据。给定一个引用一个数组,虚拟机必须能够确定数组的长度,通过索引获取和设置它的元素(检查以确保没有超出数组边界),并调用任何方法声明的对象,直接superclass的数组。

当一个新线程启动时,java虚拟机为这个线程创建一个栈。一个java堆栈存储线程状态的离散帧。在栈上java虚拟机只执行两个操作:push,pop,就是推和弹出。
当前正在执行的线程的方法是当前线程的方法。当前的堆栈帧方法是当前帧。定义当前方法的类被称为当前类,当前类常量池是当前常量池。当它执行一个方法时,java虚拟机跟踪当前类和当前的常量池。当虚拟机指令操作数据存储在堆栈帧,它对当前帧执行这些操作。
当一个线程调用一个java方法时,虚拟机创建并推动一个新的堆栈帧到线程中。这个新的帧成为当前帧。当方法执行时,它使用这个帧去存储参数,本地(局部)变量,中间计算,和其他数据。
一个方法可以通过两种方式完成。 如果一个方法通过return返回完成,那就是正常完成。如果通过异常抛出完成,那就是突然完成。当一个方法完成后不管是正常走完还是异常走完,Java虚拟机会丢弃方法的堆栈帧。之前的方法变为当前帧。
所有的数据在一个线程的Java线程堆栈中是私有(private)的。没有办法让一个线程访问或修改另一个线程的Java堆栈。由于这个原因,你不需要担心同步多线程在你的Java程序访问局部变量。当一个线程调用一个方法,团队局部变量存储在一个帧调用线程的Java堆栈上。只有一个线程能够访问这些局部变量: 调用该方法的线程。
在内存中方法区和堆,Java堆栈和堆栈帧不需要是连续的。一个连续的帧可以分配在栈上,或者他们可以堆上分配,或两者的结合。实际的数据结构用来表示Java堆栈,堆栈帧是实现设计师的决定。实现可能会允许用户或程序员指定Java堆栈的初始大小,以及最大或最小大小。

栈帧

栈帧有三个部分:局部变量,操作数帧,帧数据。局部变量和操作数帧的大小,它们的大小取决于每个方法的需要。这些大小都是在编译的时候确定的,包括在每个方法的类文件数据。数据帧的大小是依赖于实现的。
当java虚拟机调用一个方法时,在局部变量和操作数堆栈它检查类数据确定的字数要求的方法。它创建一个适当大小的堆栈帧并推动它到Java堆栈的方法。

操作数栈

像局部变量一样,操作数堆栈被组织为一个数组。但与局部变量通过数组索引访问,访问操作数堆栈弹出和推出value。如果一个指令将一个值到操作数堆栈上,后面的指令可以推出和使用value。

帧数据

除了本地变量和操作数堆栈,Java堆栈帧包括支持常量池解决问题的数据,正常的方法返回,异常调度。这个数据存储在Java堆栈帧的帧数据部分。

posted @ 2018-08-28 10:59  Levcon  阅读(130)  评论(0编辑  收藏  举报