6、JDK1.6~1.8内存模型演变

图源:bugstack虫洞栈:面经#25

狂神docker笔记 狂神ks1197_jdk

上图就是JDK 1.6、1.7、1.8 的内存模型演变过程,其实这个内存模型就是 JVM 运行时数据区依照 JVM 虚拟机规范的具体实现过程。

在图中各个版本的迭代都是为了更好的适应 CPU 性能提升,最大限度提升的 JVM 运行效率。这些版本的 JVM 内存模型主要有以下差异:

  • JDK 1.6:有永久代,静态变量存放在永久代上。
  • JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
  • JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间 。但字符串常量池仍然存放在堆上。
  • 无方法区了:
  • Method Area 方法区
  • 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
  • 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
  • static ,final ,Class ,常量池~

这也是为啥不同版本编译出来的class文件不能在不同版本的JVM上运行。

7、程序计数器

程序计数器: Program Counter Register:

定义如下:

  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码)
  • 在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
  • 这一块区域没有任何 OutOfMemoryError 定义

举例说明:

定义一段 Java 方法的代码,这段代码是计算圆形的周长。

public static float circumference(float r){
        float pi = 3.14f;
        float area = 2 * pi * r;
        return area;
}

通过反汇编可以得到其具体过程:

狂神docker笔记 狂神ks1197_jvm_02

  • 这些行号每一个都会对应一条需要执行的字节码指令,是压栈还是弹出或是执行计算。
  • 之所以说是线程私有的,因为如果不是私有的,那么整个计算过程最终的结果也将错误。

8、栈(Stack)

在计算机流传有一句废话: 程序 = 算法 + 数据结构 但是对于大部分同学都是: 程序 = 框架 + 业务逻辑

栈:后进先出 / 先进后出

队列:先进先出(FIFO : First Input First Output)

狂神docker笔记 狂神ks1197_jdk_03

在JVM中存在两个栈,java虚拟机栈和本地方法栈:

  • 本地方法栈与 Java 虚拟机栈作用类似,唯一不同的就是本地方法栈执行的是Native 方法,而虚拟机栈是为 JVM 执行 Java 方法服务的。
  • 另外,与 Java 虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和OutOfMemoryError 异常。
  • JDK1.8 HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一

下文中的栈都指:Java虚拟机栈

栈管理程序运行

存储一些基本类型的值、对象的引用、方法等。

执行过程:

  • 每一个方法在执行的同时,都会创建出一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口、线程等信息。
  • 方法从调用到执行完成,都对应着栈帧从虚拟机中入栈和出栈的过程。
  • 最终,栈帧会随着方法的创建到结束而销毁。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。

说明:

  1. 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放。
  2. 对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程私有的。
  3. 方法自己调自己就会导致栈溢出(递归死循环测试)报:StackOverflowError

栈里面会放什么东西那?

8大基本类型 + 对象的引用 + 实例的方法

栈运行原理

Java栈的组成元素—栈帧

栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。

  • 第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程:
  • 当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,
  • 于是产生了栈帧F2也被压入栈中,B方法又调用了C方法,
  • 于是产生栈帧F3也被压入栈中 执行完毕后,先弹出F3, 然后弹出F2,在弹出F1…

遵循 “先进后出” / “后进先出” 的原则。

狂神docker笔记 狂神ks1197_java_04

狂神docker笔记 狂神ks1197_java_05

对象实例化的过程。

9、堆(heap)

Java7之前

Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为

三部分:

  • 新生区 Young Generation Space Young/New
  • 养老区 Tenure generation space Old/Tenure
  • 永久区 Permanent Space Perm

堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)

狂神docker笔记 狂神ks1197_Java_06

谁空谁是to

GC垃圾回收主要是在新生区和养老区,又分为轻GC 和 重GC,如果内存不够,或者存在死循环,就会导致java.lang.OutOfMemoryError:Java heap space

新生区

新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。

新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区

  • 被new出来的,幸存区有两个:0区 和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC)。
  • 将伊甸园中的剩余对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。
  • 那如果1区也满了呢?(这里幸存0区和1区是一个互相交替的过程)再移动到养老区;
  • 若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老区的内存清理,
  • 若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常 OutOfMemoryError。

如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因如下:

  1. Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环

永久区(Perm)

永久存储区是一个常驻内存区域:

  • 用于存放JDK自身所携带的Class,Interface的元数据,
  • 也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

如果出现 java.lang.OutOfMemoryError:PermGen space,说明是 Java虚拟机对永久代Perm内存设置不够。 一般出现这种情况,都是程序启动需要加载大量的第三方jar包,例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

注意:

Jdk1.6之前: 有永久代,常量池1.6在方法区

Jdk1.7: 有永久代,但是已经逐步 “去永久代”,常量池1.7在堆

Jdk1.8及之后:无永久代,常量池1.8在元空间

在JDK8以后,永久存储区改了个名字(元空间)

狂神docker笔记 狂神ks1197_jdk_07

狂神docker笔记 狂神ks1197_jvm_08

  • JDK 1.8 JVM 的内存结构主要由三大块组成:堆内存、元空间和栈,Java 堆是内存空间占据最大的一块区域。
  • Java 堆,由年轻代和年老代组成,分别占据 1/3 和 2/3。
  • 而年轻代又分为三部分,Eden、From Survivor、To Survivor,占据比例为 8:1:1,可调。
  • 另外这里我们特意画出了元空间,也就是直接内存区域。在 JDK 1.8 之后就不在堆上分配方法区了。
  • 元空间从虚拟机 Java 堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制,说白了也就是以后不会因为永久代空间不够而抛出 OOM 异常出现了。
  • jdk1.8 以前版本的 class 和 JAR 包数据存储在 PermGen 下面 ,PermGen 大小是固定的,而且项目之间无法共用,公有的 class,所以比较容易出现 OOM 异常。

