6、JDK1.6~1.8内存模型演变
图源:bugstack虫洞栈:面经#25
上图就是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;
}
通过反汇编可以得到其具体过程:
- 这些行号每一个都会对应一条需要执行的字节码指令,是压栈还是弹出或是执行计算。
- 之所以说是线程私有的,因为如果不是私有的,那么整个计算过程最终的结果也将错误。
8、栈(Stack)
在计算机流传有一句废话: 程序 = 算法 + 数据结构 但是对于大部分同学都是: 程序 = 框架 + 业务逻辑
栈:后进先出 / 先进后出
队列:先进先出(FIFO : First Input First Output)
在JVM中存在两个栈,java虚拟机栈和本地方法栈:
- 本地方法栈与 Java 虚拟机栈作用类似,唯一不同的就是本地方法栈执行的是Native 方法,而虚拟机栈是为 JVM 执行 Java 方法服务的。
- 另外,与 Java 虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和OutOfMemoryError 异常。
- JDK1.8 HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一
下文中的栈都指:Java虚拟机栈
栈管理程序运行
存储一些基本类型的值、对象的引用、方法等。
执行过程:
- 每一个方法在执行的同时,都会创建出一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口、线程等信息。
- 方法从调用到执行完成,都对应着栈帧从虚拟机中入栈和出栈的过程。
- 最终,栈帧会随着方法的创建到结束而销毁。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
说明:
- 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放。
- 对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程私有的。
- 方法自己调自己就会导致栈溢出(递归死循环测试)报:StackOverflowError
栈里面会放什么东西那?
8大基本类型 + 对象的引用 + 实例的方法
栈运行原理
Java栈的组成元素—栈帧
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
- 第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程:
- 当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,
- 于是产生了栈帧F2也被压入栈中,B方法又调用了C方法,
- 于是产生栈帧F3也被压入栈中 执行完毕后,先弹出F3, 然后弹出F2,在弹出F1…
遵循 “先进后出” / “后进先出” 的原则。
对象实例化的过程。
9、堆(heap)
Java7之前
Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为
三部分:
- 新生区 Young Generation Space Young/New
- 养老区 Tenure generation space Old/Tenure
- 永久区 Permanent Space Perm
堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)
谁空谁是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虚拟机的堆内存不够,原因如下:
- Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
永久区(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以后,永久存储区改了个名字(元空间)
- 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文件除了有类的版本,字段,方法,接口描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!
堆内存调优
-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调优参数设置,然后启动
发现,默认的情况下分配的内存是总内存的 1/4,而初始化的内存为 1/64 !
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
VM参数调优:把初始内存,和总内存都调为 1024M,运行,查看结果!
我们来大概计算分析一下!
再次证明:元空间并不在虚拟机中,而是使用本地内存。
测试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
测试,查看结果!
这是一个young 区域撑爆的JAVA 内存日志,其中 PSYoungGen 表示 youngGen分区的变化1536k 表示 GC 之前的大小。
488k 表示GC 之后的大小。
整个Young区域的大小从 1536K 到 624K , young代的总大小为 7680K。
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文件,快速定位内存泄漏;
- 获得堆中对象的统计数据
- 获得对象相互引用的关系
- 采用树形展现对象间相互引用的情况
安装JProfiler
1、IDEA插件安装
2、安装JProfiler监控软件
下载地址:https://www.ej-technologies.com/download/jprofiler/version_92
3、下载完双击运行,选择自定义目录安装,点击Next
注意:安装路径,建议选择一个文件名中没有中文,没有空格的路径 ,否则识别不了。然后一直点Next
4、注册
自行搜索解决即可(或者到狂神视频截图下载也行哈哈)
5、配置IDEA运行环境
Settings–Tools–JProflier–JProflier executable选择JProfile安装可执行文件。(如果系统只装了一个版本, 启动IDEA时会默认选择)保存
代码测试
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
寻找文件:
使用 Jprofiler 工具分析查看
双击这个文件默认使用 Jprofiler 进行 Open大的对象!
从软件开发的角度上,dump文件就是当程序产生异常时,用来记录当时的程序状态信息(例如堆栈的状态),用于程序开发定位问题。