天天看点

JVM知识详解

JVM

1、JVM内存结构

1.1、JVM体系概述
JVM知识详解
1.2、常见的垃圾回收算法
  • 引用计数法

    顾名思义,此种算法会在每一个对象上记录这个对象被引用的次数,只要有任何一个对象引用了次对象,这个对象的计数器就+1,取消对这个对象的引用时,计数器就-1。任何一个时刻,如果该对象的计数器为0,那么这个对象就是可以回收的。

    JVM知识详解

    优点:

    引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

    缺点:

    无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。

  • 复制算法

    Java堆从GC的角度还可以细分为: 新生代(Eden 区、From Survivor 区和To Survivor 区)和老年代。

    JVM知识详解

    MinorGC的过程(复制->清空->互换):

    a. Eden、SurvivorFrom复制到SurvivorTo,年龄+1

    首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1。

    b. 清空eden-SurvivorErom

    然后,清空Eden和Survivor From中的对象,也即复制之后有交换,谁空谁是To。

    c. Survivor To和 Survivor From互换

    最后,Survivor To和Survivor From互换,原SurvivorTo成为下一次GC时的Survivor From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由ⅣM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。

    复制算法用于在新生代垃圾回收

    它的主要缺点有两个:

    ​ (1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;

    ​ (2)空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)

  • 标记清除

    算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。

JVM知识详解

​ 标记-清除(Mark-Sweep)算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。

​ 标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续使用,在标记完成后统一回收 掉所有被标记的对象 ,标记指示对象还要继续使用的,那就原地不动留下。

​ 缺点:

1. 标记与清除效率低;
           

​ 2. 清除之后内存会产生大量碎片;

  • 标记整理
JVM知识详解

​ 标记整理法在标记清除基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理,通过这种方式来进行减少碎片的目的。

​ (java中老年代使用的就是标记压缩法) 

2、CG roots

2.1、什么是垃圾?

简单的说就是内存中已经不再被使用到的空间就是垃圾。

2.2、如何判断一个对象是否可以被回收?
  • 引用计数法

    顾名思义,此种算法会在每一个对象上记录这个对象被引用的次数,只要有任何一个对象引用了次对象,这个对象的计数器就+1,取消对这个对象的引用时,计数器就-1。任何一个时刻,如果该对象的计数器为0,那么这个对象就是可以回收的。

    优点:

    引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

    缺点:

    无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。

  • 枚举根节点做可达性分析(根搜索路径) 又叫可达性分析算法

    通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,通过引用关系遍历对象,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。

2.3、Java中可以作为GC Roots的对象?
  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

3、JVM调优及参数配置

3.1、JVM的参数类型

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

JVM的参数类型:

  • 标配参数
    • -version

      java -version

    • -help

      java -heip

    • 指的是那些基本上不会改变的参数
  • X参数(了解)
    • -Xint:解释执行
    • -Xcomp:第一次使用就编译成本地代码
    • -Xmixed:混合模式
  • XX参数(下一节)
    JVM知识详解
3.2、JVM的XX参数

布尔类型:

公式:-XX:+ 或者 - 某个属性值(+表示开启,-表示关闭)

键值对类型:

公式:

-XX:属性key=值value

。比如

-XX:Metaspace=128m

-XX:MaxTenuringThreshold=15

如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?

jps -l 查看一个正在运行中的java程序,得到Java程序号。

JVM知识详解

jinfo -flag PrintGCDetails Java进程号 查看它的某个jvm参数(如PrintGCDetails )是否开启。

JVM知识详解

jinfo -flags Java进程号 查看它的所有jvm参数

JVM知识详解
3.3、VM Xms/Xmx参数

两个经典参数:

  • -Xms等价于-XX:InitialHeapSize,初始大小内存,默认物理内存1/64
JVM知识详解
  • -Xmx等价于-XX:MaxHeapSize,最大分配内存,默认为物理内存1/4
    JVM知识详解
3.4、JVM查看初始默认值

查看初始默认参数值

-XX:+PrintFlagsInitial

命令行: java -XX:+PrintFlagsInitial