熟悉三区结构后方可学习JVM垃圾回收机制

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域: 它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码, 虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名,叫做Non-Heap(非堆),目的就是要和堆分开。

对于HotSpot虚拟机,很多开发者习惯将方法区称之为 “永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,Jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。

常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本,字段,方法,接口描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!

狂神docker笔记 狂神ks1197_java_09

堆内存调优

-Xms:设置初始分配大小,默认为物理内存的 “1/64”

-Xmx:最大分配内存,默认为物理内存的 “1/4”

-XX:+PrintGCDetails:输出详细的GC处理日志

测试1

代码测试

public class Demo01 {
        public static void main(String[] args) {
			//返回Java虚拟机试图使用的最大内存量
            long maxMemory = Runtime.getRuntime().maxMemory();
			//返回Java虚拟机中的内存总量
            long totalMemory = Runtime.getRuntime().totalMemory();

            System.out.println("MAX_MEMORY="+maxMemory+"(字节)、"
                    +(maxMemory/(double)1024/1024)+"MB");
            System.out.println("TOTAL_MEMORY="+totalMemory+"(字节)、"
                    +(totalMemory/(double)1024/1024)+"MB");
        }
    }

IDEA中进行VM调优参数设置,然后启动

狂神docker笔记 狂神ks1197_jvm_10

发现,默认的情况下分配的内存是总内存的 1/4,而初始化的内存为 1/64 !

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

VM参数调优:把初始内存,和总内存都调为 1024M,运行,查看结果!

狂神docker笔记 狂神ks1197_jdk_11

我们来大概计算分析一下!

狂神docker笔记 狂神ks1197_jdk_12

再次证明:元空间并不在虚拟机中,而是使用本地内存。

测试2

代码:

public class Demo02 {
        public static void main(String[] args) { String str = "kuangShenSayJava"; while (true){
            str += str + new Random().nextInt(88888888)
                    +new Random().nextInt(999999999);
        }
        }
    }

vm参数:

-Xms8m -Xmx8m -XX:+PrintGCDetails

测试,查看结果!

狂神docker笔记 狂神ks1197_狂神docker笔记_13

这是一个young 区域撑爆的JAVA 内存日志,其中 PSYoungGen 表示 youngGen分区的变化1536k 表示 GC 之前的大小。

488k 表示GC 之后的大小。

整个Young区域的大小从 1536K 到 624K , young代的总大小为 7680K。

狂神docker笔记 狂神ks1197_java_14

user – 总计本次 GC 总线程所占用的总 CPU 时间

sys – OS 调用 or 等待系统时间

real – 应用暂停时间

如果GC 线程是 Serial Garbage Collector 串行搜集器的方式的话(只有一条GC线程,), real time 等于user 和 system 时间之和。

通过日志发现Young的区域到最后 GC 之前后都是0,old 区域 无法释放,最后报堆溢出错误。

其他文章链接

一文读懂 - 元空间和永久代

Java方法区、永久代、元空间、常量池详解

10、Dump内存快照

在运行java程序的时候,有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在

eclipse里面有 Eclipse Memory Analyzer tool(MAT) 插件可以测试,而在idea中也有这么一个插件,

就是JProfifiler,一款性能瓶颈分析工具!

作用:

  • 分析Dump文件,快速定位内存泄漏;
  • 获得堆中对象的统计数据
  • 获得对象相互引用的关系
  • 采用树形展现对象间相互引用的情况

狂神docker笔记 狂神ks1197_jdk_15

安装JProfiler

1、IDEA插件安装

狂神docker笔记 狂神ks1197_java_16

2、安装JProfiler监控软件

下载地址:https://www.ej-technologies.com/download/jprofiler/version_92

狂神docker笔记 狂神ks1197_jdk_17

3、下载完双击运行,选择自定义目录安装,点击Next

注意:安装路径,建议选择一个文件名中没有中文,没有空格的路径 ,否则识别不了。然后一直点Next

狂神docker笔记 狂神ks1197_狂神docker笔记_18

4、注册

自行搜索解决即可(或者到狂神视频截图下载也行哈哈)

5、配置IDEA运行环境

Settings–Tools–JProflier–JProflier executable选择JProfile安装可执行文件。(如果系统只装了一个版本, 启动IDEA时会默认选择)保存

狂神docker笔记 狂神ks1197_Java_19

代码测试

package code1.com.kuang;import java.util.ArrayList;
public class Demo03 {    
    byte[] byteArray = new byte[1*1024*1024]; //1M = 1024K
    public static void main(String[] args) {        
        ArrayList<Demo03> list = new ArrayList<>();        
        int count = 0;        
        try {            
            while (true) {                
                list.add(new Demo03());  //问题所在                
                count = count + 1;            
            }        
        } catch (Error e) {            
            System.out.println("count:" + count);            
            e.printStackTrace();        
        }    
    }
}

vm参数 : -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

寻找文件:

狂神docker笔记 狂神ks1197_Java_20

使用 Jprofiler 工具分析查看

双击这个文件默认使用 Jprofiler 进行 Open大的对象!

狂神docker笔记 狂神ks1197_java_21

狂神docker笔记 狂神ks1197_jvm_22

从软件开发的角度上,dump文件就是当程序产生异常时,用来记录当时的程序状态信息(例如堆栈的状态),用于程序开发定位问题。