打怪升级jvm关于jvm内存模型及GC调优

Posted fisher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了打怪升级jvm关于jvm内存模型及GC调优相关的知识,希望对你有一定的参考价值。

JVM调优,其实就是调整SWT和FGC的过程

 

  • JVM内存模型

  通过一张基础的图了解最简单的JVM模型:

  

  其实在jvm模型中,主要包含了我们常见的堆栈方法区等待--每个版本不同可能解释有所不同,这里默认以8版本为例:

  首先给出官方文档的解释:

  https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4

2.5. Run-Time Data Areas

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

2.5.1. The pc Register

The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine\'s pc register is undefined. The Java Virtual Machine\'s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

2.5.2. Java Virtual Machine Stacks

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.

In the First Edition of The Java® Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack.

This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes.

The following exceptional conditions are associated with Java Virtual Machine stacks:

  • If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.

  • If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

2.5.3. Heap

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor\'s system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.

The following exceptional condition is associated with the heap:

  • If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.

2.5.4. Method Area

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

2.5.5. Run-Time Constant Pool

run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.

Each run-time constant pool is allocated from the Java Virtual Machine\'s method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.

The following exceptional condition is associated with the construction of the run-time constant pool for a class or interface:

  • When creating a class or interface, if the construction of the run-time constant pool requires more memory than can be made available in the method area of the Java Virtual Machine, the Java Virtual Machine throws an OutOfMemoryError.

See §5 (Loading, Linking, and Initializing) for information about the construction of the run-time constant pool.

2.5.6. Native Method Stacks

An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine\'s instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.

This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes.