C:\Users\abc>java -XX:+PrintFlagsInitial
[Global flags]
      int ActiveProcessorCount                     = -1                                        {product} {default}
    uintx AdaptiveSizeDecrementScaleFactor         = 4                                         {product} {default}
    uintx AdaptiveSizeMajorGCDecayTimeScale        = 10                                        {product} {default}
    uintx AdaptiveSizePolicyCollectionCostMargin   = 50                                        {product} {default}
    uintx AdaptiveSizePolicyInitializingSteps      = 20                                        {product} {default}
    uintx AdaptiveSizePolicyOutputInterval         = 0                                         {product} {default}
    uintx AdaptiveSizePolicyWeight                 = 10                                        {product} {default}
... 

           

查看修改更新参数值

-XX:+PrintFlagsFinal

公式:

java -XX:+PrintFlagsFinal

C:\Users\abc>java -XX:+PrintFlagsFinal
...
   size_t HeapBaseMinAddress                       = 2147483648                             {pd product} {default}
     bool HeapDumpAfterFullGC                      = false                                  {manageable} {default}
     bool HeapDumpBeforeFullGC                     = false                                  {manageable} {default}
     bool HeapDumpOnOutOfMemoryError               = false                                  {manageable} {default}
    ccstr HeapDumpPath                             =                                        {manageable} {default}
    uintx HeapFirstMaximumCompactionCount          = 3                                         {product} {default}
    uintx HeapMaximumCompactionInterval            = 20                                        {product} {default}
    uintx HeapSearchSteps                          = 3                                         {product} {default}
   size_t HeapSizePerGCThread                      = 43620760                                  {product} {default}
     bool IgnoreEmptyClassPaths                    = false                                     {product} {default}
     bool IgnoreUnrecognizedVMOptions              = false                                     {product} {default}
    uintx IncreaseFirstTierCompileThresholdAt      = 50                                        {product} {default}
     bool IncrementalInline                        = true                                   {C2 product} {default}
   size_t InitialBootClassLoaderMetaspaceSize      = 4194304                                   {product} {default}
    uintx InitialCodeCacheSize                     = 2555904                                {pd product} {default}
   size_t InitialHeapSize                          := 268435456                                 {product} {ergonomic}
...

           

=表示默认,:=表示修改过的。

打印命令行参数

-XX:+PrintCommandLineFlags

命令行:java -XX:+PrintCommandLineFlags -version

C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)

           

查看虚拟机参数文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html

3.5、常用的JVM基础参数

-Xmx/-Xms:

  • -Xmx:初始的堆内存大小,默认物理内存1/64
  • -Xms:最大的堆内存大小,默认物理内存1/4

-Xss:

等价于

-XX:ThresholdStackSize

。 单个线程栈的大小,系统默认值是0,代表使用的是默认大小。

他的大小是根据操作系统的不同,有不同的值。比如64位的Linux系统是1024K,window的默认值取决于虚拟内存。

-Xmn:

设置年轻代大小,一般不调

-XX:MetaspaceSize:

设置元空间大小。命令行:-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m

JDK 1.8以及之后将最初的永久代取消了,由元空间取代。

元空间(Java8)与永久代(Java7)之间最大的区别在于:

永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

-XX:+PrintGCDetails:

输出详细GC收集日志信息。

设置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails 运行程序

import java.util.concurrent.TimeUnit;

public class PrintGCDetailsDemo {

	
	public static void main(String[] args) throws InterruptedException {
		byte[] byteArray = new byte[10 * 1024 * 1024];
		
		TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
	}
}

           

输出结果

[GC (Allocation Failure) [PSYoungGen: 1984K->488K(2560K)] 1984K->759K(9728K), 0.0031655 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->504K(2560K)] 759K->783K(9728K), 0.0009001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 279K->719K(7168K)] 783K->719K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0076688 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 719K->719K(9728K), 0.0003738 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 719K->701K(7168K)] 719K->701K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0087387 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 57K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0e788,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 701K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6af4e8,0x00000000ffd00000)
 Metaspace       used 3315K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.example.test.test.Test.main(Test.java:14)
           

对回收类型GC的解释

JVM知识详解

