JavaGuide知识点整理——java内存地区详解

藏宝库编辑 2024-9-27 15:18:32 42 0 来自 中国
本篇如果没有特殊阐明,都是针对的是HotSpot假造机。
对于java步调员来说,在假造机自动内存管理机制下,不再须要像C/C++语言的步调员如许为每一个new利用去写对应的delete/free利用,不容易出现内存走漏和内存溢出标题。正式由于java步调员把内存控制权利交给java假造机,一旦出现内存走漏和溢出方面的标题,如果不相识假造机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
运行时数据地区

java假造机在实验java步调的过程中会把它管理的内存分别成多少个差别的数据地区。jdk1.8和之前的版本有较大的差别。下面会先容到。

1.png

线程私有的:

  • 步调计数器
  • 假造机栈
  • 本地方法栈
线程共享的:


  • 方法区
  • 直接内存
java假造机规范对于运行时数据地区的规定是相当宽松的。以堆为例:堆可以是一连空间,也可以不一连。堆的巨细可以固定,也可以在运行时按需扩展。假造机实现者可以使用任何垃圾接纳算法管理堆,以致完全不举行垃圾接纳也是可以的。
步调盘算器

步调计数器是一块比较小的内存空间,可以看做是当火线程所实验的字节码的行号指示器。字节码表明器工作时通过改变这个计数器的值来选取下一条须要实验的字节码指令。分支,循环,跳转,非常处置惩罚,线程规复等功能都须要以来这个计数器来完成。
别的为了线程切换后能规复到精确的实验位置。每条线程都须要有一个独立的步调计数器。各个线程之间计数器互不影响,独立存储,我们称这类内存地区为“线程私有”的内存。
从上面的先容中我们知道了步调计数器重要有两个作用:

  • 字节码表明器通过改变步调计数器来依次读取指令,从而实今世码的流程控制,如:次序实验,选择,循环,非常处置惩罚。
  • 在多线程的情况下,步调计数器用于记载当火线程的实验的位置。从而当线程被切换返来的时候可以大概知道该线程前次运行到哪儿了。
注意:步调计数器是唯逐一个不会出现OOM的内存地区,它的生命周期随着线程的创建而创建,随着线程的竣事而死亡。
java假造机栈

与步调计数器一样,java假造机也是线程私有的,它的生命周期和线程雷同。随着线程的创建而创建,随着线程的死亡而死亡。
栈绝对算得上是JVM运行时数据地区的一个焦点。除了一些native方法调用是通过本地方法栈实现的,其他全部的java方法调用都是通过栈来实现的。
方法调用的数据须要通过栈举行转达,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用竣过后,都会有一个栈帧被弹出。
栈由一个个栈帧构成,而每个栈帧中都拥有:局部变量表,利用数栈,动态链接,方法返回地点。和数据结构上的栈雷同,两者都是先辈后出的数据结构,只支持出栈和入栈两种利用。

3.png 局部变量表:重要存放了编译期可知的各种数据范例,对象引用(reference范例,它差别于对象自己,可能是一个指向对象起始地点的引用指针,也可能是指向一个代表对象的句柄大概其他与此对象干系的位置。)
利用数栈:重要作为方法调用的中转站使用,用于存放方法实验过程中产生的中央盘算结果。别的,盘算过程中产生的临时变量也会放在利用数栈中。
动态链接:重要服务一个方法须要调用别的方法的场景。在java源文件被编译成字节码文件时,全部的变量和方法引用都作为符号引用生存在class文件的常量池里。当一个方法要调用别的方法,须要将常量池中指向方法的符号引用转化为其在内存地点中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

4.png
栈空间固然不是无穷的,但是一样平常正常调用的情况下是不会出现标题的。不外,如果函数调用陷入无穷循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程哀求栈的深度凌驾当前java假造机栈的最大深度的时候,就抛出StackOverFlowError错误。
java方法有两种返回方式,一种是return语句正常返回。一种是抛出非常、不管哪种 返回方式,都会导致栈帧被弹出。也就是说栈帧随着方法调用而创建,随着方法竣事而烧毁。无论方法是正常完成还是非常完成都算做方法竣事。
除了StackOverFlowError错误之外,栈还可能会出现OOM错误,这是由于如果栈的内存巨细可以动态扩展,如果假造机在动态扩展栈时无法申请到充足的内存空间,则抛出OOM非常。
简朴总结下步调运行时栈可能会出现的两个错误:

  • StackOverFlowError:若栈的内存巨细不答应动态扩展,那么当线程哀求栈的深度凌驾当前java假造机栈的最大深度时,就会抛出StackOverFlowError错误。
  • OutOfMemoryError:如果栈的内存巨细可以动态扩展,如果假造机在动态扩展栈的时候无法申请到充足的内存空间,则抛出OOM非常。
