JVM运行时数据区(详解+面试) 您所在的位置:网站首页 Java方法的调用采用压栈 JVM运行时数据区(详解+面试)

JVM运行时数据区(详解+面试)

2023-08-20 10:19| 来源: 网络整理| 查看: 265

运行时数据区

运行时数据区 1.概述2.程序计数器(Program Counter Register)3.虚拟机栈(Java Virtual Machine Stack)4.本地方法栈(Native Method Stack)5.Java堆内存6.方法区7.常见面试题

1.概述

JVM的运行时数据区,不同虚拟机实现可能略微不同,但都会遵从Java虚拟机规范,Java 8虚拟机规范规定,Java虚拟机所管理的内存将会包括一下几个运行时数据区域:

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

java中最小的执行单位是线程,因为虚拟机的是多线程的,每个线程是抢夺cpu时间片,程序计数器就是存储这些指令去做什么,比如循环,跳转,异常处理等等需要依赖它。

每个线程都有属于自己的程序计数器,而且互不影响,独立存储。

Java虚拟机栈(Java Virtual Machine Stacks)

描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应这一个线帧在虚拟机栈中入栈到出栈的过程。

虚拟机栈

本地方法栈(Native Method Stack)

与虚拟机的作用是相似的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的,与虚拟机栈相同的是栈的深度是固定的,当线程申请的大于虚拟机栈的深度就会抛出StackOverFlowError异常,当然虚拟机栈也可以动态的扩展,如果扩展到无法申请到足够的内存就会抛出outofMemoryError异常。

Java堆(Java Heap)

是Java虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有对象都分配在堆上渐渐变得不那么“绝对”了。

方法区(Method Area)

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

内存区域是很重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异,我们现在以使用最为流行的HotSpot虚拟机为例讲解。

Java虚拟机定义了若干中程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的。这些与线程对应的区域会随着线程开始和结束而创建销毁。

如图:红色的为多个线程共享,灰色的为单个线程私有的,即

线程间共享:堆,对外内存。

每个线程:独立包括程序计数器,栈,本地方法栈。

运行时数据区

2.程序计数器(Program Counter Register)

概述

JVM中的程序计数寄存器中的Register命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。

这里,并非是广义上所值的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

作用

程序计数器用来存储下一条指令的地址,也即将要执行的指令代码。有执行引擎读取下一条指令。

程序计数器

它是一块很小的内存空间几乎可以忽略不计,也是运行速度最快的存储区域。在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。如果是在执行native方法,则是未指定值(undefined)。它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

如下图所示:程序计数器的作用位置

作用位置

面试题

1.使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿开始继续执行。 JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。

2.程序计数器为什么被设定为线程私有的

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢? 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都分配应该程序计数器,这样一来各个线程之间便可以独立计算,从而不互相干扰。 3.虚拟机栈(Java Virtual Machine Stack)

出现背景

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更多的指令集。

栈和堆区别

栈是运行时的单位,而堆是存储的单元。

即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。

堆解决的是数据存储的问题,即数据怎么放,放在哪儿。

Java虚拟机栈是什么?

Java虚拟机栈,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应这一次方法的调用。Java虚拟机栈是线程私有的。生命周期和线程一致。

作用

主管Java程序的运行,它保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

作用

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个:调用方法,进栈。执行结束后出栈。对于栈来说不存在垃圾回收问题。

出入栈

栈中出现的异常

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

栈中存储什么?

每个线程都有自己的栈,栈中的数据都以栈帧为单位存储。在这个线程上正在执行的每个方法都有各自对应一个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行原理

JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。

在一条活动的线程中,一个时间点上,只会有一个活动栈。即只有当前执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈,与当前栈帧对应的方法称为当前方法,定义这个方法的类称为当前类。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。

栈

不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管哪种方式,都会导致栈帧被弹出。

栈帧的内部结构

每个栈帧中存储着:

