JVM详解

目录

JVM的主要组成及其作用

作用

堆栈的区别

内存分别

存放的内容

程序可见度

对象的创建

为对象分配内存

处理并发安全问题

对象的访问定位

句柄访问

直接指针

类装载的执行过程

双亲委派模型

User user =new User()操作

JVM内存模型与GC算法

JVM内存模型

程序计数器(线程私有)

Java虚拟机栈(线程私有)

本地方法栈(线程私有)

Java堆(线程共享)

方法区(线程共享)

运行时常量池

直接内存

GC算法

标记-清除算法

复制算法

标记-整理算法

垃圾回收(GC在什么时候,对什么东西,做了什么事情)

在什么时候

对什么东西

做了什么事情

内存溢出详解

栈溢出

堆溢出

持久代溢出(OutOfMemoryError:PermGen space)

堆和栈的优缺点

为什么不把基本类型放堆中

JVM调优常用参数设置

堆配置

收集器设置

打印GC回收的过程日志信息

并行收集器设置

调优方式选择

年轻代大小选择

年老代大小选择

吞吐量优先的应用

较小堆引起的碎片问题

较小堆引起的碎片问题


JVM的主要组成及其作用


​ JVM包含两个子系统两个组件,两个子系统为Class Loader(类加载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Natice Interface(本地接口)

  • Class Loader:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area的method area
  • Execution engine:执行classes中的指令
  • Native Interface:与native libraries交互,是其他编程语言交互的接口
  • Runtime data area:这就是常说的jvm内存

作用

首先通过编译器将java代码转换为字节码,类加载器再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是jvm的一套指令,并不能交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由cpu去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。

堆栈的区别

物理地址

  • 堆的物理地址分配时不连续的。因此性能较慢。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-清除、复制、标记-整理、分代(即新生代使用复制算法,老年代使用标记-整理)
  • 栈使用的是数据结构中的栈,先进后出的原则,所以物理地址分配时连续的,所以性能快

内存分别

  • 堆因为是不连续的,所以分配内存时在运行期间确认的,因此大小不固定,一般堆远远大于栈
  • 栈时连续的,所以分配内存大小要在编译期确认,大小是固定的

存放的内容

  • 堆存放的是对象的实例和数组,因此该区更加关注的是数据的存储
  • 栈存放局部变量、操作数栈,返回结果,该区更加关注的是程序方法的执行

程序可见度

  • 堆对于整个应用程序都是共享的、可见的
  • 栈只对于线程是可见的,所以也是线程私有的,它的生命周期和线程相同

对象的创建

  • 虚拟机遇到一条new指令时,先检查常量池是否已经加载过相应的类,如果没有,必须先执行相应的类加载
  • 类加载过后,分配内存,若java对中南内存时绝对规整的,使用“指针碰撞”方式分配内存;如果是不规整的,就从空闲列表中分配
  • 划分内存时需要考虑并发问题,两种方式:CAS同步处理,或者本地线程分配缓冲。
  • 内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码。。。。。。),后执行方法

为对象分配内存

类加载完成后,接着会在java堆中划分一块内存分配给对象。内存分配根据java堆是否规整,有两种方法

  • 指针碰撞:如果java堆的内存时规整的,即所有用过的内存放在一边,而空闲的放在一边。分配内存时将位于中间的指针指示器向空闲内存移动一段与对象大小相等的距离,这样便完成分配内存工作
  • 空闲列表:如果java堆的内存时不规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表

选择那种分配方法时由java堆是否规整来决定的,而Java堆是否规整又由才用的垃圾收集器是否带有整理功能决定

处理并发安全问题

对象的创建在虚拟机中是一个频繁的行为,哪怕是只修改了一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象a分配内存,指针还没有来得及修改,对象b又同时使用了原来的指针来分配内存的情况,解决这个问题有两种情况:

  • 对分配内存空间的动作进行同步处理(采用CAS+失败重试来保障更新操作的原子性)
  • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲。那个线程要分配内存,就在那个线程的TLAB上分配。只有TALB用完并分配新的TLAB时,才需要同步锁