The following exceptional conditions are associated with native method stacks:

  • If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
  • If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

 

  针对oracle的官方文档,给我们描述了jvm虚拟机的几个主要模块。那么从普通开发者的角度,其实这几大块分别干了这些事:

    • Heap堆:

  Java虚拟机具有在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中为所有类实例和数组分配内存。堆可以是固定大小的,或者可以根据计算的需要进行扩展,并且如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。 Java虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,以及如果堆可以动态扩展或收缩,则可以提供对最大和最小堆大小的控制。如果计算所需的堆数超过了自动存储管理系统所能提供的堆数,Java虚拟机将抛出OutOfMemoryError

   那么可以看出,堆的主要作用就是分配空间,属于运行时数据区,会将我们运行时的内存分配。

  同时,堆内存又分为新生代和老年代,以Young和Old区分,其中新生代主要存放回收年龄较短或者一些新new的对象,而老年代则是存放一些无法被gc的对象(考虑一下哪些对象会被放入老年代?

  堆内存,其实又被分为了三个区域:

    Young:新生代;Old:老年代;Mate:元空间(永久代)

  其中,新生代又分为这几段:

    Eden:伊甸区;

    s0、s1:survivor,用于YGC中复制移动;

  

    默认,Eden:s0:s1是8:1:1的关系,而老年代和年轻代的比例默认是2:1

  那么,什么时候会放入新生代什么时候会放入老年代呢?

    默认,新创建的对象第一次会存放在新生代Eden中,我们认为新生代的对象百分之80都可能被回收掉,那么第一次YGC就会把Eden的对象先复制到s0,这是内存的复制,速度很快;

    下一次YGC,又会把Eden和不为空的s0做YGC,因为我们认为大多数对象都要被回收,再把Eden和s0全部清空,那么没有回收的对象就位于s1;

    再下一次.... 就是s0和s1来回倒。

  但是,如果对象很大无法被放入新生代,或者它已经超过动态survivo的大小50%以上,我们就认为它不适合在新生代了,就会直接放去老年代。

  同时,jvm的Object有对象头,对象头包含了比如说对象线程,锁,和一些gc年龄的属性,那么我们认为默认它的年龄到达15,它就是一个不可被回收的对象,那么就放入老年代;大对象也会放入老年代;还有如果Eden满了出发YGC,那么存活对象大小s0没办法承受也会将部分多余的对象放入老年代,我们认为老年代的对象很难被回收,那么什么对象可以出现在老年代呢?例如:spring生命周期的bean,常量(这里string常量池其实有些许变化),定义线程池、连接池这些都应该属于不可被回收。

    • Stock栈(也叫虚拟机栈,线程栈)

  每个Java虚拟机线程都有一个与线程同时创建的专用Java虚拟机堆栈。该规范允许Java虚拟机堆栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果Java虚拟机堆栈具有固定大小,则在创建每个Java虚拟机栈时,可以独立地选择该栈的大小。如果线程中的计算需要比允许的更大的Java虚拟机堆栈,则Java虚拟机会抛出StackOverflowError。如果Java虚拟机堆栈可以动态扩展,并且尝试进行扩展,但没有足够的内存可用于实现扩展,或者如果没有足够的存储器可用于创建新线程的初始Java虚拟机栈,则Java虚拟机将抛出OutOfMemoryError。

  栈,顾名思义,是一种FILO的结构,那么在我们方法调用时,就会在栈内存中存储,包括一些对象的引用,栈中又有一个概念叫栈桢,什么是栈帧呢?

  简单举例子就是说,比如我们有一个A方法调用B方法,B再调用C方法,那么这个栈帧可以这么表示:

  

  并且,栈存储会随着线程的创建而创建,会随着线程的销毁而释放,不存在gc。这里其实有一个概念,比如说栈内存的OOM,会是什么样子?

  如果一个递归,无法跳出递归或者递归数量太大,栈内存设置太小,是可能会抛出OOM的。

   局部变量表:对应的就是方法参数和一些局部变量,因为这些都是线程私有的,所以不需要额外gc,随着线程结束被释放。

  操作数栈:栈内的一些计算操作。

  动态链接:一些引用?

  方法返回地址:调用当前方法寄存器的值。

    • Native Method Stock本地方法栈

  与stock类似,只不过这里存储的是基于一些native之类的本地方法。

    • Method Area 方法区/元空间

  首先,明确一个概念,方法区/元空间,又可以叫做no-heap,它是用于与堆内存进行分开的概念!这个概念在jdk7叫做方法区,在jdk8叫做元空间,而且元空间不需要指定默认的大小了,而是会根据物理内存进行计算,当然如果物理内存不够了也会抛出OOM。并且,元空间是代替了7的老年代,其本质也是属于堆的一部分。

  那么,为什么从7->8会有这样的变化呢?

  1.永久代是有固定空间的,如果永久代的空间太小,-XX:MaxPermSize太小就会导致永久代OOM。

  2.元空间使用的情况根据物理存储,最大没有限制,当物理内存达不到要求后同样会出发OOM。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1。

  3.MetaspaceSize是初始化指定的大小,当达到了这个大小后,会触发FGC,同时会根据FGC的回收情况适当调整。所以线上如果频繁FGC,可能跟这个值有关,可以适当增大。

    • STW

  STW全名叫stop the world,它代表的意思是说在jvm触发GC时,会停止当前所有的用户线程,然后在gc完成后释放,那么在gc的时间段内就会发生所有的动作暂停无响应的情况。当然后面会有针对G1,ZGC以及一些回收算法可以并行的模式。

  其中,Class对象 -- 永久代/元数据;

   字符串常量 -- 1.7永久代;1.8在堆heap里;

  在1.8之后,元数据不存在于堆,而是根据操作系统的内存进行管理的;

 

  • 什么是垃圾

  垃圾,顾名思义就是要被回收的对象,或者说要被回收的一组对象。

  首先我们需要有个概念,java本身针对内存指针甚至内存空间的方式,都是基于unsafe或者其他的方式,我们在写java代码中不需要手动释放内存,这与c有很大的不同。

  那么为什么java帮助我们去这么做呢?无非这么几点

    1.忘记回收内存,那么这一块内存空间就会被占用无法释放。

    2.多次回收,那么会不会将新的可用的数据回收掉?

    3.更加简化开发。

  那既然GC就是回收不可用的垃圾,是有jvm帮我们去完成的,那么jvm到底是如何确定什么是垃圾的呢?

  主要有两种办法:

    1.计数器,jvm会在对象头记录计数器,计数器代表引用计数器,那么如果这个计数器为0了,它没有被任何对象引用那么它就是一个可以回收的垃圾了。

    2.GCROOT,根可达算法:试想一下如果有三个对象ABC,A持有B,B持有C,C持有A,但是它们再没有其他的对象引用了,那么它们的引用计数器不为0,但是它们其实算一堆垃圾,这样的话我们的根可达就派上用场了。比如我们根据Object,一直向下去找,那么找得到的对象就是可用的对象,那么其他的对象都可以称为不可用对象。什么概念呢?比如我们的局部变量 和我们的连接池 线程池 包括常量池 JNI指针持有的对象那这些一定是可用变量甚至加载的Clazz,一定不会被回收

  什么是对象头?

object header Mark word(64bit) klass pointer(64bit)
normal object 普通对象,无锁。 unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
biased object 带有偏向锁的对象 thread:54 |epoch:2 |unused:1|age:4|biased_lock:1|lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
带有轻量锁的对象 prt_to_lock_record:62 |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
带有重量锁的对象 prt_to_heavyweigth_monitor:62 |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
GC |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩

 

 

 

 

 

 

 

 

 

 

  • 常见的垃圾回收算法

    • 标记清除

  首先对垃圾进行标记,然后进行回收,但是这种方式会导致内存间断,产生大量的内存碎片;这时当我们要分配一个大的对象时,可能会经历频繁的GC(目前大对象也可以直接扔在老年代中)

    • copy复制

  复制算法,相当于把可用的对象复制在另一个内存块中,然后直接将当前的内存清除;这样不会有内存碎片,但是缺点是内存占用会很大,最少需要将内存划分成两块进行复制移动,而且存活对象过多会导致效率低下

    • 标记压缩

  这时复制的升级版,其实它会将不可用对象清除后将可用对象向一端移动,这样的好处是不会有内存碎片并且不需要划分内存空间、但是效率会比较低

    • 分代收集

  这是最常用的算法,会根据内存空间进行划分,并且针对不同的内存选择不同的算法:例如新生代可以选用复制算法,默认新生代的对象很多都需要被回收;而老年代采用标记压缩或复制,腾出大片内存。

 

  • 常见的垃圾回收器

    • Serial Young:串行回收,会触发STW。
    • Paraller Scavenge: ps,并行回收。
    • ParNew配合CMS: 并行回收年轻代。
    • Serial Old:串行回收,老年代。
    • ParOld:并行回收,老年代。
    • CMS:ConcurrentMarkSweep:并发回收,老年代。GC和用户进程同时进行,降低STW。
    • G1:10ms 不区分老年代新生代
    • ZGC:1ms 不区分老年代新生代

 

  • 如何判断当前参数

  https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  -XX:+PrintCommandLineFlags:当前jvm参数

  -XX:+PrintFlgsFinal :最终参数值

  -XX:+PrintFlagsInitial:默认参数值

  1.8版本默认使用的应该是Paraller GC 并行回收 : 默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)。

  这里整理一个简单的表格,对这几种GC做简单的对比:

GC 特性 描述
Paraller  吞吐量 多线程STW 
G1(Garbage first) 均衡 多线程SWT 并发 分代回收
ZGC(Z Garbage) 延迟 所有并发
Shenandoah(jdk 12后) 延迟 所有并发
Serial  内存大小 启动时间 单线程STW 

 

  

 

 

 

 

  Parallel GC是JDK 8以及更早版本的默认回收期。它专注于吞吐量,尽快完成工作,而很少考虑延迟(暂停)。

  Parallel GC会在STW(全局暂停)期间,以更紧凑的方式,将正在使用中的内存移动(复制)到堆中的其他位置,从而制造出大片的空闲内存区域。当内存分配请求无法满足时就会发生STW暂停,然后JVM完全停止应用程序运行,投入尽可能多的处理器线程,让垃圾回收算法执行内存压缩工作,然后分配请求的内存,最后恢复应用程序执行。

  G1 GC是JDK 9以后的默认回收期。G1试图平衡吞吐量和延迟。一方面,在STW暂停期间,依然会利用分代继续执行内存回收工作,从而最大化效率,这一点和Parallel GC相同;但是,它还会尽可能避免在暂停期间执行需要较长时间的操作。G1的长时间操作会与应用程序并行进行,即通过多线程方式,在应用程序运行时执行。这样可以大幅度减少暂停,代价是整体的吞吐量会降低一点。

  所以,GC在8-9版本其实是一个分水岭,从9版本后默认使用G1,并且优化了G1处理的时间,包括G1处理大对象及老年代的时间。

 

  那么,哪些场景下适用哪些垃圾回收器呢?

  Serial:适用于单线程场景,简单的client客户端,内存小,没有过多的对象,单线程回收不需要线程切换的开销。

  ParNew/Paraller :多CPU的服务器,可以采用多线程的方式回收,但是Paraller 追求的是短时间内尽量完成任务,那么就会有SWT时间,不适合交互型场景;ParNew降低了SWT时间,更适合交互场景。

    Parallel Scavenge提供的参数

    -XX:GCTimeRadio
    直接设置吞吐量大小,GC时间占总时间比率.相当于是吞吐量的倒数.

    -XX:MaxGCPauseMillis
    设置最大GC停顿时间.
    Parallel Scavenge会根据这个值的大小确定新生代的大小.如果这个值越小,新生代就会越小,从而收集器就能以较短的时间进行一次回收;但新生代变小后,回收的频率就会提高,吞吐量也降下来了,因此要合理控制这个值.

    -XX:+UseAdaptiveSizePolicy
    通过命令就能开启GC 自适应的调节策略(区别于ParNew).我们只要设置最大堆(-Xmx)和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survior的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis              或GCTimeRadio.

  

  • 聊聊G1

  首先一点,不论是新生代还是老年代,G1 ZGC等垃圾回收器是不区分内存类型的。

  通过 -XX:+UseG1GC 可以指定使用G1垃圾回收器。

  G1首先具备压缩功能、避免碎片化内存问题、而且G1的SWT暂停时间可控、多线程GC、面向服务端应用比较友好、而且可以预测停顿的时间。

  首先,G1会将所有堆内存划分成很多块大小相等的Region。每次要触发GC时,首先估算每个Region中可回收垃圾的数量、每次先从可回收最大的量开始回收,因此它的效率性能是很高的。

 

  这样,其实在G1里,不再区分老年代新生代了,整个堆内存都是Region。但是衍生出了一个Humongous,它是特殊的Old,专门存放大型的对象。

  这样的划分方式意味着不需要一个连续的内存空间管理对象.G1将空间分为多个区域,优先回收垃圾最多的区域.
  G1采用的是Mark-Copy ,有非常好的空间整合能力,不会产生大量的空间碎片
  G1的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成垃圾回收任务,通过jstat命令可以查看垃圾回收情况,在YGC时S0/S1并不会交换.

 

  那么,如果一个对象,它自身和它持有引用的对象没有分配在一个Region中,我们是否需要遍历所有的Region才能进行一次GCRoot?每个Region上都有一个RememberSet,用于记录当前区域引用对象所在的区域。

 

   

  G1的GC模式

  1.YoungGC年轻代收集
  在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。

  YoungGC的回收过程如下:

  根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
  处理Dirty card,更新RSet.
  扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
  拷贝扫描出的存活的对象到survivor2/old区
  处理引用队列,软引用,弱引用,虚引用
     2. mixed gc

  当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

  G1没有fullGC概念,需要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)。

 

  何时使用G1

  G1的第一个重要特点是为用户的应用程序的提供一个低GC延时和大内存GC的解决方案。这意味着堆大小6GB或更大,稳定和可预测的暂停时间将低于0.5秒。

  如果应用程序使用CMS或ParallelOld垃圾回收器具有一个或多个以下特征,将有利于切换到G1:

  Full GC持续时间太长或太频繁
  对象分配率或年轻代升级老年代很频繁
  不期望的很长的垃圾收集时间或压缩暂停(超过0.5至1秒)
  注意:如果你正在使用CMS或ParallelOld收集器,并且你的应用程序没有遇到长时间的垃圾收集暂停,则保持与您的当前收集器是很好的,升级JDK并不必要更新收集器为G1。

 

  • 关于jvm调优

  关于jvm调优,我相信很多人甚至不会接触,因为毕竟有多少开发能直接操作线上服务器环境呢?可能也就是公司大牛级别的人了。

  jvm本身东西很多,但是更多的说到jvm调优,我们主要是针对full GC 就是FGC的优化,至于YGC 是正常的,但是我们希望在应用服务中,更多的对象应该在YGC被回收,而不是无法回收全部放入FGC,因为FGC里的对象都是长期存活的,对应的FGC的时间也会更长!!同时还有一些基于jvm的参数,例如新生代中eden、s0、s1的大小,这些都会直接影响到对象是否会被直接扔在老年代中。当然,如果线上程序很稳定,jvm监控FGC的频率 时间都很正常,不建议修改jvm的参数!而且升级jdk版本也无需修改GC回收器!!

  首先,我们要知道哪些会导致FGC

  1.System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。

  2.老年代空间不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。,当执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  3.Metaspace区内存达到阈值

  4.统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间 Survivor区域对象晋升到老年代有两种情况:
  (1)一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
  (2)另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。                5.堆中产生大对象超过阈值这个参数可以通过-XX:PretenureSizeThreshold进行设定,大对象或者长期存活的对象进入老年代,典型的大对象就是很长的字符串或者数组,它们在被创建后会直接进入老年代,虽然可能新生代中的Eden区域可以放置这个对象,在要放置的时候JVM如果发现老年代的空间不足时,会触发GC。

  6.老年代连续空间不足
