JVM系列(二)、Jvm内存结构(上)、堆


1. Java内存结构

Java 内存结构图示

JVM内存结构主要有三大块:堆内存、方法区和栈。

:存放 new 出来的对象和数组,堆是JVM中最大的一块,由年轻代和老年代组成,而年轻代内存又被分为三部分:Eden空间、Form Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1 的比例来分配。

方法区:存储已经被虚拟机加载的类的信息、常量、静态变量、编译后的代码等数据,是线程共享的区域,与Java堆区分,方法区还有一个别名Non-Heap(非堆)。

: 又分为Java虚拟机栈和本地方法栈主要用于方法的执行。

Java8之后方法区发生了一些变化:

  1. 移除了永久代(PermGen),替换为元空间(Metaspace);
  2. 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
  3. 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
  4. 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

再通过一张图来了解如何通过参数来控制各个区域的内存大小:

控制参数:

参数 说明
-Xms 设置堆的最小空间大小。
-Xmx 设置堆的最大空间大小。
-XX:NewSize 设置年轻代最小空间大小。
-XX:MaxNewSize 设置年轻代最大空间大小。
-XX:PermSize 设置永久代最小空间大小。
-XX:MaxPermSize 设置永久代最大空间大小。
-Xss 设置每个线程的堆栈大小。

没有直接设置老年代的参数,但是可以设置堆空间大小和年轻代空间大小两个参数来间接控制。

老年代空间大小 = 堆空间大小 – 年轻代空间大小

从更高的一个纬度再来看JVM和系统调用之间的关系

方法区和堆是所有线程共享的内存区域,而 Java栈、本地方法栈 和 程序计数器是运行在线程私有的内存区域。

再通过另外一张图来观察不同的内存区域抛出不同类型的异常
内存异常抛出类型

2.Java堆(Heap)

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

Java堆是垃圾收集器管理的主要区域,因此有很多时候被称为“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:年轻代和老年代,其中年轻代包括Eden空间、From Survivor空间、To Survivor空间三个部分。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是可连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

我们可以通过 jmap -heap pid 的方式查看堆内存信息


[root@master1 opt]# jmap -heap 5792
Attaching to process ID 5792, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12

using thread-local object allocation.
Mark Sweep Compact GC

Heap Configuration:--堆内存初始化配置
   #-XX:MinHeapFreeRatio设置JVM堆最小空闲比率 
   MinHeapFreeRatio         = 40  
   #-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率
   MaxHeapFreeRatio         = 70   
   #-XX:MaxHeapSize=设置JVM堆的最大大小
   MaxHeapSize              = 482344960 (460.0MB)    
   #-XX:NewSize=设置JVM堆的‘年轻代’的默认大小
   NewSize                  = 10485760 (10.0MB)      
   #-XX:MaxNewSize=设置JVM堆的‘年轻代’的最大大小
   MaxNewSize               = 160759808 (153.3125MB) 
   #-XX:OldSize=设置JVM堆的‘老生代’的大小
   OldSize                  = 20971520 (20.0MB)      
   #-XX:NewRatio=:‘年轻代’和‘老生代’的大小比率
   NewRatio                 = 2    
   #-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
   SurvivorRatio            = 8    
   #-XX:MetaspaceSize=<value>:设置元空间的初始大小 
   MetaspaceSize            = 21807104 (20.796875MB)   
   #-XX:MetaspaceSize=<value>:设置元空间的最大大小
   MaxMetaspaceSize         = 17592186044415 MB       
   #-XX:CompressedClassSpaceSize=<value>:设置元空间中Klass Metaspace的大小
   CompressedClassSpaceSize = 1073741824 (1024.0MB)   
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
   #年轻代区内存分布,包含伊甸园区+1个Survivor区
   New Generation (Eden + 1 Survivor Space):  
   capacity = 9502720 (9.0625MB) 
   used = 3302968 (3.1499557495117188MB)
   free = 6199752 (5.912544250488281MB)
   34.758132408405174% used
Eden Space: #伊甸区
   capacity = 8454144 (8.0625MB)
   used   = 2348608 (2.23980712890625MB)
   free   = 6105536 (5.82269287109375MB)
   27.780553536821706% used
