JVM(四)

144 阅读9分钟

一. 堆的内存结构

jvisualvm

image.png

1.堆的核心概念

  • 《Java虚拟机规范》中对java堆的描述:所有的对象实例以及数组都应在运行时分配在堆上。
  • 一个JVM实例只存在一个堆内存(就是new一个对象),Java内存管理的核心区域。
  • java堆区在jvm启动时就被创建,空间大小确定,是jvm管理的最大的一块内存区域。
  • 《Java虚拟机规范》规定,堆可以处于物理上的不连续的内存空间,但在逻辑上应当被视为连续的。
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
  • 数组和对象可能永远不会创建在栈上,栈帧中保存引用,这个引用指向数组或对象在堆中的位置。
  • 垃圾回收的重点区域。
  • 方法结束后,堆中的区域不会立马被回收。

2.堆的内存细分

  • Java7以及之前堆内存逻辑上为:新生区、养老区、永久代。
  • Java8及之后堆内存逻辑上分为:新生去、养老去、元空间。

新生代

  • 新生代占堆空间的1/3,而新生代包括三个部分:1.伊甸园Eden、2.S0幸存者0区、3.S1幸存者1区,这三个部分在新生代中的空间占比分别为Eden:S0:S1=8:1:1

image.png

老年代

  • 老年代占堆空间的2/3,是新生代空间的2倍。

image.png

既然都是堆空间的部分,都是用来存储实例对象,那为什么要分新手代和老年代呢?
这就涉及到了实例对象的创建到销毁,以及垃圾回收机制GC

堆中的GC机制

  • 刚new出来的对象一开始是放在伊甸园里的,但是实例对象不可能一直保存在堆中,当不需要这个对象的时候就会触发GC,将该对象作为垃圾进行回收。
  • GC一般分为两种:1.Minor GC(发生在新生代的GC) 2.Major GC也称Full GC(发生在老年代的GC)。Minor GC只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快;而Major GC or Full GC指发生在老年代的垃圾回收动作,出现了Major GC,经常会伴随至少一次的Minor GC(并不是绝对的)。Major GC的速度一般比Minor GC慢上10倍以上。
  • 流程大概如下:
    • 1.在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。
    • 2.当Eden区满了的时候,minor garbage 被触发,JVM的垃圾回收器将对伊甸园区进行垃圾回收。
    • 3.经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁。
    • 4.在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。需要注意的是,在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象。
    • 5.再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。
    • 6.再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold默认是15),就会被从年轻代Promotion到老年代。
    • 7.随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。
    • 8.上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩(标记-清除或者标记整理)。
    • 9.对象动态年龄判断: 当前方对象的Survivor区域(其中一块区域,放对象的S区),一批从伊甸区过来的对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大的对象,年龄从小到大增加,就可以直接进入老年代了,例如Survivor区域有一批对象,年龄1+年龄2+年龄N的多个年龄对象总和超过了Survivor的50%,此时就会把年龄N(含)以上的对象放入老年代。这个规则是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
    • 10、 堆中存放我们new出来的对象,存放的区域就是Eden区域,但这个区域存满以后就会做minor gc,首先进行STW,然后通过gc roots去查找这些对象是不是有引用关系,通过关系来判断是否是垃圾,如果能够找到引用就不是垃圾,找不到就是垃圾(也叫可达性分析算法),minor gc过程中我们有一部分是垃圾一部分不是垃圾,会将这些垃圾对象清除,然后非垃圾对象会放到第一块survivor区域s0中,并且将对象的分代年龄+1,分代年龄(用来区分对象生命周期的一种标识,比如parallel默认对象分代年龄达到15岁就放到老年代中)。此时一次minor gc结束。第二次minor gc的时候会将Eden区域和s0、s1区域一块扫描(因为第一次的时候s区域是空的),扫描后会有对象进入surviver区域中s0并且分代年龄+1,上一次在s0的对象会放到s1中并且分代年龄+1。进入surviver区域中的对象会在s0到s1之间来回存放如果一直没有被清除,那么等到分代年龄达到15岁的时候放入老年代中。我们可以推断出,老年代做full gc的频率应该远远小于年轻代minor gc,老年代等到存放对象快满的时候会做full gc,扫描老年代中的对象是否是垃圾,如果都不是并且此时还有对象要进入老年代,那此时就会报OOM(内存溢出错误)。在jdk1.8中,垃圾回收器是paraller,在minor gc 和 full gc过程时都会做STW,直到垃圾回收结束
    • 总结:此时如果新生的对象无法在 Eden 区创建(Eden 区无法容纳) 就会触发一次Young GC 此时会将 S0 区与Eden 区的对象一起进行可达性分析,找出活跃的对象,将它复制到 S1 区并且将S0区域和 Eden 区的对象给清空,这样那些不可达的对象进行清除,并且将S0 区 和 S1区交换。

