JVM内存结构/Java 内存区域

1.JVM内存整体的结构?线程私有还是共享的?
  Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。除此之外,还有由堆中引用的JVM外的直接内存。

  线程私有:虚拟机栈、本地方法栈、程序计数器
  线程共享:堆、方法区、堆外内存
 
2.程序计数器(线程私有)Program counter register
  PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
  (寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。
  在中央处理器的控制部件中,有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,寄存器有累加器(ACC))
使用PC寄存器存储字节码指令地址有什么用?
  因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
PC寄存器为什么会被设定为线程私有的?
  多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。
 

“==”比较的是内存中存放的位置

str1 == str2  -->true

str1 == str3 -->false

str2 == str3  --> false

 

“equals()”比较是字符序列

str1.equals(str2) -->true

str1.equals(str3) -->true

str2.equals(str3) -->true

 

str1.hasCode()str2.hasCode()str3.hasCode()三者的hasCode值是相同的

只有内容是相同的,hasCode()值就相同,就是同一个对象

 
3.虚拟机栈(线程私有) VM stack
  虚拟机栈,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
  虚拟机栈的作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

  栈:线程运行时需要的内存空间,一个栈存在多个栈帧。栈先入后出,后入先出。
  栈帧:每个方法运行时需要的内存(局部变量表、操作数栈、动态链接和方法返回值等信息),每次调用一个方法,便会将栈帧压入栈中,方法执行完毕将栈帧从栈顶压出
  活动栈帧:指在栈顶的栈帧,既正在调用的方法,每个线程只能有一个活动栈帧,对应着该线程正在调用的那个方法
  虚拟机栈的特点:
    栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
    JVM直接对虚拟机栈的操作只有两个:每个方法执行入栈,方法执行结束出栈
    栈不存在垃圾回收问题
    可通过-Xss来设置线程最大栈空间,栈的大小决定了函数调用的最大可达深度
 
垃圾回收是否设计栈内存?
  否,栈内存即每次方法执行的栈桢内存,每个栈桢内存在方法调用结束后都会被释放
栈内存分配越大越好吗?
  在运行java代码的时候,可以设置栈内存的大小,默认大小为1024kb
  栈内存越大,只是能进行多次的方法递归调用,反而会让线程数变少。
方法内的局部变量是否线程安全?
  看一个变量是否线程安全就要看它是否是对一个线程共享的还是独占的。
  如果是一个基本变量是线程安全的,一个线程对应一个栈,不同的线程在调用同一个方法的时候,会有不同的栈。线程内每一次方法调用都会产生一个新的栈桢。但如果是staic的变量,是针对多个的线程共享的,则会涉及到线程安全问题
  如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。反之,如果它作为行参传入或return了这个变量,则它可能会被其他线程操作,为线程所共享。
 
该区域有哪些异常:
  栈溢出StackOverflowError:在栈内存不能动态扩展的情况下,栈帧过多,栈帧过大
  内存溢出OutOfMemoryError:栈可以动态扩展的情况下,并且尝试扩展无法申请到足够的内存时,或者在创建新线程时没有足够的内存去创建对应的虚拟机栈时。
 
栈帧的内部结构jvm-stack-frame
  局部变量表 Local Variables
  操作数栈 Operand Stack
  动态链接Dynamic Linking:指向运行时常量池的方法引用
  方法返回地址 Return Address:方法正常退出或异常退出的地址
  一些附加信息
 
Java虚拟机栈如何进行方法计算:例如计算100+98
 

 

4.本地方法栈(线程私有)Native Method Stack
  ·本地方法接口:一个Native Method就是一个Java调用非Java代码(C/C++之类)的接口。
  ·本地方法栈:本地方法栈用于管理本地方法的调用(虚拟机栈用于管理Java方法的调用)
 
5.方法区(线程共享) Method Area
  方法区是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,不同的厂商有不同的实现。
·方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
·方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样可以是不连续的。
·方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展。
·方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者java.lan.OutOfMemoryError: Metaspace
 
  永久代PermGenHotspot虚拟机特有的概念,java8时又被元空间取代。永久代和元空间都可以理解为方法区的落地实现。
JDK1.8之前调节方法区大小
  -XX:PermSize=N //方法区(永久代)初始大小
  -XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError
JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置:
  -XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
  -XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
 
6.栈、堆、方法区的交互关系:
 

  栈内存:基本类型的变量(int a=3  a),对象的引用变量(Thread t=new Thread()  t)

  堆内存:存放由new创建的对象和数组,由Java虚拟机垃圾回收器来管理。
在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量持有的内容等于数组或者对象在堆内存中的首地址。在栈中的这个特殊的变量,就成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于一个别名。
  方法区:也称静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,例如classstatic变量。
通俗来讲,堆是用来存放对象的,而栈是用来执行程序的。