From Space:
   capacity = 1048576 (1.0MB)
   used   = 954360 (0.9101486206054688MB)
   free   = 94216 (0.08985137939453125MB)
   91.01486206054688% used
To Space:
   capacity = 1048576 (1.0MB)
   used   = 0 (0.0MB)
   free   = 1048576 (1.0MB)
   0.0% used
tenured generation: # 老年代
   capacity = 20971520 (20.0MB)
   used    = 13093296 (12.486740112304688MB)
   free   = 7878224 (7.5132598876953125MB)
   62.43370056152344% used

14624 interned Strings occupying 1375560 bytes.

根据提供的Java堆内存信息,可以得出以下分析:

  1. 堆内存初始化配置:根据堆内存初始化配置信息,可以看到堆的最小空闲比率(MinHeapFreeRatio)设置为40%,最大空闲比率(MaxHeapFreeRatio)设置为70%,最大堆大小(MaxHeapSize)为482344960字节(460.0MB)。此外,还可以看到年轻代(New Generation)的默认大小(NewSize)为10485760字节(10.0MB),最大大小(MaxNewSize)为160759808字节(153.3MB),老年代(tenured generation)的大小(OldSize)为20971520字节(20.0MB)。
  2. 年轻代内存分布:根据年轻代区内存分布信息,可以看到伊甸园区(Eden Space)的容量为8454144字节(8.0625MB),已使用3302968字节(3.1MB),剩余6105536字节(5.8MB)。Survivor区的容量为1048576字节(1.0MB),已使用954360字节(0.9MB),剩余94216字节(0.1MB)。
  3. 老年代内存分布:老年代的容量为20971520字节(20.0MB),已使用13093296字节(12.4MB),剩余7878224字节(7.5MB)。
  4. 字符串池占用:有14624个字符串池对象,占用1375560字节。

通过这些信息,可以了解到堆内存的配置和使用情况,包括年轻代和老年代的容量、使用情况,以及字符串池的占用情况。这些信息有助于分析内存使用情况和性能调优。

2.1 Java对象分配规则

  • 对象主要分配在新生代的Eden区上
/**
 * @Author haonan.bian
 * @Description 测试初始化类分配在Eden区
 * @Date 2019-12-25 20:36
 * //vm optition 参数 -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
 **/
public class HeapEdenTest {
    private static final int byteSize = 1024*1024;

    public static void main(String[] args) {
        byte[] bytes = new byte[40*byteSize];

    /**
     * Heap
     *  PSYoungGen      total 76288K, used 44892K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
     *   eden space 65536K, 68% used [0x000000076ab00000,0x000000076d6d7250,0x000000076eb00000)
     *   from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
     *   to   space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
     *  ParOldGen       total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
     *   object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000)
     *  Metaspace       used 3073K, capacity 4496K, committed 4864K, reserved 1056768K
     *   class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
     */

    /**
     * Heap
     *  def new generation   total 78656K, used 45157K [0x00000006c0000000, 0x00000006c5550000, 0x0000000715550000)
     *   eden space 69952K,  64% used [0x00000006c0000000, 0x00000006c2c19628, 0x00000006c4450000)
     *   from space 8704K,   0% used [0x00000006c4450000, 0x00000006c4450000, 0x00000006c4cd0000)
     *   to   space 8704K,   0% used [0x00000006c4cd0000, 0x00000006c4cd0000, 0x00000006c5550000)
     *  tenured generation   total 174784K, used 0K [0x0000000715550000, 0x0000000720000000, 0x00000007c0000000)
     *    the space 174784K,   0% used [0x0000000715550000, 0x0000000715550000, 0x0000000715550200, 0x0000000720000000)
     *  Metaspace       used 3073K, capacity 4496K, committed 4864K, reserved 1056768K
     *   class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
     */
    }
}
  • 少数情况下也可能会直接分配在老年代中
    有些大对象会直接分配在老年代中,所谓的大对象是指需要连续内存空间的Java对象,典型的大对象就是那种很长的字符串以及数组。
    虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于阈值的对象直接在老年代中分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制