局部变量表(Local Variables)

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量则存的是只想对象的引用。

操作数栈(Operand Stack)(或表达式栈)

栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)

因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

当一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

一些附加信息

附加信息

两个栈帧之间的数据共享

在概念模型中,两个栈帧作为虚拟机栈的元素,是完全互相独立的,但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数赋值传递,重叠的过程如图所示。

共享

Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

面试题

什么情况下会出现栈溢出(StackOverflowError)?

栈溢出就是方法执行时创建的栈帧超出了栈的深度。那么最有可能的就是方法递归调用产生这种结果。

通过调整栈大小,就能保证不出现溢出吗?

不能。

分配的栈内存越大越好吗?

并不是的,只能延缓这种现象的出现,可能会影响其他内存空间。

垃圾回收机制是否会涉及到虚拟机栈?

不会。 4.本地方法栈(Native Method Stack) Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。内存溢出方面也是相同的。

如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError。

如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出OutOfMemoryError。

本地方法是用C语言写的。它的具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。 5.Java堆内存

概述

堆

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

Java堆区在JVM启动时的时候即被创建,其空间大小也确定了,是JVM管理的最大一块内存空间。

堆内存的大小是可以调节的。

例如:-Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)

一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。

所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区。

《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时配对在堆上。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

堆内存区域划分

Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)

新生区分为Eden(伊甸园)区和Survivor(幸存者)区

划分

为什么分区(代)?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

1

2

3

对象创建内存分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

new的新对象先放到伊甸园区,此区大小有限制。

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

然后将伊甸园区中的剩余对象移动到幸存者0区。

如果再次触发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区是空的。

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

什么时候去养老区呢?默认是15此,也可以设置参数

-XX:MaxTenuringThreshold=

在老年区,相对悠闲,当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。

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

Java.lang.OutOfMemoryError:Java heap space

例如:

public static void main(String[] args) { List list = new ArrayList(); while(true){ list.add(new Random().nextInt()); } }

分配

新生区与老年区配置比例

配置新生代与老年代在堆结构的占比(一般不会调)

默认-XX:NewRatio = 2,表示新生代占1,老年代占2,新生代占整个堆的1/3.

可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5.

当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优。

比例 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1,当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。

新生区的对象默认生命周期超过15,就会区养老区养老。

大对象直接进入老年代 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。

新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。

比例 6. 分代收集思想Minor GC、Major GC、Full GC

JVM在进行GC时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区。针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。

部分收集:不是完整收集整个Java堆的垃圾收集,其中又分:

新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.。老年区收集(Major GC / Old GC):只是老年区的垃圾收集.。混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

整堆收集出现的情况:

System.gc();时。

老年区空间不足。

方法区空间不足。

开发期间尽量避免整堆收集。

TLAB机制

为什么有TLAB(Thread Local Allocation Buffer)

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB?

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在之间的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

TLAB

堆空间的参数设置

官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+PrintFlagsInitial

查看所有参数的默认初始值

-XX:+PrintFlagsFinal

查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内存的 1/64)

-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

字符串常量池

字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。在jdk1.8后,将String常量池放到了堆中。

String table还存在一个hash表的特性,里面不存在相同的两个字符串,默认容量为1009。当字符串常量池中的存储比较多的字符串时,会导致hash冲突,从而每个节点形成长长的链表,导致性能下降。所以在使用字符串常量池时,一定要控制容量。

字符串常量池为什么要调整位置?

JDK7中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC 是老年代的空间不足、永久代不足是才会触发。这就导致String Table回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

6.方法区

概念

方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据、static final常量、static变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。”所以,方法区看做是一块独立与Java堆的内存空间。

方法区

方法区在JVM启动时被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误: java.lang.OutOfMemoryError:Metaspace。

关闭 JVM 就会释放这个区域的内存.

方法区,栈,堆的交互关系

三者关系

方法区大小设置

Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.