为什么要有堆和栈,这样设计有什么好处?分成堆栈的好处:
  1、从软件设计角度分析,栈代表了处理逻辑,堆代表了数据,这样分开,使得处理逻辑更清晰。分而治之的思想,这种隔离、模块化的思想体现在软件中的很多地方。
  2、堆和栈的分离,使得堆的内容可以被多个栈共享(即多个线程访问同一个对象)。这种共享的收益很多,这种共享提供了一种有效的数据交互方式(共享内存),另一方面,堆中共享的常量和缓存可以被所有栈访问,节省了内存。
  3、栈因为运行是需要,比如保存系统运行的上下文,需要地址段的划分,由于栈只能向上增长,因此限制住栈存储内容的能力,而堆是根据需要可以动态增长的,因此栈和堆的拆分,使得堆动态增长成为可能,相应栈只需要记住堆中的一个地址即可。
  4、面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美
★只有栈没有堆会怎样:
  首先,没有堆没有了共享内存,少了一种线程间通信的方式。
  其次,如果没有堆,数据都存到栈里面,那么各个线程都需要有自己独立的内存存储,无疑会占用更多的内存。
  而且栈是只支持先进先出的,这样也不太适合存储共享数据。
 
7.永久代和元空间内存使用上的差异
  (1)jdk1.7开始,符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底此时永久代中还保存另一些与类的元数据无关的杂项。
  (2)jdk8后,HotSpot 原永久代中存储的类的元数据将存储在metaspace中,而类的静态变量和字符串常量将放在Java堆中,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。
  (3)永久代有一个JVM本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存(操作系统分配给进程的内存空间,与 Java 堆不同,它并不受 JVM 管理,因此在内存使用上更加灵活),受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError
  (4)符号引用没有存在元空间中,而是存在native heap中,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM
 
永久代和元空间的区别:
  (1)存储位置:永久代是在 Java 堆中的一个特殊区域,而元空间是在本地内存中的。
  (2)大小调整:永久代的大小是有限制的,并且必须在启动时指定,而元空间可以根据需要自动调整大小。
  (3)垃圾收集:永久代使用Java堆的垃圾收集器进行垃圾回收,而元空间使用本地内存的垃圾收集器。
  (4)存储内容:永久代主要存储类的信息(如类名、方法名、字段名等),而元空间存储的是类的元数据(如类的结构、方法表、字段表等)。
  (5)类信息的存储方式:永久代中的类信息是使用永久代专用的类加载器加载和卸载的,而元空间中的类信息是使用与应用程序类加载器相同的类加载器加载和卸载的。
 
8.堆区内存是怎么细分的
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):
  新生(年轻代):新对象和没达到一定年龄的对象都在新生代
  老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
  元空间JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存。

  Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的
  主流虚拟机都是可扩展的(通过 -Xmx最大分配内存  -Xms初始分配内存 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。
 
(1)年轻代 Young Generation
  年轻代是所有新对象创建的地方,当填充年轻代时,执行垃圾收集Minor GC。年轻一代被分为三个部分:伊甸园Eden Memory和两个幸存区Survivor Memory,默认比例是8:1:1
  大多数新创建的对象都位于Eden内存空间中。
  当Eden空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中。
  Minor GC检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的。
  经过多次GC循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代。

 

(2)老年代 Old Generation
  旧的一代内存包含经过多轮小型GC后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行。老年代垃圾收集称为主GC,Major GC,通过需要更长时间。
  大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝
 
9.JVM中对象在堆中的生命周期
(1)在JVM内存模型的堆中,堆被划分为新生代和老年代
  新生代被分为Eden区和Survivor区,Survivor区由From SurvivorTo Survivor组成
(2)当创建一个对象时,对象会被优先分配到新生代的 Eden
  此时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold
(3)当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC
  JVM 会把存活的对象转移到Survivor中,并且对象年龄 +1
  对象在Survivor中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
(4)如果分配的对象超过了-XX:PetenureSizeThreshold,对象会直接被分配到老年代
 
10.JVM中对象的分配过程
不仅需要考虑内存如何分配、在哪里分配等问题,还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。内存的分配和回收

(1)new 的对象先放在伊甸园区,此区有大小限制

(2)当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区

(3)然后将伊甸园中的剩余对象(被引用的对象)移动到幸存者 0

(4)如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1

(5)如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1

(6)什么时候才会去养老区呢? 默认是 15 次回收标记

(7)在养老区,相对悠闲。当养老区内存不足时,触发Major GC,进行养老区的内存清理

(8)若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常

 
11.TLAB  Thread Local Allocation Buffer 线程本地分配缓冲区
  TLAB指的是为每一个线程分配线程专有的堆内存操作空间。
  从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
  多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略。
  OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计。
 
为什么需要TLAB
  对象分配在堆上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要进行同步,而同步带来的效果就是对象分配效率变差,尽管JVM采用了CAS的形式处理分配失败的情况,但是对于存在竞争激烈的分配场合仍然会导致效率变差。那么能不能构造一种线程私有的堆空间,哪怕这块堆空间特别小,但是只要有,就可以每个线程在分配对象到堆空间时,先分配到自己所属的那一块堆空间中,避免同步带来的效率问题,从而提高分配效率。
 
  尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。
  在程序中,可以通过 -XX:UseTLAB 设置是否开启 TLAB 空间。
  默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。
  一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
posted @ 2023-05-04 19:37  壹索007  阅读(54)  评论(0编辑  收藏  举报