对象的访问定位

Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决 于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。

  • 指针: 指向对象,代表一个对象在内存中的起始地址。
  • 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是 指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的 真实内存地址

句柄访问

Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中 包含了对象实例数据与对象类型数据各自的具体地址信息

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是 非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改

直接指针

如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内 部的布局中就必须考虑如何放置访问类型数据的相关信息。

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非 常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用 的就是这种方式。

类装载的执行过程

  1. 加载:根据查找路径找到相应的 class 文件然后导入;
  2. 验证:检查加载的 class 文件的正确性;
  3. 准备:给类中的静态变量分配内存空间;
  4. 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为 一个标示,而直接引用直接指向内存中的地址;
  5. 初始化:对静态变量和静态代码块执行初始化工作
  • 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载 Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚 拟机识别的类库;
  • 其他类加载器:
  1. 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
  2. 应用程序类加载器(Application ClassLoader)。负责加载用户类路径 (classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我 们没有自定义类加载器默认就是用这个加载器

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去加载 这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如 此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无 法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加 载类。

User user =new User()操作

  • new User();创建一个User对象,内存分配在堆上
  • User user;创建一个引用,内存分配在栈上
  • =;将User对象地址赋值给引用

JVM内存模型与GC算法

JVM内存模型


黄色:由所有线程共享的数据区

蓝色:线程隔离的数据区

程序计数器(线程私有)

程序计数器是众多编程语言都共有的一部分,作用是标示下一条需要执行的指令的位置,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器完成的。
​ 对于Java的多线程程序而言,不同的线程都是通过轮流获得cpu的时间片运行的,这符合计算机组成原理的基本概念,因此不同的线程之间需要不停的获得运行,挂起等待运行,所以各线程之间的计数器互不影响,独立存储。这些数据区属于线程私有的内存。

Java虚拟机栈(线程私有)

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

本地方法栈(线程私有)

本地方法栈与虚拟机栈的作用十分类似,不过本地方法是为本地方法服务的。部分虚拟机(比如 SunHotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StactOverFlowError与OutOfMemoryError异常。

Java堆(线程共享)

Java堆是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此块内存的唯一目的就是存放对象实例,几乎所有的对象实例都在对上分配内存。JVM规范中的描述是:所有的对象实例以及数据都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配(对象只存在于某方法中,不会逃逸出去,因此方法出栈后就会销毁,此时对象可以在栈上分配,方便销毁),标量替换(新对象拥有的属性可以由现有对象替换拼凑而成,就没必要真正生成这个对象)等优化技术带来了一些变化,目前并非所有的对象都在堆上分配了。
​ 当java堆上没有内存完成实例分配,并且堆大小也无法扩展是,将会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域。

方法区(线程共享)

方法区与java堆一样,是线程共享的数据区,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码。JVM规范将方法与堆区分开,但是HotSpot将方法区作为永久代(Permanent Generation)实现。这样方便将GC分代手机方法扩展至方法区,HotSpot的垃圾收集器可以像管理Java堆一样管理方法区。但是这种方向已经逐步在被HotSpot替换中,在JDK1.7的版本中,已经把原本存放在方法区的字符串常量区移出。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Poll Table)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。其中字符串常量池属于运行时常量池的一部分,不过在HotSpot虚拟机中,JDK1.7将字符串常量池移到了java堆中

直接内存

直接内存不是JVM运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中引入了NIO(New Input/Output)类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java中的DirectByteBuffer对象作为对这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和Native对中来回复制数据。

GC算法

标记-清除算法

最基础的垃圾收集算法是“标记-清除”(Mark Sweep)算法,正如名字一样,算法分为2个阶段:

1.标记出需要回收的对象,

2.回收被标记的对象。

标记算法分为两种:

1.引用计数算法(Reference Counting)

2.可达性分析算法(Reachability Analysis)。由于引用技术算法无法解决循环引用的问题,所以这里使用的标
记算法均为可达性分析算法。

复制算法


​为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就让还存活的对象复制到另外一块内存区域中,然后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减少为实际的一半,代价比较高

标记-整理算法

​复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进行分配担保。所以在老年代中这种情况一般是不适合的。所以就出现了标记-整理(Mark-Compact)算法。与标记清除算法一样,首先是标记对象,然而第二步是将存活的对象向内存一段移动,整理出一块较大的连续内存空间。

垃圾回收(GC在什么时候,对什么东西,做了什么事情)

在什么时候

首先需要知道,GC又分为minor GC 和 Full GC(major GC)。Java堆内存分为新生代和老年代,新生代
中又分为1个eden区和两个Survior区域。一般情况下,新创建的对象都会被分配到eden区,这些对象经过一个minor gc后仍然存活将会被移动到Survior区域中,对象在Survior中每熬过一个Minor GC,年龄就会增加一岁,当他的年龄到达一定程度时,就会被移动到老年代中。

​ 当eden区满时,还存活的对象将被复制到survior区,当一个survior区满时,此区域的存活对象将被复制到另外一个survior区,当另外一个也满了的时候,从前一个Survior区复制过来的并且此时还存活的对象,将可能被复制到老年代。

  • Minor GC:从年轻代回收内存,当jvm无法为一个新的对象分配空间时会触发Minor GC,比如当Eden区满了。当内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden和Survior区不存在内存碎片写指针总是停留在所使用内存池的顶部。执行minor操作时不会影响到永久代,从永久代到年轻代的引用被当成GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉(永久代用来存放java的类信息)。如果eden区域中大部分对象被认为是垃圾,永远也不会复制到Survior区域或者老年代空间。如果正好相反,eden区域大部分新生对象不符合GC条件,Minor GC执行时暂停的线程时间将会长很多。Minor may call “stop the world”;
  • Full GC:是清理整个堆空间包括年轻代和老年代。那么对于Minor GC的触发条件:大多数情况下,直接在eden区中进行分配。如果eden区域没有足够的空间,那么就会发起一次Minor GC;对于FullGC的触发条件:如果老年代没有足够的空间,那么就会进行一次FullGC在发生MinorGC之前,虚拟机会先检查老年代最大可利用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否是允许担保失败(不允许则直接FullGC)如果允许,那么会继续检查老年代最大可利用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试minor gc (如果尝试失败也会触发Full GC),如果小于则进行Full GC。但是,具体什么时候执行,这个是由系统来进行决定的,是无法预测的。

对什么东西

​ 主要根据可达性分析算法,如果一个对象不可达,那么就是可以回收的,如果一个对象可达,那么这个对象就不可以回收,对于可达性分析算法,它是通过一系列称为“GC Roots”的对象最为起始点,当一个对象GC Roots没有任何引用链相接的时候,那么这个对象就是不可达,就可以被回收。

做了什么事情

主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。例如新生代采用了复制算法,老年代采用了标记整理法。

​在新生代中,分为一个ede区域和两个Survior区域,真正使用的是一个eden区域,和一个Survior区域,GC的时候,会把存活的对象放入到另一个Survior区域中,然后再把这个eden区域和Survior区域清除。

​那么对于老年代,采用的是标记整理发,首先标记出存活对象,然后在移动到一段。这样有利于减少内存碎片。
​标记:标记的过程其实就是,遍历所有gc root 然后将所有gc root 可达的对象标记为存活对象
​清除:清除的过程中将遍历堆中所有的对象,将没有标记的对象全部清除掉
​ 主要缺点:标记和清除过程效率不高,标记清除之后会产生大量不连续的内存碎片但是,老年代中因为对象存活率高,没有额外空间对他进行分配担保,就必须使用标记整理算法。
​ 标记整理算法 :标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一段移动,并更新其引用对象的指针
​ 主要缺点:在标记清除的基础上还需要进行对象的移动,成本相对比较高,成本相对较高,好处是不会产生内存碎片。

内存溢出详解

栈溢出

栈溢出抛出StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。 下面我们通过一段代码来模拟一下此种情况的内存溢出。

  1. public class OOMTest
  2. {
  3.     public void stackOverFlowMethod(){
  4.         stackOverFlowMethod();
  5.     }
  6.     public static void main(String... args){
  7.         OOMTest oom = new OOMTest();
  8.         oom.stackOverFlowMethod();
  9.     }
  10. }

运行上面代码,会出现以下异常

  1. Exception in thread "main" java.lang.StackOverflowError
  2. at OOMTest.stackOverFlowMethod(OOMTest.java:6)
     

堆溢出

堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。

  • 如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。
  • 如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。下面我们通过如下的代码来演示一下此种情况的溢出:
  1. public class OOMTest{
  2.     public static void main(String... args){
  3.         List<byte[]> buffer = new ArrayList<byte[]>();
  4.         buffer.add(new byte[10*1024*1024]);
  5.     }
  6. }

通过命令运行以上代码

  1. java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest

程序输出信息如下

  1. [GC 1180K->366K(19456K), 0.0037311 secs]
  2. [Full GC 366K->330K(19456K), 0.0098740 secs]
  3. [Full GC 330K->292K(19456K), 0.0090244 secs]
  4. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  5. at OOMTest.main(OOMTest.java:7)

从运行结果可以看出,JVM进行了一次Minor gc和两次的Full gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。
​ 通过上面的实验其实也从侧面验证了一个结论:对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,触发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了。

持久代溢出(OutOfMemoryError:PermGen space)

我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久代溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space。可能在如下几种场景下出现:

  1. 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
  2. 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
  3. 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出.下面我们通过如下的代码来模拟此种情况:
  4. public class OOMTest{
  5.     public static void main(String... args){
  6.         List<String> list = new ArrayList<String>();
  7.         while(true){
  8.             list.add(UUID.randomUUID().toString().intern());
  9.         }
  10.     }
  11. }

我们通过如下的命令运行上面代码:

  1. java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest

运行后的输出如下图所示:

  1. Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
  2. at java.lang.String.intern(Native Method)
  3. at OOMTest.main(OOMTest.java:8)


堆和栈的优缺点

  1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
  2. 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
  3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
  4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。

为什么不把基本类型放堆中

因为基本类型占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增
长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是Java中参数传递时的问题。

JVM调优常用参数设置

堆配置

  1. -Xms:初始堆大小
  2. -Xms:最大堆大小
  3. -XX:NewSize=n:设置年轻代大小
  4. -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年
  5. 轻代年老代和的1/4
  6. -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示
  7. Eden: 3 Survivor:2,一个Survivor区占整个年轻代的1/5
  8. -XX:MaxPermSize=n:设置持久代大小

收集器设置

  1. -XX:+UseSerialGC:设置串行收集器
  2. -XX:+UseParallelGC:设置并行收集器
  3. -XX:+UseParalledlOldGC:设置并行年老代收集器
  4. -XX:+UseConcMarkSweepGC:设置并发收集器

打印GC回收的过程日志信息

  1. -XX:+PrintGC
  2. -XX:+PrintGCDetails
  3. -XX:+PrintGCTimeStamps
  4. -Xloggc:filename

并行收集器设置

  1. -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数
  2. -XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收
  3. 完,也会停止回收)
  4. -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
  5. -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
  6. -XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的CPU数。并行收集线程数

调优方式选择

年轻代大小选择

响应时间优先的应用:尽可能设置大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时减少到达年老代的对象。吞吐量优先的应用:尽可能的设置大,可能到达Gbit的成都,因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8核CPU以上应用。

年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可能会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考一下数据获得:
1、并发垃圾收集信息
2、持久代并发收集次数
3、传统GC信息
4、花在年轻代和年老代回收上的时间比例减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期对象,而年老代存放长期存活的对象

较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

  1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩
  2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次FullGc后,对年老代进行压缩

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期对象,而年老代存放长期存活的对象

较小堆引起的碎片问题

​ 因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

  1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩
  2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次FullGc后,对年老代进行压缩

本文转载自:https://blog.csdn.net/qq_41167306/article/details/123636051

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值