看了这篇干货,搞定绝大多数JVM面试........

目录

Java类从编译到执行的全过程

类加载子系统

类加载器:

双亲委派:

为什么要双亲委派呢?

运行时数据区

堆:

堆内存中的垃圾收集规则:

新生代对象在经历过几次垃圾回收之后存活,才会被转到老年代?

为什么是15次?:

栈:

方法区:

Java8的元数据区:

本地方法栈:

程序计数器:

执行引擎

垃圾回收算法

如何定位垃圾?

标记清除算法(Mark-Sweep):

复制算法(copying):

标记整理算法(Mark-Compact): 

分代收集算法: 

分区收集算法:

垃圾回收器介绍以及组合

Serial 垃圾收集器(单线程、复制算法):

ParNew 垃圾收集器(Serial+多线程):

Parallel Scavenge 收集器(多线程复制算法、高效):

Serial Old 收集器(单线程标记整理算法 ):

Parallel Old 收集器(多线程标记整理算法):

CMS 收集器(多线程标记清除算法):

G1 收集器:

垃圾收集器之间的配合使用:

JVM常见面试问题解答

JVM调优场景举例

实际工作中遇到的案例

JVM常用的命令和参数

对象的创建过程

对象在内存中的存储布局

对象头包括什么

对象怎么定位

对象的分配过程

内存泄露和内存溢出

最大堆和最小堆的设置


Java类从编译到执行的全过程

Java类从编译到操作系统的执行,大致经过几个部分的处理,分别如下:

           Java类->编译器编译为class文件->类加载子系统->运行时数据区->执行引擎->操作系统。

乍一看上面描述的可能有点懵,那么让我们来讲一讲,这几部分到底是怎么协同工作的:

        1. 编码:我们通过编程工具对java语言进行编写,此时编写出来的是后缀为.java的java类文件,这个文件是为了让我们编程时更加便捷,是我们能看得懂的文件,但是虚拟机看不懂。

        2. 编译:运行代码时,通过计算机中的javac编译器,将java文件转化为class文件,这个文件是jvm所能看得懂的文件。

        3. 类加载子系统:通过这一步,对class文件进行加载、链接、初始化操作,最终将class文件的信息加载到jvm的内存中。

        4. 运行时数据区:在这一步,class文件被大卸八块,内部对象被分配到堆、方法被分配到栈、变量将被赋值、执行步骤将被记录。

        5. 执行引擎:字节码文件会被分配到执行引擎执行,执行引擎以指令为单位读取java字节码,一条条的执行指令,执行引擎在执行时会调用GC来进行垃圾回收。

        6. 操作系统:执行引擎在执行时通过JVM指令,指示JVM应该做什么工作,而JVM指令则是将操作系统的指令做出封装后的产物,换句话说,JVM此时告诉操作系统,应该做什么。

        看了以上的介绍,大家是不是有所了解了呢,下面让我们来一起了解一下类加载子系统、运行时数据区、执行引擎、垃圾回收算法这几部分的相关知识吧。

类加载子系统

类加载子系统的具体结构如下图所示:

类加载器:

BootstrapClassLoader:加载目录:lib/rt.jar等核心类。

