JVM 运行时数据区——方法区、堆、栈

欢迎访问我的个人博客休息的风

JVM运行时数据区分为堆,栈,方法区(元空间),我将从一个class文件加载到内存,再分配对象,再使用这个对象这样一个过程进行分析。总体情况如下图所示:

JVM 运行时数据区——方法区、堆、栈

  • 方法区

方法区是各个线程共享的内存区域,用于存储被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码,运行时常量池。在HotSpot上也被称为“永久代”。

这一区域主要的知识点是类加载的过程,这一过程分为五个阶段,分别是加载,验证,准备,解析,初始化

  • 加载

虚拟机需要完成以下三件事:

1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态数据结构转换为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,做为方法区这个类的各种数据结构的访问入口。
简单来说,就是先通过全类名把文件加载到二进制字节流,然后转换为方法区的数据结构,再生成class对象,做为访问入口。
类加载器
对于1中的这一操作,需要用到类加载器模块。这个类加载器在用于判断两个类是否相等时用到。也就是只有再同一个类加载器加载的前提下,再去比较是否相等才有意义。这里的相等包括:equals、isAssignableForm、instanceof等
双亲委派模型
java虚拟机通过双亲委派模型来实现类加载器。所谓的双亲委派模型如下图:

JVM 运行时数据区——方法区、堆、栈

从上到下分别为:

启动类加载器,加载<JAVA_HOME>/lib目录下(或通过-Xbootclasspath参数指定的目录下),能被java虚拟机识别的
文件名(如:rt.jar)类库加载虚拟机内存中。开发者不能直接使用
扩展类加载器,加载<JAVA_HOME>/lib/ext目录下(或java.ext.dirs系统变量所指定的目录下),所有的类库,参被开发者使用
应用程序类加载器,加载类路径下(ClassPath)的所有类库,开发者可以直接使用
自定义类加载器,自己定义的类加载器。
双新委派模型的加载查找过程也是自顶向下的,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这些父加载器的组织方法一般不通过继承,而是通过组合来复用。

  • 验证
这一阶段顾名思义,就是验证文件信息,避免java虚拟机遭受恶意攻击。有文件格式验证、元数据验证、字节码验证、符号引用验证,这一系列验证,目的是为了确保Class文件的字节流中包含的信息 符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
  • 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化

初始化阶段是执行类构造器<clinit>()方法的过程
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他 线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。(单例模式静态内部类的实现方式就是根据这一原理)。


这里需要注意,只有以下五种情况下才会触发初始化操作:

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初 始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字 实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常 量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化, 则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父 类的初始化。

  • 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个 类),虚拟机会先初始化这个主类。 
5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后 的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄 所对应的类没有进行过初始化,则需要先触发其初始化。

  • 堆区
JVM规定,所有的对象实例和数组都要在堆上分配;堆的主要作用就是用来分配给对象生存的空间。
先来看看java对象是如何分配的。

  • TLAB分配
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。
默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合
被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。
因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。也就是说,Java中每个线程都会有自己
的缓冲区称TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,
TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配
的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
java对象分配过程:
1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2.
2. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,
如果现有的TLAB不足以存放当前对象则3.
3. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
4. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,
增加eden_top 的值,如果Eden区不足以存放,则5.
5. 执行一次Young GC(minor collection)。
6. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。


有分配就要加收,java堆的回收,主要解决三个问题:什么时候可以回收?什么时候回收?怎么回收?

  • 可达性分析(什么时候可以回收?)
JVM 运行时数据区——方法区、堆、栈
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所
走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

  • 安全点(什么时候回收?)
安全点的选定基 本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的;“长时间 执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有 这些功能的指令才会产生Safepoint
  • 安全区域(什么时候回收?)
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方 开始GC都是安全的。
  • 垃圾回收算法(怎么回收?)
  具体可以查看我之前写的博客垃圾回收算法
  • 垃圾回收器(怎么回收?)
JVM 运行时数据区——方法区、堆、栈

JVM 运行时数据区——方法区、堆、栈
JVM 运行时数据区——方法区、堆、栈


JVM 运行时数据区——方法区、堆、栈
JVM 运行时数据区——方法区、堆、栈
JVM 运行时数据区——方法区、堆、栈

  • 虚拟机栈
虚拟机栈是描述java方法执行的内在模型。
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素
栈帧包括:局部变量表,操作数栈,动态链接(每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用),方法出口方法区等
局部变量表:八种基本数据类型,dubbo、long占两个局部变量空间(slot),reference对象引用,returnAddress;所需空间编译期就确定,运行期不再改变
JVM 运行时数据区——方法区、堆、栈

  • 静态分派(重载)
虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本
  • 动态分派(重写)
invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校
验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。