[GC (Allocation Failure) [PSYoungGen: 1984K->488K(2560K)] 1984K->759K(9728K), 0.0031655 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

对回收类型FULL GC的解释

JVM知识详解

[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 279K->719K(7168K)] 783K->719K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0076688 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

-XX:SurvivorRatio:

新生代中,

Eden

区和两个

Survivor

区的比例,默认是

8:1:1

。通过

-XX:SurvivorRatio=4

改成

4:1:1

JVM知识详解

默认的占比

-XX:NewRatio:

老生代和新年代的比列,默认是2,即老年代占2,新生代占1。如果改成

-XX:NewRatio=4

,则老年代占4,新生代占1。

-XX:MaxTenuringThreshold:

新生代设置进入老年代的时间,默认是新生代逃过15次GC后,进入老年代。如果改成0,那么对象不会在新生代分配,直接进入老年代。

java8设置的范围只能在0-15内

4、强、软、弱、虚引用

Reference类以及继承派生的类

JVM知识详解
4.1、强引用
// 这样定义的默认就是强应用
Object obj1 = new Object();
           

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

4.2、软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。对于只有软引用的对象来说,

当系统内存充足时它不会被回收,当系统内存不足时它会被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

4.3、弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存。

4.4、虚引用

虚引用需要java.lang.ref.PhantomReference类来实现。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态

设置虚引用关联的唯一目的,就是在这个对象要被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

4.5、ReferenceQueue引用队列

引用的对象被回收之前,可以用队列保存,然后做一些特定的操作。

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class ReferenceQueueDemo {
    public static void main(String[] args) {
        Object o1 = new Object();

        // 创建引用队列
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

        // 创建一个弱引用
        WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

        System.out.println("==================");
        
        o1 = null;
        System.gc();
        System.out.println("执行GC操作");

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

    }
}

           

输出结果

[email protected]
[email protected]
null
==================
执行GC操作
null
null
[email protected]

           
4.6、GCRoots和四大引用小总结
JVM知识详解

5、OOM(OutOfMemoryError)

5.1、StackOverflowError

​ 栈溢出

public class StackOverflowErrorDemo {

	public static void main(String[] args) {
		main(args);
	}
}
============================================================================

Exception in thread "main" java.lang.StackOverflowError
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	...
           
5.2、java heap space

​ 堆空间不足

public class OOMEJavaHeapSpaceDemo {

	/**
	 * 
	 * -Xms10m -Xmx10m
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		byte[] array = new byte[80 * 1024 * 1024];
	}

}
=====================================================================================

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lun.jvm.OOMEJavaHeapSpaceDemo.main(OOMEJavaHeapSpaceDemo.java:6)

           
5.3、GC overhead limit exceeeded

超出GC开销限制,GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。

import java.util.ArrayList;
import java.util.List;

public class OOMEGCOverheadLimitExceededDemo {

    /**
     * 
     * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
     * 
     * @param args
     */
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while(true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Exception e) {
            System.out.println("***************i:" + i);
            e.printStackTrace();
            throw e;
        }
    }

}

=============================================================================
    [GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1658K(9728K), 0.0033090 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2323K->489K(2560K)] 3483K->3305K(9728K), 0.0020911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2537K->496K(2560K)] 5353K->4864K(9728K), 0.0025591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2410K->512K(2560K)] 6779K->6872K(9728K), 0.0058689 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6360K->6694K(7168K)] 6872K->6694K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0894928 secs] [Times: user=0.42 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->1421K(2560K)] [ParOldGen: 6694K->6902K(7168K)] 8742K->8324K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0514932 secs] [Times: user=0.34 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6902K->6902K(7168K)] 8950K->8950K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0381615 secs] [Times: user=0.13 sys=0.00, real=0.04 secs] 
...
***************i:147041
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7050K->7048K(7168K)] 9098K->9096K(9728K), [Metaspace: 2670K->2670K(1056768K)], 0.0371397 secs] [Times: user=0.22 sys=0.00, real=0.04 secs] 
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) 	at java.lang.Integer.toString(Integer.java:401)
[PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7051K->7050K(7168K)] 9099K->9097K(9728K), [Metaspace: 2676K->2676K(1056768K)], 0.0434184 secs] [Times: user=0.38 sys=0.00, real=0.04 secs] 
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7054K->513K(7168K)] 9102K->513K(9728K), [Metaspace: 2677K->2677K(1056768K)], 0.0056578 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Heap
 PSYoungGen      total 2560K, used 46K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0bb90,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 513K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6807f0,0x00000000ffd00000)
 Metaspace       used 2683K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

           
5.4、Direct buffer memory

导致原因:

写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避兔了在Java堆和Native堆中来回复制数据。

  • ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
  • ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

但如果不断分配本地内存,堆内存很少使用,那么JV就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。