ExtentionClassLoade:加载扩展jar包,jre/lib/ext/*.jar

AppClassLoader:加载classpath内容

CustomClassLoader:加载自定义的class类

这一部分需要提的就是,类加载器采用双亲委派的原则,对class类进行加载,关于双亲委派,具体解释如下:

双亲委派:

        当一个类加载器收到需要加载的请求时,它自己不会先去加载,而是将这个请求交给父类的加载器去加载,如果父类的加载器还有父类,那就一直向上请求直到请求到顶级的加载器。如果父类可以成功加载,那就直接成功返回;如果父类不能成功加载,那就由子加载器尝试自己加载,实际上就是收到请求后自己先不处理给父类处理。

         用大白话说就是,儿子在加载时不直接进行加载,而是一层一层的向上询问,父亲加载了吗,父亲没加载的话再问它的父亲,直至顶层,最后都没加载的话,顶层的加载器再逐层向下进行委派,直至委派到最底层的加载器。

为什么要双亲委派呢?

         为了安全和避免类的重复加载,例如:如果直接加载,不委派给父类加载器,那我们可以重写java核心类库,覆盖java的类库,在代码加入自己的逻辑,把代码移交给客户,在客户运行程序时通过自定义的类去窃取客户信息。

运行时数据区

运行时数据区的结构如下:

运行时数据区可谓是jvm面试的重点,下面我们来介绍一下运行时数据区的组成,以及相关的问题:

堆:

是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。 

新生代:用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区,默认分配内存比例:8:1:1.

Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行 一次垃圾回收。

ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

ServivorTo:保留了一次 MinorGC 过程中的幸存者。

堆内存中的垃圾收集规则:

        新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。

        老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

        Minor GC : 清理年轻代

        Major GC : 清理老年代

        Full GC : 清理整个堆空间,包括年轻代和永久代

所有GC都会停止应用所有线程。

新生代对象在经历过几次垃圾回收之后存活,才会被转到老年代?

        当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升),主要存放应用程序中生命周期长的内存对象,MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。

为什么是15次?:

        一个Java对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充,而一个对象的GC年龄,是存储在对象头里面的,而对象头里面有4个bit位来存储GC年龄,而4个bit位能够存储的最大数值是15,所以从这个角度来说,JVM分代年龄之所以设置成15次是因为它最大能够存储的数值就是15,虽然JVM提供了参数来设置分代年龄的大小,但是这个大小不能超过15,而实际当中也不一定非得到15次后才挪到老年代,在ServivorTo的内存占用率达到50%时,会将ServivorTo中最大的对象挪到老年代,新创建的对象在栈和线程内部以及eden区无法进行分配时,也会直接被放进老年代。

栈:

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

    比如:

          在main方法中定义 int i= 8; i=i++;

          将8压入栈,将8弹出,放入局部变量表的i中,此时i等于8

          i=i++,将i的值拿出来压栈,此时局部变量表的值进行了++是9,但是此时弹出的栈值是8.

          ++i正是做的相反的操作

方法区:

        用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据,运行时常量池(Runtime Constant Pool)也是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加 载后存放到方法区的运行时常量池中。

Java8的元数据区:

       Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。

本地方法栈:

        本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为 Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个 C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

程序计数器:

        记录当前代码执行的行号信息,就是执行到class文件的哪一行了。

执行引擎

        字节码文件会被分配到执行引擎执行,执行引擎以指令为单位读取java字节码,一条条的执行指令,执行引擎结构如下:

垃圾回收算法

如何定位垃圾?

引用计数法:

        对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

可达性分析:

        如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

标记清除算法(Mark-Sweep):

        最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间,最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

复制算法(copying):

        为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

 

标记整理算法(Mark-Compact): 

        结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

分代收集算法: 

        分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

新生代与复制算法:

        大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

老年代与标记复制算法:

        老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。 1. JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。 2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。 3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理。 4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。 5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。 6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。

分区收集算法:

        分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。

垃圾回收器介绍以及组合

Serial 垃圾收集器(单线程、复制算法):

        Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。

ParNew 垃圾收集器(Serial+多线程):

        ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。只有ParNew它能与CMS收集器配合工作。

Parallel Scavenge 收集器(多线程复制算法、高效):

        Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

Serial Old 收集器(单线程标记整理算法 ):

        Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。在 Server 模式下,主要有两个用途: 1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。 2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

Parallel Old 收集器(多线程标记整理算法):

        Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

CMS 收集器(多线程标记清除算法):

        Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程

重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。

并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

G1 收集器:

        Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是: 1. 基于标记-整理算法,不产生内存碎片。 2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。追求低停顿,建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征

垃圾收集器之间的配合使用:

连线的都可以配合使用:

JVM常见面试问题解答

JVM调优场景举例

所谓调优,不仅仅是针对线上出现了JVM问题的解决与跟踪,我理解调优包含以下几方面:

  1. 根据需求进行JVM规划和预调优。
  2. 优化JVM运行环境。
  3. 解决JVM运行中遇到的问题。

以下罗列几种调优的场景供大家参考,也可将场景带入自身项目中:

案例1,如何确认服务器配置?:

案例2,如何选择合适的垃圾回收器?:

案例3,系统cpu占用100,如何调优?:

案例4,系统内存过高,如何解决?:

案例5,如何面对cpu占用率居高不下?:

实际工作中遇到的案例

以下是小编在实际工作中遇到的JVM问题的举例:

场景1 CPU占用率过高:

       试运行时,发现周期性的在某个月的月底的一天中的两个小时,会有cpu居高不下的情况,每次过了这两个小时就没事了,起初以为是访问量增大没太在意,但是后来再次出现,查询了登录日志表,发现访问量并不大,开始排查原因:

  1. 使用top命令查看cpu占用情况,找到java占用的线程号。
  2. 使用top-Hp 线程号的命令查看线程的情况。
  3. 找到占用率最高的线程的线程号,使用jstack工具查看线程执行情况。
  4. 可以定位到哪个类的哪一行了,进行验证。

最后发现,在一个查询类中循环和判断的次数很多(每到月底会对新增的政策做出查询和导出,要求先查询万人助企的政策,然后按照万人助企下面的分类进行复杂排序,再查询其他类型的政策,再按照复杂规则排序),代码逻辑很是复杂,督促整改,最后将数据重新依据业务排序要求重新进行清洗和打标(修改了模型的入库顺序,同时要求模型的同事按照新的逻辑进行了分类),以简单的代码逻辑实现了业务需求。

场景2:CPU占用过高的问题:

1. 怀疑是不是GC导致的CPU突然变高。

2.使用top命令查看占用cpu较高的线程,但是发现找不到重点的线程了。

3.然后使用jstat -gcutil 线程号 端口号 打印出了gc日志。

4.发现Fullgc占用时间较长,stop the world 时间长达21分钟,查看内存分配怀疑分配给应用的内存小,但是运维反馈分配多少吃多少,所以确定是代码的问题了。

5.使用jmap命令下载gc日志,使用eclipse进行分析后发现,对象什么的都是正常的,使用命令,jstat -gccause 16671 1000 后发现,有代码中出现了System.gc()

6. 代码场景,统计中国移动每个分公司参加培训的人员信息,全国各地有好多个分公司,把这个统计写在了程序里面,而不是写在了sql里面,并且写完了之后怕对象占用的过高,所以又调用了System.gc()。

场景3:

        突然间有一天客户经理反馈系统整体变慢,且运维也反馈了有OOM出现并备份了GC的相关日志,申请相关的GC日志后,通过JVM自带的工具进行分析,发现一个类里面的一个对象占用过多,客户开通产品的程序中,客户可以开通最多好几千个产品,在持久化时可传入list对象,进行批量的持久化修改操作,只需要传入一次即可,有个程序写持久化的时候循环向list中放入对象,第一次放一个第二次放两个第三次放三个.......导致本该一次性完成的动作,循环了好几万次,造成了内存溢出。

JVM常用的命令和参数

常用命令:

    1、jps:查看进程及其相关去信息

    2、jmap:用来生成dump文件和查看堆相关的各类信息的命令。

    3、jstat:查看jvm运行时的状态信息

    4、jstack:查看jvm线程快照的命令

    5、jinfo:查看jvm参数和动态修改部分jvm参数

常用参数:

    1、-Xms:初始化堆大小

    2、-Xmx:最大堆大小

    3、-Xmn:新生代的内存空间大小

    4、-XX:SurvivorRatio

    5、-Xss:每个线程的堆栈大小

    6、-XX:PermSize:设置永久代初始值

    7、- XX:MaxPermSize:设置永久代最大值

对象的创建过程

  1. 加载:通过classLoader加载对象。
  2. 链接:验证、准备、解析
  3. 初始化:为类中的符号引用赋值,将内存中的地址指向变量。
  4. 申请对象内存
  5. 成员变量赋默认值
  6. 调用构造方法
  7. 成员变量顺序赋初始值
  8. 执行构造方法语句

对象在内存中的存储布局

普通对象:

数组对象:

对象头包括什么

对象头没有固定的内容,根据对象的状态去划分,下面的内容说出来即可:

对象怎么定位

由于reference类型在Java虚拟机规范里只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄直接指针

通过句柄池:

如果使用句柄访问方式,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。使用句柄方式最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

通过直接指针:

       如果使用该方式,Java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。使用直接指针方式最大的好处就是速度更快,他节省了一次指针定位的时间开销

HotSpot而言,他使用的是直接指针访问方式进行对象访问,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

对象的分配过程

  1. 先尝试在栈上分配,对象过大的话。
  2. 直接放在老年代。
  3. 否则,放在堆的edan区,之后经过S1和S2的洗礼,到达老年代。

内存泄露和内存溢出

        内存泄漏是指程序在申请内存后,无法释放已申请的内存空间。内存溢出是指程序申请内存时,没有足够的内存供申请者使用。

最大堆和最小堆的设置

        假设如果在生产环境中,初始堆大小-Xms与最大堆大小-Xmx是不等的,那么JVM就会根据堆内存的使用情况,动态的向操作系统申请内存,扩大或者是缩小,以-Xmx-Xms的值为上下界,这里的每一次调整都会产生一定的系统开销,虽然做到了动态申请堆大小的能力,不过生产环境中,很少说一台机器跑好多个JAVA程序,一般情况下都是一对一,那么动态申请调整堆大小就没有意义了,因为不管内存申请的多还是少,都只是这个JAVA程序在用,不需要给其他的程序腾出空间,相反的,如果把初始堆大小-Xms与最大堆大小-Xmx设置成不相等,那么反而画蛇添足,因为如果初始堆大小-Xms与最大堆大小-Xmx不相等,那么就会需要申请空间时,而每次申请空间,就会产生相应的系统开销,同时如果一开始堆大小是-Xms,会增加程序运行时进行垃圾回收的次数,降低程序的性能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只为code醉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值