JVM如果判断老年代没有做足够的连续空间来放置大对象,那么就会引起Full GC,例如老年代可用空间大小为200K,但不是连续的,连续内存只要100K,而晋升到老年代的对象大小为120K,由于120>100的连续空间,所以就会触发Full GC。

 

  那如何排查服务gc频率呢?

  直接上arthas:

  • Github:https://github.com/alibaba/arthas
  • 文档:https://arthas.aliyun.com/doc/

  https://arthas.aliyun.com/doc/vmoption.html 

  通过dashboard 以及命令可以排查gc的问题:

  • 使用vmoption命令动态打开GC日志

  $ vmoption PrintGC true

$ vmoption PrintGC true
Successfully updated the vm option.
 NAME     BEFORE-VALUE  AFTER-VALUE
------------------------------------
 PrintGC  false         true

  $ vmoption PrintGCDetails true

$ vmoption PrintGCDetails true
Successfully updated the vm option.
 NAME            BEFORE-VALUE  AFTER-VALUE
-------------------------------------------
 PrintGCDetails  false         true
  • 使用vmtool强制GC

  $ vmtool --action forceGc

[GC (JvmtiEnv ForceGarbageCollection) [PSYoungGen: 2184K->352K(76288K)] 19298K->17474K(166912K), 0.0011562 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (JvmtiEnv ForceGarbageCollection) [PSYoungGen: 352K->0K(76288K)] [ParOldGen: 17122K->16100K(90112K)] 17474K->16100K(166400K), [Metaspace: 20688K->20688K(1069056K)], 0.0232947 secs] [Times: user=0.14 sys=0.01, real=0.03 secs]
  • 其他gc参数

  $ vmoption PrintGCID true 打印GC ID

$ vmoption PrintGCID true
Successfully updated the vm option.
 NAME       BEFORE-VALUE  AFTER-VALUE
--------------------------------------
 PrintGCID  false         true

  $ vmoption PrintGCDateStamps true 打印GC时间戳

$ vmoption PrintGCDateStamps true
Successfully updated the vm option.
 NAME               BEFORE-VALUE  AFTER-VALUE
----------------------------------------------
 PrintGCDateStamps  false         true

  $ vmoption PrintGCTimeStamps true 打印GC启动时间

$ vmoption PrintGCTimeStamps true
Successfully updated the vm option.
 NAME               BEFORE-VALUE  AFTER-VALUE
----------------------------------------------
 PrintGCTimeStamps  false         true
  • heapdump

  打开HeapDumpBeforeFullGC开关,可以在GC前生成heapdump文件;打开HeapDumpAfterFullGC开关,可以在GC结束后生成heapdump文件

$ vmoption HeapDumpBeforeFullGC true
Successfully updated the vm option.
 NAME                  BEFORE-VALUE  AFTER-VALUE
-------------------------------------------------
 HeapDumpBeforeFullGC  false         true
$ vmtool --action forceGc

  再使用vmtool --action forceGc强制GC,则可以在GC日志中发现heapdump信息,并且在应用目录下会生成heapdump hprof

  打开PrintClassHistogramBeforeFullGC开关,可以在GC前打印类直方图;打开PrintClassHistogramAfterFullGC开关,可以在GC结束后打印类直方图

$ vmoption PrintClassHistogramBeforeFullGC true
Successfully updated the vm option.
 NAME                             BEFORE-VALUE  AFTER-VALUE
------------------------------------------------------------
 PrintClassHistogramBeforeFullGC  false         true
$ vmtool --action forceGc

  再使用vmtool --action forceGc强制GC,在GC日志中会打印类直方图,可以直观知道每个类的instances数量,占用内存大小:

 

#13: [Class Histogram (before full gc):
 num     #instances         #bytes  class name