元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的两个参数。默认值依赖于平台,windows 下,-XXMetaspaceSize是21MB,-XX:MaxMetaspaceSize 的值是-1,级没有限制.这个-XX:MetaspaceSize初始值是21M也称为高水位线一旦触及就会触发Full GC.因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一个较高的值。

方法区内部结构

内部结构

方法区它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存,运行常量池等。

方法区的垃圾回收

有些人认为方法区是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。

回收废弃常量与回收Java堆中的对象非常类似。

判断一个常量是否“废弃”还是相对简单,而要判定一个类是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器以及被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 7.常见面试题

为什么两个 survivor 区?Eden 和 survior 的比例分配

我们知道,目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成 Eden 空间、 From Survivor 和 To Survivor 三块区域。 看书的时候有个疑问,为什么非得是两个 Survivor 空间呢?要回答这个问题,其实等价于:为什么不是0个或1个 Survivor 空间?为什么2个 Survivor 空间可以达到要求?

为什么不是0个 Survivor 空间?

这个问题等价于:为什么需要 Survivor 空间。我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。 总之,设置 Survivor 空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。

为什么不是1个 Survivor 空间?

回答这个问题有一个前提,就是新生代一般都采用复制算法进行垃圾收集。原始的复制算法是把一块内存一分为二, gc 时把存活的对象(Eden和Survivor to)从一块空间(From space)复制到另外一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换 From space 和 To space 的逻辑角色(这样下一次 gc 的时候还可以按这样的方式进行)。 我们知道,在 HotSpot 虚拟机里, Eden 空间和 Survivor 空间默认的比例是 8:1 。我们来看看在只有一个 Survivor 空间的情况下,这个 8:1 会有什么问题。此处为了方便说明,我们假设新生代一共为 9 MB 。对象优先在 Eden 区分配,当 Eden 空间满 8 MB 时,触发第一次 Minor GC 。比如说有 0.5 MB 的对象存活,那这 0.5 MB 的对象将由 Eden 区向 Survivor 区复制。这次 Minor GC 过后, Eden 区被清理干净, Survivor 区被占用了 0.5 MB ,还剩 0.5 MB 。到这里一切都很美好,但问题马上就来了:从现在开始所有对象将会在这剩下的 0.5 MB 的空间上被分配,很快就会发现空间不足,于是只好触发下一次 Minor GC 。可以看出在这种情况下,当 Survivor 空间作为对象“出生地”的时候,很容易触发 Minor GC ,这种 8:1 的不对称分配不但没能在总体上降低 Minor GC 的频率,还会把 gc 的时间间隔搞得很不平均。把 Eden : Survivor 设成 1 : 1 也一样,每当对象总大小满 5 MB 的时候都必须触发一次 Minor GC ,唯一的变化是 gc 的时间间隔相对平均了。 上面的论述都是以“新生代使用复制算法”这个既定事实作为前提来讨论的。如果不是这样,比如说新生代采用“标记-清除”或者“标记-整理”算法来实现幸存对象的移动,好像确实是只需要一个 Survivor 就够了。

为什么2个 Survivor 空间可以达到要求?

问题很清楚了,无论 Eden 和 Survivor 的比例怎么设置,在只有一个 Survivor 的情况下,总体上看在新生代空间满一半的时候就会触发一次 Minor GC 。那有没有提升的空间呢?比如说永远在新生代空间满 80% 的时候才触发 Minor GC ? 事实上是可以做到的:我们可以设两个 Survivor 空间( From Survivor 和 To Survivor )。比如,我们把 Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后: 1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ; 2) 清空 Eden 和 From Survivor ; 3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。 可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。

说一下 JVM 内存模型吧,有哪些区?分别干什么的?

JVM 内存分布/内存结构?栈和堆的区别?堆的结构?

讲讲 jvm 运行时数据库区

jvm 的方法区中会发生垃圾回收吗?

后续问题解答可看上面。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有