/**
 * @Author haonan.bian
 * @Description 测试初始化大对象分配到老年代
 * @Date 2019-12-25 20:55
 * //vm optition 参数 -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3M
 **/
public class HeapBigObjectTest {
    private static final int byteSize = 1024*1024;

    public static void main(String[] args) {
        byte[] bytes = new byte[40*byteSize];
    /**
     * Heap
     *  def new generation   total 78656K, used 4197K [0x00000006c0000000, 0x00000006c5550000, 0x0000000715550000)
     *   eden space 69952K,   6% used [0x00000006c0000000, 0x00000006c0419618, 0x00000006c4450000)
     *   from space 8704K,   0% used [0x00000006c4450000, 0x00000006c4450000, 0x00000006c4cd0000)
     *   to   space 8704K,   0% used [0x00000006c4cd0000, 0x00000006c4cd0000, 0x00000006c5550000)
     *  tenured generation   total 174784K, used 40960K [0x0000000715550000, 0x0000000720000000, 0x00000007c0000000)
     *    the space 174784K,  23% used [0x0000000715550000, 0x0000000717d50010, 0x0000000717d50200, 0x0000000720000000)
     *  Metaspace       used 3074K, capacity 4496K, committed 4864K, reserved 1056768K
     *   class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
     */
    }
}

如果启动了本地线程分配缓冲,将按线程有限在TLAB上分配

2.2 GC参数指定垃圾回收

-Xms20M、-Xmx20M、-Xmn10M 这三个参数限制了Java堆大小为20M,不可扩展,其中 10MB分给新生代,剩下的10MB 分配给老年代,
-Xx:SurvivorRatio=8 配置说明新生代中Eden 区与两个Survivor的空间比例是8:1:1

2.3 年轻代

2.3.1 Eden

Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的survivor区域。

2.3.2 Survivor

Survivor Space幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象。Survivor有两个,分别为 To SurvivorFrom Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。
Eden Space 和 Survivor Space 都属于年轻代,年轻代中执行的垃圾回收被称之为Minor GC(因为是对年轻代进行垃圾回收,所以又被称为Young GC),每一次 Young GC 后留下来的对象age加1。
年轻代内存图示

2.4 老年代

老年代 (Old Generation Space),用于存放年轻代中经过多次垃圾回收仍然存活的对象,也有可能是年轻代分配不了内存的大对象会直接进入老年代。经过多次垃圾回收都没有被回收的对象,这些对象的年代已经足够老(old)了,就会放入到老年代。

当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC。

heap区即堆内存,整个堆大小 = 年轻代大小 + 老年代大小。堆内存默认为物理内存的1/64(<1GB);默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以通过MinHeapFreeRatio参数进行调整;默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以通过MaxHeapFreeRatio参数进行调整。

2.5 GC 模式

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  1. Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集新生代的GC
    • Old GC :只收集老年代的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个新生代以及部分老年代的GC。只有G1有这个模式
  2. Full GC:收集整个堆,包括新生代、老年代、永久代(如果存在的话)等所有部分的模式。

2.5.1 Fulll GC

又叫”Major GC”,Full GC 收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
当准备要触发一次Minor GC时,如果发现统计数据说之前Minor GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Minor GC而是转为触发Full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代n,所以不需要事先触发一次单独的Minor GC);或者,如果有永久代的话,要在永久代分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

  • Major GC和Full GC的区别是什么?
    Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说 “Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC。

  • FullGC 触发条件

    1. 调用 System.gc():此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下,它会触发Full GC。因此强烈建议不能使用此方法就不要使用,让虚拟机自己去管理它的内存。可以通过 -XX:DisableExplicitGC 来禁止RMI调用System.gc()
    2. 老年代空间不足:老年代空间不足是常见场景为前面讲的大对象直接进去老年代,长期存活的对象进去老年代等,当执行Full GC 空间仍然不足的时候,就会抛出Java.lang.OutOfMemory。为避免以上原因引起的Full GC,调优时应该做到让对象在Minor GC 阶段被回收,让对象在新生代多存活一段时间以及不要创建过大的对象以及数据。
    3. 空间分配担保失败:使用复制算法的Minor GC 需要老年代的空间做担保,如果出现了HandlePromotionFailure担保失败,则会触发Full GC