元空间存放的信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

public class OOMEDirectBufferMemoryDemo {

	/**
	 * -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
	 * 
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		System.out.println(String.format("配置的maxDirectMemory: %.2f MB",// 
				sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
		
		TimeUnit.SECONDS.sleep(3);
		
		ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
	}	
}
============================================================================
 [GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Exception in thread "main" Heap
 PSYoungGen      total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
 Metaspace       used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
  class space    used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)


           
5.5、unable to create new native thread

不能够创建更多的新的线程了,也就是说创建线程的上限达到了。高并发请求服务器时,经常会出现异常

java.lang.OutOfMemoryError:unable to create new native thread

,准确说该native thread异常与对应的平台有关。

导致原因:

  • 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
  • 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

解决方法:

  1. 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
  2. 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改Linux服务器配置,扩大linux默认限制
public class OOMEUnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("************** i = " + i);
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}
===========================================================================
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread


           
5.6、unable to create new native thread上限调整

查看系统线程限制数目:ulimit -u

修改系统线程限制数目: vim /etc/security/limits.d/90-nproc.conf

打开后发现除了root,其他账户都限制在1024个

JVM知识详解
5.7、Metaspace

永久代(Java8后被原空向Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们借助CGLib直接操作字节码运行时不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.10</version>
</dependency>


==================================================

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class OOMEMetaspaceDemo {
    // 静态类
    static class OOMObject {}

    /**
     * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     * 
     * @param args
     */
    public static void main(final String[] args) {
        // 模拟计数多少次以后发生异常
        int i =0;
        try {
            while (true) {
                i++;
                // 使用Spring的动态字节码技术
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("发生异常的次数:" + i);
            e.printStackTrace();
        } finally {

        }

    }
}
===================================================================

发生异常的次数:569
java.lang.OutOfMemoryError: Metaspace
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)


           

6、垃圾回收器

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

6.1、垃圾回收器的种类
  • Serial 串行垃级回收器

    它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。

  • Parallel 并行垃圾回收器

    多个垃圾收集线程并行工作,此时用户线程也是阻塞的,进行垃圾收集时,主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短。

  • CMS 并发垃圾回收器

    用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程。

  • G1垃圾回收器

    G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

6.2、查看默认的垃圾收集器

​ java -XX:+PrintCommandLineFlags -version

JVM知识详解
6.3、JVM的垃圾收集器

Java中一共有7大垃圾收集器用于垃圾回收,

年轻代GC

  • UserSerialGC:串行垃圾收集器
  • UserParallelGC:并行垃圾收集器
  • UseParNewGC:年轻代的并行垃圾回收

老年代

  • UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
  • UseParallelOldGC:老年代的并行垃圾回收器
  • UseConcMarkSweepGC:(CMS)并发标记清除

UseG1GC:G1垃圾收集器不区分新生代和老年代分区域进行

| 参数 新生代垃圾收集器 新生代算法 老年代垃圾收集器 老年代算法XX:+UseSerialGC SerialGC 复制 SerialOldGC 标记整理

-XX:+UseParNewGC ParNew 复制 SerialOldGC 标记整理

-XX:+UseParallelGC Parallel [Scavenge] 复制 Parallel Old 标记整理

-XX:+UseConcMarkSweepGC ParNew 复制 CMS + Serial Old的收集器组合 标记清除

-XX:+UseG1GC 不区分新生代和老年代 G1整体上采用标记整理算法 每一个区域使用局部复制
6.4、SerialGC

一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

JVM知识详解

STW: Stop The World 指的是暂停用户线程

JVM参数是:-XX:+UseSerialGC ,命令作用的是新生代,老年代会自动匹配相对应的垃圾回收器。

开启后会使用:Serial(Young区用) + Serial Old(Old区用)的收集器组合,新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

6.5、ParNewGC

使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World暂停其他所有的工作线程直到它收集结束。

JVM知识详解

ParNew收集器其实就是Serial收集器新生代的并行多线程版本,,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。但是执行的效率要比Serial的要高效。

JVM参数:

  • -XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代。
  • -XX:ParallelGCThreads限制线程数量,默认开启和CPU数目相同的线程数。

开启上述参数后,会使用:ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法

6.6、ParallelGC

Parallel / Parallel Scavenge

JVM知识详解

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。