----------------------------------------------
   1:         24519        5783400  [C
   2:          5648        5102712  [B
   3:          3685         888128  [Ljava.lang.Object;
   4:          3255         619560  [I
   5:         24263         582312  java.lang.String
   6:          4227         475320  java.lang.Class
   7:          1288         402112  [Ljava.util.HashMap$Node;
   8:            75         296160  [Ljava.nio.channels.SelectionKey;
   9:          6759         216288  java.util.HashMap$Node
  10:          2069         182072  java.lang.reflect.Method
  11:          3326         133040  java.util.LinkedHashMap$Entry

 

  具体使用,参考arthas的使用文档。

 

  • 常用JVM参数

堆设置:
-Xmx3500m 设置JVM最大可用内存为3550M
-Xms3500m 设置JVM堆内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
-Xmn2g 设置年轻代大小为2G
-Xss128k 设置每个线程的堆栈大小
‐XX:MetaspaceSize=256M  设置元空间大小
‐XX:MaxMetaspaceSize=256M  设置元空间最大值
-XX:NewRatio=4 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)
-XX:SurvivorRatio=4 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4
-XX:MaxPermSize=16m 设置持久代大小为16m
-XX:MaxTenuringThreshold=0 设置垃圾最大年龄,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代
垃圾收集器:
-XX:+UseParallelGC 选择垃圾收集器为并行收集器
-XX:ParallelGCThreads=20 配置并行收集器的线程数
-XX:+UseParallelOldGC 配置年老代垃圾收集方式为并行收集
-XX:MaxGCPauseMillis=100 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
-XX:+UseConcMarkSweepGC设置年老代为CMS并发收集
-XX:+UseParNewGC 设置年轻代为并行收集。可与CMS收集同时使用
日志打印:
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间
-XX:PrintHeapAtGC 打印GC前后的详细堆栈信息

  例如:

  1.将堆的最大、最小设置为相同的值,目的是防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间。
  -Xmx3550m: 最大堆大小为3550m。
  -Xms3550m: 设置初始堆大小为3550m。

  2.在配置较好的机器上(比如多核、大内存),可以为老年代选择并行收集算法: -XX:+UseParallelOldGC 。

  3.年轻代和老年代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率来调整二者之间的大小,也可以针对回收代。

  比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

  4.年轻代和老年代设置多大才算合理

  1)更大的年轻代必然导致更小的老年代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的老年代会导致更频繁的Full GC

  2)更小的年轻代必然导致更大老年代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的老年代会减少Full GC的频率

  如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。但很多应用都没有这样明显的特性。

  在抉择时应该根 据以下两点:
  (1)本着Full GC尽量少的原则,让老年代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
  (2)通过观察应用一段时间,看其他在峰值时老年代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给老年代至少预留1/3的增长空间。

  在实际过程中,我们并不频繁调整JVM参数,保证能够使用就好,当然在日常的监控中我们可以观察一下jvm中gc的频率,FGC的大小,根据具体的场景进行选择!正常情况下非必要不要去尝试调整,否则线上问题会很头疼。

 

JVM(Java虚拟机)详解(JVM 内存模型堆GC直接内存性能调优)

JVM(Java虚拟机)

JVM 内存模型 结构图

jdk1.8 结构图(极简)


jdk1.8 结构图(简单)

JVM(Java虚拟机):

  • 是一个抽象的计算模型。
  • 如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。
  • 目的是为构建在其上运行的应用程序提供一个运行环境,能够运行 java 字节码。
  • JVM 可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。


jdk1.7 结构图(详细)


JVM 内存模型 组成元素

Java 内存模型主要包含线程私有程序计数器java虚拟机栈本地方法栈线程共享堆空间元数据区直接内存

  • Java运行时数据区域

    Java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 Java 进程产生和消失。

    根据 JVM 规范,JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。

  • 程序计数器(PC 寄存器、计数器)

    程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。

    在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。

    可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

    倘若执行的是 native 方法,则程序计数器中为空

  • Java 虚拟机栈(JVM Stacks)

    虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。

    不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

    每个栈帧主要包含的内容如下:

    • 局部变量表

      存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用

      注意:这里的基本数据类型指的是方法内的局部变量

      局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。

    • 操作数栈

    • 动态连接

    • 方法返回地址

    虚拟机栈可能会抛出两种异常:
    • 栈溢出(StackOverFlowError):

      若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常

    • 内存溢出(OutOfMemoryError):

      若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常

  • 本地方法栈(Native Method Stacks)

    本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。

    本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。

    本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

    虚拟机栈和本地方法栈的主要区别:

    • 虚拟机栈执行的是 java 方法
    • 本地方法栈执行的是 native 方法
  • Java 堆(Java Heap)

    Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。

    Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

    Java 堆的分区:

    • 在 jdk1.8 之前,分为新生代、老年代、永久代

    • 在 jdk1.8 及之后,只分为新生代、老年代

      永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代

    Java 堆内存大小:

    • 堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)
    • 既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)
    • 如果堆无法扩展或者无法分配内存时报 OOM

    主要存储的内容是:

    • 对象实例

    • 类初始化生成的对象

    • 基本数据类型的数组也是对象实例

    • 字符串常量池

      字符串常量池原本存放在方法区,jdk8 开始放置于堆中

      字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table

    • 静态变量

      • static 修饰的静态变量,jdk8 时从方法区迁移至堆中

      • 线程分配缓冲区(Thread Local Allocation Buffer)

        线程私有,但是不影响 java 堆的共性

        增加线程分配缓冲区是为了提升对象分配时的效率

    堆和栈的区别:

    • 管理方式,堆需要GC,栈自动释放
    • 大小不同,堆比栈大
    • 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
    • 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
    • 效率:栈的效率比堆高
  • 方法区(逻辑上)

    方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)

    方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区

    各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

    方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。

    • Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制
    • Java8 移除了永久代和方法区,引入了元空间

    拓展:

    JDK版本方法区的实现运行时常量池所在的位置
    JDK6PermGen space(永久代)PermGen space(永久代)
    JDK7PermGen space(永久代)Heap(堆)
    JDK8Metaspace(元空间)Heap(堆)
  • 元空间(元数据区、Metaspace)

    元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。

    元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

    元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

    • 类元信息(Class)

      类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表

      常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

    • 运行时常量池(Runtime Constant Pool)

      运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些

      运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

  • 直接内存(Direct Memory)

    直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

    常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。

    分配、回收成本较高,但读写性能高。

    直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。