2.5.2 Minor GC

又叫 “Young GC”,只收集新生代的GC。

  • Minor GC 触发条件
    当Eden区满时,就会触发MinorGC。注意新生代中有部分存活对象会晋升到老年代,所以Minor GC后老年代n的占用量通常会有所升高。

2.6 逃逸分析和栈上分配

  1. 逃逸分析
    逃逸分析的基本行为就是分析对象动态作用域:
    • 方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸
    • 线程逃逸:当一个对象在方法中被定义后,它可能被外部线程访问到,譬如复制给变量或可以在其他线程中访问的实例变量,称为线程逃逸。
  1. 栈上分配
    栈上分配就是把方法中的变量和对象分配到栈上,方法执行完成后自动销毁,不需要垃圾回收的介入,从而提高系统性能。

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk 1.8 默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析

/**
 * @Author haonan.bian
 * @Description 测试逃逸分析
 * @Date 2019-12-25 21:20
 **/
public class EscapeTest {
    public static Object obj;

    public void variableEscape(){
        obj = new Object(); // 发生逃逸
    }

    public Object methodEscape(){
        return new Object(); //方法逃逸
    }

    public static void alloc(){
        byte [] b = new byte[2];
        b[0] = 1;
    }
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for(int i = 0;i< 10000000;i++){
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start); //开启逃逸 3 , 关闭逃逸 63
    }

    /**
     * 关闭逃逸 可以看到经过多次的young GC
     * [GC (Allocation Failure) [PSYoungGen: 65536K->528K(76288K)] 65536K->536K(251392K), 0.0013581 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
     * [GC (Allocation Failure) [PSYoungGen: 66064K->544K(76288K)] 66072K->560K(251392K), 0.0006352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
     * [GC (Allocation Failure) [PSYoungGen: 66080K->576K(76288K)] 66096K->600K(251392K), 0.0007234 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
     * 67
     * Heap
     *  PSYoungGen      total 76288K, used 44000K [0x000000076ab00000, 0x0000000774000000, 0x00000007c0000000)
     *   eden space 65536K, 66% used [0x000000076ab00000,0x000000076d568008,0x000000076eb00000)
     *   from space 10752K, 5% used [0x000000076eb00000,0x000000076eb90000,0x000000076f580000)
     *   to   space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000)
     *  ParOldGen       total 175104K, used 24K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
     *   object space 175104K, 0% used [0x00000006c0000000,0x00000006c0006000,0x00000006cab00000)
     *  Metaspace       used 3077K, capacity 4500K, committed 4864K, reserved 1056768K
     *   class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
     */
    /**
     * 开启逃逸之后没有了 young GC
     * 4
     * Heap
     *  PSYoungGen      total 76288K, used 7864K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
     *   eden space 65536K, 12% used [0x000000076ab00000,0x000000076b2ae2e0,0x000000076eb00000)
     *   from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
     *   to   space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
     *  ParOldGen       total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
     *   object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000)
     *  Metaspace       used 2998K, capacity 4500K, committed 4864K, reserved 1056768K
     *   class space    used 328K, capacity 388K, committed 512K, reserved 1048576K
     */
}

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
 上一篇
spark-sql Required-field-'filesAdded'-is-unset spark-sql Required-field-'filesAdded'-is-unset
1. 背景使用sparkSQL计算数据向一个已经存在数据的分区中写数据报错 使用版本: Spark2 2.3.2 Hive 3.1.0 错误信息如下: org.apache.spark.sql.AnalysisException: or
2019-12-26
下一篇 
SparkML中关联规则的应用 SparkML中关联规则的应用
1. 概念什么是关联规则?(Association Rules) 关联规则是数据挖掘中的概念, 通过分析数据, 找到数据之间的关联, 电商中经常用来分析购买商品之间的相关性, 例如,”购买尿布的用户 有大概率购买啤酒”, 这就是一个关联规
2019-12-24
  目录