优点:

  • 高吞吐量,高吞吐量意味着高效利用CPU的时间
  • 自适应调节策略,自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别,自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。

JVM参数:

  • -XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。
  • -XX:ParallelGCThreads=数字N 表示启动多少个GC线程 cpu>8 N= 5/8 cpu<8 N=实际个数

开启该参数后:会使用:ParallelGC(Young区)+ ParallelOldGC(Old区)的收集器组合, 新生代使用复制算法,老年代使用标记-整理算法。

6.7、ParallelOldGC

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。Parallel Old 是为了在年老代同样提供吞吐量优先的垃圾收集器

JVM常用参数:-XX:+UseParallelOldGC使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代 Parallel Old。 新生代使用复制算法,老年代使用标记-整理算法。

6.8、SerialOldGC

Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法。现在基本已经不使用了。

6.9、ConcMarkSweepGC

又叫做CMS,CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

JVM知识详解

Concurrent Mark Sweep并发标记清除,并发收集,低停顿,并发指的是与用户线程一起执行

开启该收集器的JVM参数:

  • -XX:+UseConcMarkSweepGC 老年代使用的
  • -XX:CMSFullGCsBeForeCompaction (默认O,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

开启该参数后,使用ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。

执行过程:

  • 初始标记(CMS initial mark) - 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  • 并发标记(CMS concurrent mark)和用户线程一起 - 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
  • 重新标记(CMS remark)- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
  • 并发清除(CMS concurrent sweep) - 清除GCRoots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
JVM知识详解

优点:并发收集低停顿,响应速度快,用户体验好

缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片。

由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。

标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认O,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

6.10、如何选择垃圾收集器

组合的选择

  • 单CPU或者小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大的吞吐量,如后台计算型应用
    • -XX:+UseParallelGC(这两个相互激活)
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停顿时间,需要快速响应如互联网应用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC
JVM知识详解
6.11、G1

G1收集器与之前垃圾收集器的一个显著区别就是——之前收集器都有三个区域,新、老两代和元空间。

而G1收集器只有G1区和元空间。而G1区,不像之前的收集器,分为新、老两代,而是一个一个Region,每个Region既可能包含新生代,也可能包含老年代。

G1`收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。

VM参数:

  • -XX:+UseG1GC G1整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。
  • -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

底层原理:

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数

-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32 M B ∗ 2048 = 65536 M B = 64 G 32MB*2048=65536MB=64G32MB∗2048=65536MB=64G内存。

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。

在G1中,还有一种特殊的区域,叫Humongous区域。

如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

G1收集器下的Young GC

  • 针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片
  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区。
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。
    JVM知识详解

4步过程:

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行GC Roots Tracing的过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  4. 筛选回收:根据时间来进行价值最大化的回收

特点:

  1. 并行和并发:充分利用多核、多线程CPU,尽量缩短STW。
  2. 分代收集:虽然还保留着新、老两代的概念,但物理上不再隔离,而是融合在Region中。
  3. 空间整合:

    G1

    整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。
  4. 可预测停顿:用户可以指定一个GC停顿时间,

    G1

    收集器会尽量满足。
6.12、G1参数配置
  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45。
  • -XX:ConcGCThreads=n:并发GC使用的线程数。
  • -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。

开发人员仅仅需要声明以下参数即可:

  1. -XX:+UseG1GC
  2. -Xmx32g
  3. -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间

G1和CMS比较

  • G1不会产生内碎片
  • 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

区数据会部会晋升到Old区。

  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

[外链图片转存中…(img-HIW1kfyS-1617879000684)]

4步过程:

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行GC Roots Tracing的过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  4. 筛选回收:根据时间来进行价值最大化的回收

特点:

  1. 并行和并发:充分利用多核、多线程CPU,尽量缩短STW。
  2. 分代收集:虽然还保留着新、老两代的概念,但物理上不再隔离,而是融合在Region中。
  3. 空间整合:

    G1

    整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。
  4. 可预测停顿:用户可以指定一个GC停顿时间,

    G1

    收集器会尽量满足。
6.12、G1参数配置
  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45。
  • -XX:ConcGCThreads=n:并发GC使用的线程数。
  • -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。

开发人员仅仅需要声明以下参数即可:

  1. -XX:+UseG1GC
  2. -Xmx32g
  3. -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间

G1和CMS比较

  • G1不会产生内碎片
  • 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。