Java 程序内存 = JVM 内存 + 本地内存

  • JVM 内存(JVM 虚拟机数据区)

    Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。

    JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM

  • 本地内存(元空间 + 直接内存)

    对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。

    本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。

    虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM


堆外内存

  • 直接内存

    直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

    可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样

    内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;

  • 线程堆栈

    可通过 -Xss 调整大小

    内存不足时抛出

    • StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)
    • OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)
  • Socket 缓存区

    每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。

    如果无法分配,可能会抛出 IOException:Too many open files异常

  • JNI 代码

    如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存

  • 虚拟机和垃圾收集器

    虚拟机、垃圾收集器的工作也是要消耗一定数量的内存


JVM 堆及各种 GC 详解

参考:Java 中的新生代、老年代、永久代和各种 GC

结构图(新生代、老年代、永久代)

JVM 中的堆,一般分为三大部分:新生代、老年代、永久代( Java8 中已经被移除)


新生代、MinorGC(Young GC)

新生代

  • 主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。

  • 新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区

    • Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。

      当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

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

    • SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。

    Eden 和 S0,S1 区的比例为 8 : 1 : 1

    幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区

    JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%

  • 当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。


MinorGC

  • MinorGC 的过程(采用复制算法)

    1. 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)
    2. 同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区)
    3. 然后,清空 Eden 和 ServicorFrom 中的对象;
    4. 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。
  • Minor GC 触发机制:

    当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)

  • 对象年龄(Age)计数器

    虚拟机给每个对象定义了一个对象年龄(Age)计数器。

    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。

    对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

    对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。


老年代、MajorGC(Old GC)

老年代

  • 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。

  • 在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

    当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

  • MajorGC 采用标记-清除算法

    1. 首先扫描一次所有老年代,标记出存活的对象
    2. 然后回收没有标记的对象。

    MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。

    MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。

  • 当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。


永久代、元数据区(元空间)、常量池

永久代(PermGen)

  • 是 JDK7 及之前, HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,其他虚拟机如 JRockit(Oracle)、J9(IBM) 有方法区 ,但是没有永久代。

    在 JDK1.8 已经被移除,取而代之的是元数据区(元空间)

  • 内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。

    和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。

    所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

元数据区(元空间、Metaspace)

  • 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。

  • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

    默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

    • -XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整

      如果释放了大量的空间,就适当降低该值;

      如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。

    • -XX:MaxMetaspaceSize(最大空间)默认是没有限制的。

    除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

    • -XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
    • -XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

    类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。


元空间替换永久代的原因分析:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。

  • 通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

    当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  • Oracle 可能会将HotSpot 与 JRockit 合二为一。


类常量池、运行时常量池、字符串常量池

  • 类常量池

    在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池

    主要存放字面量(字面量一部分便是文本字符)和符号引用

  • 运行时常量池

    在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池

    (文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)

    • 在 JDK6,运行时常量池 存在于 方法区
    • 在 JDK7,运行时常量池 存在于 Java 堆
  • 字符串常量池

    存储的是字符串对象的引用,而不是字符串本身

    字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)

拓展

  • 字面量

    java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:

    int a=1; // 这个1便是字面量
    String b="iloveu"; // iloveu便是字面量
    
  • 符号引用

    由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址。

    比如:com.javabc.Solution 类中引用了 com.javabc.Quest,那么 com.javabc.Quest 作为符号引用就会存到类常量池,等类加载完后,就可以拿着这个引用去元空间找此类的内存地址


Full GC 、Major GC(Old GC)

Minor GC、Major GC、Full GC 的区别

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC ):只是老年代的垃圾收集
  • 整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集

Full GC 触发机制:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
  • 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

堆空间分成不同区的原因

  • 堆空间分为新生代和老年代的原因

    根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

  • 新生代分为了 eden、Survivor 区的原因

    为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。

    如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。

    将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。

  • 新生代的 Survivor 区又分为 s0 和 s1 区的原因:

    分两个区的好处就是解决内存碎片化。

    为什么一个 Survivor 区不行?

    假设现在只有一个survivor区,模拟一下流程:

    新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

  • GC 优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。


堆不是对象存储的唯一选择(逃逸分析)

如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。

逃逸分析概述: 一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。

GC(垃圾回收)