但是这里会产生一个问题,Q:为啥会有两个 Survivor 区?

A: 因为假设设想一下只有一个 Survibor 区 那么就无法实现对于 S0 区的垃圾收集,以及分代年龄的提升。

Major GC

发生在老年代的GC ,基本上发生了一次Major GC 就会发生一次 Minor GC。并且Major GC 的速度往往会比 Minor GC 慢 10 倍。 。

堆中的GC机制

image.png

  • 查看自己堆内存大小
    public static void main(String[] args) {
        long mx = Runtime.getRuntime().maxMemory();//单位是字节
        long ms = Runtime.getRuntime().totalMemory();//单位是字节
        System.out.println("堆内存初始大小为:"+(ms/1024.0/1024.0)+"MB");
        System.out.println("堆内存最大为大小为:"+(mx/1024.0/1024.0)+"MB");

    }
}

  • 堆内存发生OOM,首先设置堆内存大小 -Xms6m -Xmx6m image.png
    public static void main(String[] args) {
        String s="";
        while(true){
            s+="1233333";
       }
    }
}

GC回收日志

image.png 从GC回收日志可以看出,分别触发了GC和Full GC,并且可以看出堆空间的组成充分,分别是
PSYoungGen,ParOldGen和Metaspace,分别是新手代,老年代,元空间
。 什么样的实例会在老年代中。

二. GC Root对象是什么 ?哪些对象可作为GC Root对象

1.概念

GcRoot是一个对象引用链的起点,引出他们指向的下一处节点,再以下一个节点为起点,引出此节点指向的下一个节点。这样通过GC Root串成的一条线就叫引用链,直到所有的节点都遍历完毕,如果相关对象不在任意一个以GC Root为起点的引用链中,那么虚拟机就可以在内存不足的时候,回收这个对象。

2. GC Root对象有哪些

  • 虚拟机栈 -----栈帧中的本地 变量表中引用的对象
  • 本地方法栈 -----即一般说的 Native方法引用的对象
  • 方法区中 类静态属性引用的对象
  • 方法区中 常量引用的对象

3:分析各个GC Root对象实例

3.1 虚拟机栈中引用的对象

如下代码:a是栈帧中的本地变量,当 a= null 时,由于此时a充当了 GC Root对象,a 与原来指向的 实例 new Test() 断开了连接,所以对象会被回收。

    public static  void main(String[] args) {
	Test a = new Test();
	a = null;
    }
}

3.2 本地方法栈中JNI引用的对象

1: 所谓本地方法就是一个 java方法调用非 Java代码的接口,该方法不是有java实现的,可能有 C 或Python等其他语言实现的,java通过 jni 来调用本地方法

2:而本地方法是以库文件形式存放的,通过调用本地的库文件的内部方法,使java可以实现和本地机器的紧密联系。

3: 调用 java方法时,虚拟机会创建一个栈帧并压入java栈,而他调用本地方法时,虚拟机会保持java栈不变,不会爱java栈帧中压入新的栈,虚拟机只是简单地动态连接并直接调用指定的本地方法。

image.png

...
   // 缓存String的class
   jclass jc = (*env)->FindClass(env, STRING_PATH);
}

如上代码所示,当 java 调用以上本地方法时,jc 会被本地方法栈压入栈中, jc 就是我们说的本地方法栈中 JNI 的对象引用,因此只会在此本地方法执行完成后才会被释放。