本地方法栈

和假造机栈所发挥的作用非常相似,区别是:假造机栈为假造机实验java方法(也就是字节码)服务,而本地方法栈则为假造机使用到的native方法服务。在HotSpot假造机中和java假造机栈合二为一。
本地方法被实验的时候,在本地方法栈也会创建一个栈帧,用于存放本地方法的局部变量表,利用数栈,动态链接,出口信息。
方法实验完毕后相应的栈帧也会出栈并开释内存空间,也会出现StackOverFlowError和OutOfMemoryError两种错误。


java假造机所管理的内存中最大的一块。java堆是全部线程共享的一块内存地区,在假造机启动时创建。此内存地区的唯一目的就是存放对象实例,险些全部的对象实例以及数组都在这里分配内存。
java天下中“险些”全部的对象都在堆中分配,但是随着jit编译器的发展与逃逸分析技能渐渐成熟,栈上分配,标量更换优化技能将会导致一些玄妙的厘革。全部的对象都分配到堆上渐渐变得不那么绝对了。从jdk1.7开始已经默认开启逃逸分析。如果某些方法中的对象引用没有被返回大概未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
java对是垃圾网络器管理的重要地区。因此也被称作GC堆。从垃圾接纳的角度。由于现在网络器根本都接纳分代垃圾网络算法,以是java堆还可以细分为:新生代和老年代。再细致一点有:Eden,Survivor,Old等空间。进一步分别的目的是为了更好的接纳内存。大概更快的分配内存。
在JDK7版本以及7版本之前,堆内存通常被分为下面三部门:

  • 新生代内存
  • 老年代
  • 永世代
上面说的Eden和两个Survivor都属于新生代。如下图所示:

5.png
JDK8版本之后永世代被元空间代替,元空间使用的是直接内存。如下图所示:
6.png
大部门情况对象都会起首在Eden地区分配。再一次新生代垃圾接纳后如果对象还存活,则会进入S0大概S1.而且对象的年龄+1.当它的年龄增长到肯定水平(默认15)就会被提升到老年代中。对象提升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
ps:这里有一个须要注意的点:Hotspot有个机制:遍历全部对象时,按照年龄从小到大对其所占用的巨细举行累积,当累积的某个年龄巨细凌驾survivor区的一半的时候,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的提升年龄阈值。
动态盘算年龄的代码如下:
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {    //survivor_capacity是survivor空间的巨细size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);size_t total = 0;uint age = 1;while (age < table_size) {total += sizes[age];//sizes数组是每个年龄段对象巨细if (total > desired_survivor_size) break;age++;}uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;    ...}堆里很容易出现OOM错误,而且还会有几种体现情势,好比:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded: 当JVM耗费太多的时间实验垃圾接纳而且只能接纳很少的堆空间,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space:如果在创建新的对象时,堆内存中的空间不敷以存放新创建的对象就会引发此错误。(和设置的最大堆内存有关,且受制于物理内存的巨细)
方法区

方法区属于是JVM运行时数据地区的一块逻辑地区。是各个线程共享的内存地区。
《java假造机规范》只规定了有方法区这么个概念和它的作用,方法区到底怎样实现就是假造机自己要思量的事了,也就是说不通的假造机,方法区的实现是差别的。
当假造机密使用一个类时,他须要读取并剖析class文件获取干系信息。再将信息存入到方法区。方法区会存储已经被假造机加载的类信息,字段信息,方法信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
方法区和永世代以及元空间是什么关系呢?方法区和永世代已经元空间的关系很像java中接口和类的关系,类实现了接口,这里的类就可以看做是永世代和元空间。接口可以看做是方法区。也就是说永世代以及元空间是HotSpot假造机对假造机规范中方法区的两种实现方式。而且永世代是1.8之前的方法区的实现。元空间是1.8之后的方法区的实现。


为什么要将永世代换成元空间呢?

  • 整个永世代有一个JVM自己设置的固定巨细上限。无法举行调解,而元空间使用的是直接内存,受本机可用内存的限定,固然元空间仍旧可能溢出,但是相比原来出现的几率会更小。
元空间溢出会报错:java.lang.OutOfMemoryError:MetaSpace
可以使用-XX: MaxMetaspaceSize标志设置最大元空间巨细,默认值为unlimited。这意味着它只受体系内存的限定。-XX: MaxMetaspaceSize调解标志界说元空间的初始巨细如果未指定此标志,则Metaspace将根据运行时的应用步调需求动态的重新调解巨细。

  • 元空间里存放的是类的元数据,如许加载多少类的元数据就不由MaxPermSize控制了,而是由体系的现实可用空间来控制,如许就能加载更多的类了。
  • 在JDK8,合并HotSpot和JRockit的代码时,JRockit从没有一个叫永世代的东西,合并之后也没须要额外设置这么一个永世代的地方了。
方法区常用参数有哪些?
JDK1.8之前永世代还没有被彻底移除的时候通常通过下面这些参数来调治方法区的巨细。
-XXermSize = N //方法区(永世代)初始巨细
-XX:MaxPermSize = N //方法区(永世代)最大巨细,凌驾这个值会抛出OOMermGen
相对而言,垃圾网络活动在这个地区是比较少出现的,但是并非数据进入方法区后就永世存在了。
JDK1.8的时候方法区(HopSpot的永世代)被彻底移除了(JDK1.7开始),取而代之的是元空间,元空间使用的是直接内存,下面是一些常用参数:
-XX:MetaspaceSize = N //设置Metaspace的初始(和最小巨细)
-XX:MaxMetaspaceSize =N//设置Metaspace的最大巨细
与永世代很大的差别就是。如果不指定巨细的话,随着更多的类的创建,假造机遇耗尽全部可用的体系内存。
运行时常量池

Class文件中除了有类的版本,字段,方法,接口等形貌信息外。还有用于存放编译器天生的各种字面量和符号引用的常量池表。
字面量是源代码中的固定值的体现法,即通过字面我们就能知道其值得寄义。字面量包罗整数,浮点数和字符串字面量,符号引用包罗类符号引用,字段符号引用,方法符号引用和接口方法符号引用。
常量池表会在类加载后存放到方法区的运行时常量池中。
运行时常量池的功能雷同于传统编程语言的符号表,只管它包罗了比典范符号表更广泛的数据。
既然运行时常量池是方法区的一部门,自然受到方法区内存的限定,当常量池无法再申请到内存时会抛出OOM错误。
字符串常量池

字符串常量池是JVM为了提升性能和淘汰内存斲丧针对字符串专门开发的一块地区。重要目的是为了克制字符串的重复创建。
// 在堆中创建字符串对象”ab“// 将字符串对象”ab“的引用生存在字符串常量池中String aa = "ab";// 直接返回字符串常量池中字符串对象”ab“的引用String bb = "ab";System.out.println(aa==bb);// trueHoSpot假造机中字符串常量池的实现是src/hotspot/share/classfile/stringTable.cpp。StringTable本质上就是一个HashSet<String>,容量为StringTableSize(可以通过-XX:StringTableSize参数来设置)。
StringTable中生存的是字符串对象的引用。字符串对象的引用指向堆中的字符串对象。
JDK1.7之前,字符串常量池存放在永世代,JDK1.7字符串常量池和静态变量从永世代移动到了java堆中。

8.png
9.png
JDK1.7为什么要将字符串常量池移动到堆中?
重要是由于永世代(方法区的实现)的GC接纳效率太低只有在整堆网络(Full GC)的时候才会被实验GC,java步调中通常会有大量的被创建的字符串等待接纳。将字符串常量池放到堆中,可以大概更高效实时的接纳字符串内存。
运行时常量池,方法区,字符串常量池这些都是不随假造机实现而改变的逻辑概念。是公共且抽象的,Metaspace,Heap是与详细某种假造机实现干系的物理概念。是私有且详细的。
直接内存

直接内存并不是假造机运行时数据区的一部门,也不是假造机规范中界说的内存地区。但是这部门内存也被频仍的使用,而且也可能导致OOM错误出现。
JDK1.4中加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓存区的I/O方式,它可以直接使用Native函数库直接分配堆外内存,然后通过存储一个在java堆中的DirectByteBuffer对象作为这块内存的引用举行利用。如许就能在一些场景中显著进步性能。由于克制了在java堆和Native堆之间来复兴制数据。
本机直接内存的分配不会受到java堆的限定。但是既然是内存就会收到本级总内存巨细以及处置惩罚器寻址空间的限定。
HotSpot假造机对象探秘

通过上面的先容我们大概知道了假造机的内存情况,下面我们来详细的相识一下HotSpot假造机在java堆中对象分配,结构和访问的全过程。
java对象的创建过程:
1. 类加载查抄

假造机碰到一条new指令时,起首将去查抄这个指令的参数是否在常量池中定位到这个类的符号引用。并查抄这个符号引用代表的类是否已被加载过,剖析和初始化过。如果没有, 那必须先实验相应的类加载过程。
2. 分配内存

类加载查抄通过后,接下来假造机将为新生对象分配内存。对象所需的内存巨细在类加载完成后便可确定。为对象分配空间的任务等同于把一块确定巨细的内存从java堆中分别出来。分配方式指针碰撞空闲列表两种,选择哪种分配方式由java堆是否规整决定。而java堆是否规整又由所接纳的垃圾网络器是否带有压缩整理功能决定的。
内存分配的两种方式:

  • 指针碰撞:

    • 适用场景:堆内存规整(没有内存碎片)的情况下。
    • 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中央有一个分界指针,只须要向着没用过的内存方向将该指针移动对象内存巨细位置即可。
    • 使用该分配方式的GC网络器:Serial,ParNew

  • 空闲列表:

    • 适用场合:堆内存不规整的情况下。
    • 原理:假造机遇维护一个列表,该列表中会记载哪些内存块是可用的。在分配的时候,找一块充足大的内存块来分别给对象实例,最后更新列表记载。
    • 使用该分配方式的GC网络器:CMS

选择以上两种方式中的哪一种,取决于java堆内存是否规整。而java堆内存是否规整取决于GC网络器的算法是标志-扫除还是标志-整理。值得注意的是,复制算法内存也是规整的。
内存分配并发标题
在创建对象的时候,有一个很紧张的标题,就是线程安全。由于在现实开发过程中,创建对象是很频仍的原形。作为假造机来说,必须要包管线程是安全的。通常来讲,假造机接纳两种方式来包管线程安全:

  • CAS+失败重试:CAS是乐观锁的一种实现。所谓乐观锁就是每次不加锁而是假设没有辩论而去完成某项利用,如果由于辩论失败就重试,直到乐成为止。假造机接纳CAS配上失败重试的方式包管更新利用的原子性。
  • TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,起首在TLAB分配,当对象大于TLAB中的剩余内存大概TLAB的内存已经用尽时,再接纳上述的CAS举行内存分配。
3. 初始化零值

内存分配完成后,假造机须要将分配到的内存空间都初始化成零值(不包罗对象头)。这一步利用包管了对象的实例字段在java代码中可以不赋初始值就直接使用。步调能访问到这些字段的数据范例所对应的零值。
4. 设置对象头

初始化零值完成之后,假造机密对对象举行须要的设置,比方这个对象是哪个类的实例,怎样才气找到类的元数据信息,对象的哈希码,对象的GC分代年龄信息。这些信息存放在对象头中。别的,根据假造机当前运行状态的差别,如是否启用方向锁等,对象头会有差别的设置方式。
5. 实验init方法

在上面的工作全部完成之后,从假造机的视角来看,一个新的对象已经产生了。但是从java步调的视角来看,对象创建才刚刚开始。init方法还没有实验,全部的字段都还是零,以是一样平常来说实验new指令之后会接着实验init方法,把对象按照步调员的意愿举行初始化,如许一个振中可用的对象才算完成产生出来。
对象的内存结构

在HotSpot假造机中,对象在内存中的结构分为三块地区:对象头,实例数据和对齐添补。
HotSpot假造机的对象头包罗两部门信息:第一部门用于存储对象自身的运行时数据(哈希码,GC分代年龄,锁状态标志等),另一部门是范例指针、即对象指向它的类元数据的指针,假造机通过这个指针来确定这个对象是哪个类的实例。
实例数据部门是对象真正存储的有用信息。也是在步调中所界说的各种范例的字段内容。
对齐添补部门不是肯定存在的。也没有什么特殊的寄义,仅仅起占位作用。由于HotSpot假造机的自动内存管理体系要求对象起始地点必须是8字节的整数倍,换语言说对象的巨细必须是8字节的整数倍。而对象头部门恰好是8字节的倍数(1倍大概2倍),因此当对象实例数据部门没有对齐时,就须要通过对齐添补来补全。
对象的访问定位

创建对象就是为了使用对象。我们的java步调通过栈上的reference数据来利用堆上的详细对象。对象的访问方式由假造机实现而定。现在主流的访问方式有:使用句柄,直接指针。
句柄

如果使用句柄的话,那么java堆中会分别出一块内存来作为句柄池,reference中存储的就是对象的句柄地点,而句柄中包罗了对象实例数据与范例数据各自的详细地点信息。

直接指针

如果使用直接指针访问,那么java堆对象的结构中就必须思量怎样放置访问范例数据的干系信息,而reference中存储的直接就是对象的地点。

12.png 这两种对象访问方式各有上风,使用句柄来访问的最大利益是reference中存储的是稳固的句柄地点,在对象被移动时只会改变句柄中的实例指针,而reference自己不须要修改。使用直接指针访问方式最大的利益就是速率快,它节省了一次指针定位的时间开销。
本篇条记就记到这里,如果稍微帮到你了记得点个喜好点个关注。文章中都是很浅近和直接的东西,恰当当八股文背。想要深入相识可以自己去查阅一些资料。也祝各人工作顺顺遂利,天天进步哟~!
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 18:29, Processed in 0.166602 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表