System.gc()

  • GC(Garbage Collection)垃圾回收。

    System.gc() 是用 Java,C#和许多其他流行的高级编程语言提供的API。

    当它被调用时,它将尽最大努力从内存中清除垃圾(即未被引用的对象)。

  • 在默认情况下,通过 System.gc() 或者Runtime.getRuntime().gc() 的调用,会显式触发 Full GC(完整的 GC 事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

  • 在 GC 完成之前,整个 JVM 将冻结(即正在运行的所有服务将被暂停),通常完整的 GC 需要很长时间才能完成。

    因此在不合适的时间运行 GC,将导致不良的用户体验,甚至是崩溃。

    JVM 具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发 GC 的计算。当显式调用 System.gc() 调用时,所有这些计算都将被抛掉。

  • system.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用**(不能确保立即生效)**

  • System.gc() 可以从应用程序堆栈的各个部分调用:

    • 开发的应用程序可以显式的调用 System.gc() 方法
    • System.gc() 也可以由第三方库,框架触发
    • 可以由外部工具(如 VisualVM)通过使用 JMX 触发
    • 如果应用程序使用了RMI,RMI会定期调用 System.gc()
  • GC 操作应该由 JVM 自行控制,在绝大部分的场景都不建议程序员手动写代码显式进行 System.gc() 操作。

    但是也不排除其中个别例外:

    在开发多个微服务时,每个服务都有多个备份节点。在非业务高峰时段,可以从微服务-负载均衡的节点池中取出其中一个 JVM 实例。然后通过该 JVM 上的 JMX 显式触发 System.gc() 调用,一旦 GC 事件完成并且从内存中清除了垃圾,将该 JVM 放回到微服务-负载均衡的节点池中。

    当然这个过程需要很好的微服务管理及服务发布机制配合,这样既能保证 JVM 垃圾内存的有效清理,又不影响业务的正常运行。


如何检测应用程序正在进行 System.gc()?

  • System.gc() 可以从多个渠道进行的调用,而不仅仅是从应用程序源代码进行的调用。因此,搜索应用程序代码System.gc() 字符串,不足以知道 GC 是否正在被调用。

  • 通过 GC 日志可以检测应用程序是否正在进行垃圾回收

    // java 8 启动 GC 日志:-XX:+PrintGCDetails -Xloggc:<gc-log-file-path>
    -XX:+PrintGCDetails -Xloggc:/opt/tmp/myapp-gc.log
    
    // java 9 启动 GC 日志:-Xlog:gc*:file=<gc-log-file-path>
    -Xlog:gc*:file=/opt/tmp/myapp-gc.log
    
  • 建议始终在所有生产服务器中始终启用 GC 日志,因为它有助于排除故障并优化应用程序性能。

    启用GC日志只会增加微不足道的开销。

    还可以将 GC 日志上传到垃圾收集日志分析器工具,例如GCeasy,HP JMeter等。这些工具将生成丰富的垃圾收集分析报告。


如何禁止GC显式调用或调整调用GC的频率?

如果就是想避免程序员显式调用GC,避免不成熟的程序员在不合适时间调用GC,避免人为造成的GC崩溃,可以通过如下方法:

  • 搜索和替换

    在代码库中搜索 System.gc() 和 Runtime.getRuntime().gc()

    如果看到匹配项,则将其删除。但是这种方法无法避免第三方库、框架或通过外部源进行调用。

  • 通过JVM参数强制禁止

    通过传递 JVM 参数 -XX:+DisableExplicitGC 来强制禁止显式调用。

    这种方式强制、有效,应用程序内的任何 GC 显式代码调用 System.gc() 都将被禁止生效。

    JVM 自身的 GC 策略不受此参数影响,只禁止人为的触发 GC。

  • RMI

    如果应用程序正在使用 RMI,则可以控制 GC 调用的频率 。启动应用程序时,可以使用以下JVM参数配置该频率:

    • -Dsun.rmi.dgc.server.gcInterval=n
    • -Dsun.rmi.dgc.client.gcInterval=n

    这些属性的默认值在

    • JDK 1.4.2 和 5.0 是 60000毫秒(即60秒)
    • JDK 6 和更高版本是 3600000毫秒(即60分钟)

    如果应用主机内存资源非常富余,可以将这些属性设置为很高的值,以便可以将GC带来的对应用程序的影响最小化。这也是应用程序性能优化的一种方式之一。


STW(Stop The World)事件

stop-the-world,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。

可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少 STW 的发生。

STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 Stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

开发中除非特殊情况,不要用 system.gc() 进行手动 GC,会导致 stop-the-world 的发生。


GC 常用算法

  • 分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)

    它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。

    新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。

    老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。

  • 标记-清除算法

    每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。

    标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

    优点是可以避免内存碎片。

  • 标记-压缩(标记-整理)算法

    标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。

    不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。

  • 复制算法

    复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。

    当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址

    此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

    复制算法不会产生内存碎片。


直接内存(Direct Memory)详解

参考:JVM 直接内存

文件的读写过程

  • 传统 io 方式

    Java 本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)。在这里 CPU 的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】。

    内存这边也会有一些相关的操作,当切换到内核态以后,就可以由 CPU 的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,会在操作系统内存中划出一块儿缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取);系统的缓冲区是不能被 Java 代码直接操作的,所以 Java 会先在堆内存中分配一块儿 Java 的缓冲区,即代码中的 new byte[大小],Java 的代码要能访问到刚才读取的那个流中的数据,必须先从系统缓冲区的数据间接读入到 Java 缓冲区,然后 CPU 的状态又切换到用户态了,然后再去调用 Java 的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。

    可以发现,由于有两块儿内存,两块儿缓冲区,即系统内存和 Java 堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为 Java 代码不能直接访问系统缓冲区,所以需要先把系统缓冲区数据读入到 Java 缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高。

  • directBuffer(直接缓存区)方式

    当 ByteBuffer 调用 allocateDirect 方法后,操作系统这边划出一块缓冲区,即 direct memory(直接内存),这段区域与之前不一样的地方在于这个操作系统划出来的内存可以被 Java 代码直接访问,即系统可以访问它,Java 代码也可以访问它,它是 java 代码和系统共享的一段内存区域,这就是直接内存。

    磁盘文件读到直接内存后,Java 代码直接访问直接内存,比传统 io 方式少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。

    这也是直接内存带来的好处,适合做较大文件拷贝的这种 io 操作。

演示案例(运行并比较时间后可以发现,尤其是读写大文件时使用 ByteBuffer 的读写性能非常高):

