Java内存区域

一、概述

JDK1.8之前如图所示:

 


pic-1617982423626.png

 

JDK1.8(含)之后如图所示:

 


pic-1617982423627.png

 

区分就是1.8的元数据区替代方法区了

二、具体介绍

接下来对每个内存区域进行介绍

2.1 程序计数器

该块是一块较小的内存空间,因为JVM可以并发执行线程,因此会存在线程之间切换的情况,这个计数器就会记录下当前程序执行到的位置,以便回到该线程继续执行时可以恢复线程继续执行。
JVM会为每个线程都分配一个程序计数器,它的生命周期跟线程的相同。
如果线程执行的是JAVA方法,计数器记录的就是虚拟机字节码指令的地址,如果执行的是Native方法,计数器的值则为undefined
程序计数器是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

2.2 本地方法栈

本地方法栈是调用本地native方法,可以认为是通过JNI直接调用C/C++库,不受JVM的控制
本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常

2.3 虚拟机栈

虚拟机栈是用于描述Java方法执行的内存模型
每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
虚拟机栈也是每个线程独有的,随着线程的创建而存在,线程结束而死亡。
在虚拟机栈内存不够时会OutOfMemoryError,在线程运行中需要更大的虚拟机栈时会出现StackOverFlowError。

接下来对栈帧中存储的信息进行解析:

2.3.1 局部变量表

局部变量表是存在方法参数和局部变量的区域。
全局变量的放在堆空间的,因为全局变量是可以被多个方法共用的,而虚拟机栈本身就是方法执行时创建的

2.3.2 操作数栈

这是一个先入后出的栈。
当方法刚刚执行时,这个操作数栈就是空的,在执行过程中,会有各种字节码指令往操作栈写入和提取内容,也就是出栈/入栈操作。

2.3.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。 持有这个引用的目的就是为了当前方法能够实现动态链接(其实就是实现方法间调用,能在B方法中通过动态连接调用方法A)
在java的源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,这样一个方法调用另一个方法时,就能通过常量池中指向方法的符号引用来表示,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

    public void methodA(){

    }
    public void methodB(){
        methodA();//methodB()调用methodA(),先找到调用methodA()的版本符号,再变为直接引用
    }

如上代码所示,两个方法就会有两个栈帧,每个栈帧中都在动态连接中存储了自己方法对应的引用,方法B调用方法A时,先找到方法A的引用符号,再改为直接引用,这样方法A被执行,就会初始化一个栈帧,当方法结束,也就是栈帧在虚拟机栈出栈后,如果有返回值就会压入调用者(方法B)栈帧的的操作数栈中

2.3.4 方法出口

分为正常返回和异常退出
无论何种退出情况,都将返回至方法被调用的位置,程序才能继续执行
一般方法正常退出时,调用者PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
方法退出的过程相当于在虚拟机栈中弹出当前栈帧

2.4 Java堆

Java堆是被所有线程共享的一个区域,在虚拟机启动时就创建好了,此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆是垃圾回收管理的主要区域,又称为“GC堆”,可以说是Java虚拟机管理的内存中最大的一块
现在的虚拟机都采用分代回收算法,把堆分为了新生代+老年代+永久代(1.8后删除),新生代又分为Eden + From Survivor + To Survivor区

简单介绍:

  • 当我们new一个对象后,会先放到Eden区划分出来一块作为存储空间的空间,因为堆是线程共享的,可能会出现两个对象共用一个内存,所以JVM会为每个线程预申请连续的内存空间,当这个空间不足再申请多块内存空间,这个操作叫为TLAB.

  • 这是当Eden区满了之后,会触发一个叫做Minor GC(发生在年轻代的GC)操作,还存活的对象就会移动到S0区,S0满后会再次触发Minor GC,然后将存活的对象移动到S1区,此时S0和S1的指针会交换,保证一段时间内有一个Survivor区为空,经过多次Minor GC还存活的对象(正常是15次,可通过指定参数修改)就会被移动到老年代。
    解释
    针对新生代的垃圾回收,当eden区满了,会将存活对象复制到from区,此时eden区就空了,当eden区再次满时,会将eden区和from区的对象复制到to区,此时eden区和from区就空了,当eden区再次满时,就会将eden区和to区存活的复制到from区

  • 老年代是储存长期存活的对象,占满时会触发Full GC.期间会停止所有线程等待GC的完成,所以项目应该尽量区减少发生Full GC以避免响应超时的问题。

  • 当老年区执行Full GC后,对象还是无法保存时,就会产生OOM,此时就是堆内存的不足。可以通过-Xms、-Xmx来调整,也可能是代码创建的对象很大并且很多导致垃圾无法收集它们。

 


pic-1617982423630.png

 

2.5 方法区/元空间

1.8之后把方法去改为元空间了,类的元信息(包括版本、field、方法、接口等信息)被存储在元空间中,元空间没有使用堆内存,而是与堆不相连的本地内存区域,所以理论上系统内存多大,元空间可以多大

方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。方法区比较重要的一部分是运行时常量池(Runtime Constant Pool),为什么叫运行时常量池呢?是因为运行期间可能会把新的常量放入池中,比如说常见的String的intern()方法。

String a = "I am HaC";
Integer b = 100;

在编译阶段就把所有的字符串文字放到一个常量池中,复用同一个(比如说上述的“I am HaC”),节省空间。

2.6 垃圾回收算法

常见的有标记-清除算法、复制算法、标记-整理算法、分代收集算法

2.6.1 标记-清除算法

首先标记出所有需要回收的对象,结束后统一回收
缺点是效率比较低下,会让内存产生大量的碎片,导致我们如果需要较大内存时,可能会无法分配

2.6.2 复制算法

它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了,注意此时的区域划分不是1:1

2.6.3 标记整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

分代回收算法

这种算法主要是根据Java堆的划分情况,针对每个区域的特点采用适当的收集算法,在新生代中,每次垃圾回收都会发现有大批对象死去,只有少量存活,所以采用复制算法,只需要复出少量存活对象的复制成本就可以完成收集,而老年代的对象存活率高、所以使用标记-清理或者是标记-整理算法来进行回收。

posted @ 2021-04-10 11:47  老油条1号  阅读(87)  评论(0)    收藏  举报