JAVA虚拟机精讲-第六章 内存分配和垃圾回收(一)

0x00 内存分配和垃圾回收

JVM自动管理内存怎么自动法?内存区结构,分配方式,垃圾回收算法和jvm的垃圾回收器,这节有详细说明。由于内容较多,先讲一下JVM内存,下一篇将讲GC算法和JVM的垃圾回收器。

0x01 内存结构

(java 7)

“Java虚拟机”的图片搜索结果

  1. 程序计数器:当前线程所执行的字节码的指示器,用于记录下一条要运行的指令
  2. Java虚拟栈:存放基本数据类型和对象的引用
  3. Native方法栈:和虚拟栈相似,只不过它服务于Native方法
  4. Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆
  5. 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。永久代。

0x02 Java堆(Heap)

这里有一幅很清晰的jvm堆和方法区结构图(堆和方法区都是线程共享的),在hotspot中方法区和堆是逻辑上的独立,其实是包含在java堆内:

jvm 堆

  • Java 堆内存的分配策略
  1. Young区:又分为 Eden 区,survivior 1 (from)和 survivior 2(to),Old 区
  2. 一般小型的对象都会在 Eden 区上分配
  3. 当内存不够时候发生 minor GC,将存活的对象拷贝到 survivor 1 区域,(此时 2 是空的),然后清空 Eden
  4. 大对象直接进入老年代.
  5. 长期存活的对象将直接进入老年代.

0x03 java栈

java栈主要用于存放栈帧,从第一幅图可以看到栈的结构,书上主要在第八章介绍栈帧。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
每一个栈帧都包括了局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

  1. 局部变量表: 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。
  2. 操作数栈(执行方法代码的重要途径):操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的max_stacks数据项中。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
  3. 动态连接:  每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
  4. 方法返回地址:  当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。  另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
  5. 附加信息
    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

Java内存堆和栈区别

  1. 栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
  2. 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问
  3. 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出lang.StackOverFlowError,如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError
  4. 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满,-Xss选项设置栈内存的大小。-Xms选项可以设置堆的开始时的大小

0x04 对象内存分配

这个书本上有个图很清晰,可以看一下。

大概流程:加载类,TLAB分配(本地线程分配缓冲区,在Eden空间内,属于快速分配策略),如果不成功,通过加锁的方式在Eden去直接分配内存,如果内存不足则Minor GC(大对象直接进入老年区),TLAB或者Eden区分配之后将 引用 入栈,更新PC寄存器。

0x05 逃逸分析和栈上分配

一般认为new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中new出来的对象并一定分别在所认为的堆上。这两个点分别是Java中的逃逸分析TLAB(Thread Local Allocation Buffer)

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。可以理解为变量是局部变量,并且没有被外部使用(全局变量引用或被方法返回等离开作用域后使用),这样变量就没有逃逸。

栈上分配

我们都知道Java中的对象都是在堆上分配的,而垃圾回收机制会回收堆中不再使用的对象,但是筛选可回收对象,回收对象还有整理内存都需要消耗时间。如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

总结:没有逃逸的对象直接在栈里分配,这样随着方法的执行完成,对象也销毁,加快分配的速度并且减轻GC压力。

 

 

 

发表回复

您的电子邮箱地址不会被公开。

粤ICP备17041560号-2