// 演示ByteBuffer作用
public class Demo 
	static final String FORM = "D:\\\\asd\\\\asd.mp4"; // 选比较大的文件,比如200多兆
	static final String TO = "D:\\\\asd.mp4";
	static final int _1Mb = 1024 * 1024;

	public static void main(String[] args) 
        // io 用时:3187.41008(大概用了3秒),多跑几遍,多比较,跑一次不算。
		io();
        // directBuffer 用时:951.114625(不到1秒)
		derectBuffer();
	

	private static void deirectBuffer() 
		long start = System.nanoTime();
		try (FileChannel from = new FileInputStream(FROM).getChannel();
			FileChannel to = new FileOutputStream(TO).getChannel();
		) 
			ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb); // 读写的缓冲区(分配一块儿直接内存)
			while (true) 
				int len = from.read(bb);
				if (len == -1) 
					break;
				
				bb.flip();
				to.write(bb);
				bb.clear();
			

		catch (IOException e) 
			e.printStackTrace();
		
		long end = System.nanoTime();
		print("directBuffer用时:" + (end - start) / 1000_000.0);
	

	// 用传统的io方式做文件的读写
	private static void io() 
		long start = System.nanoTime();
		try ( // 网友1:写到try()括号里就不用手动close了
			FileInputStream from = new FileInputStream(FROM);
			FileOutPutStream to = new FileOutputStream(TO);
		) 
			byte[] buf = new byte[_1Mb];// byte数组缓冲区(与上面的读写缓冲区设置大小一致,比较时公平)
			while (true) 
				int len = from.read(buf);// 用输入流读
				if (len == -1) 
					break;
				
				to.write(buf, 0, len);// 用输出流写
			
		catch(IOException e) 
			e.printStackTrace();
		
		long end = System.nanoTime();
		print("io用时:" + (end - start) / 1000_000.0);
	


直接内存的分配和回收

直接内存的分配和释放是 Java 通过 UnSafe 对象来管理的,并且回收需要主动调用 freeMemory() 方法,不直接受 JVM 内存回收管理。

ByteBuffer 底层分配和释放直接内存的大概情况

  • ByteBuffer 对象被创建时,调用 Unsafe 对象的 allocateMemory(_1Gb) 方法分配直接内存,返回 long base,即内存地址

  • ByteBuffer 对象被销毁时,调用 unsafe 对象的 freeMemory(base) 方法释放直接内存。

    ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来检测 ByteBuffer 对象,一旦 ByteBuffer 对象被(Java)垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory() 方法来释放直接内存。

演示案例(演示直接内存溢出)

  • 运行后,输出 36

    即循环 36 次(一次 100 兆,循环 36 次也算 3 个 G 多了)后,爆出直接内存溢出异常:

    Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory

// 演示直接内存溢出
public class Demo 
	static int _100Mb = 1024 * 1024 * 100;
	
	public static void main(String[] args) 
		List<ByteBuffer> list = new ArrayList<>();
		int i = 0; 
		try 
			while (true) 
				ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);// 每次分配100兆内存
				list.add(byteBuffer);// 把这玩意放到List中,一直循环
				i++;
			
		finally 
			print(i);
		
	


使用 System.gc() 间接进行直接内存的回收可能存在的问题

  • 代码案例

    public class Demo 
    	static int _1Gb = 1024 * 1024 * 1024;
    	public static void main(String[] args) throws IOException 
    		ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
    		print("分配完毕");
    		print("开始释放");
    		byteBuffer = null;
    		System.gc(); // 显式的垃圾回收
    	
    
    
  • System.gc() 触发的是一次 Full GC,是比较影响性能的垃圾回收 ,不光要回收新生代,还要回收老年代,所以它造成的程序暂停时间比较长。

  • 为了防止一些程序员不小心在代码里经常写 System.gc() 以触发显式的垃圾回收,做一些 JVM 调优时经常会加上 JVM 虚拟机参数 -XX:+DisableExplicitGC,禁用这种显式的垃圾回收,也就是让 System.gc() 代码无效。但是加上这个虚拟机参数后,可能会间接影响到直接内存的回收机制。

    • 没加虚拟机参数的话,由于 byteBuffer 被 null 了,显式触发 Java 垃圾回收,byteBuffer 的堆内存被回收时,会调用 unsafe 对象的 freeMemory(base) 方法释放直接内存,所以也导致了直接内存也被释放掉。

    • 加虚拟机参数之后,System.gc() 代码失效,虽然 byteBuffer 被 null 了,但如果内存比较充足,那么它还会暂时存活着,其创建的直接内存(ByteBuffer.allocateDirect(-1Gb))也会在 byteBuffer 的堆内存被 JVM 自动进行垃圾回收前一直存在着。

    所以禁用 System.gc() 之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为不能用显式的方法回收掉Bytebuffer,所以 ByteBuffer 只能等到 JVM 自动进行垃圾回收时,才会被清理,从而它所对应的那块儿直接内存在此之前也会一直不会被释放掉,这就会造成直接内存可能占用较大,长时间得不到释放这样一个现象。

    所以使用直接内存的情况比较多,由程序员直接手动的管理直接内存时,推荐用 Unsafe 的相关方法,直接调用 Unsafe 对象的 freeMemory() 方法来释放直接内存。


JVM 的性能调优

调优参数

配置方式

  • java [options] MainClass [arguments]
  • options :JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。
  • 参数命名: 常见为 -参数名
  • 参数赋值: 常见为 -参数名=参数值 或 -参数名:参数值

内存参数: