共计 8866 个字符,预计需要花费 23 分钟才能阅读完成。
运行数据区
字节码只是一个二进制文件寄存在那里。要想在 jvm 里跑起来,先得有个运行的内存环境。
也就是咱们所说的 jvm 运行时数据区。
1)运行时数据区的地位
运行时数据区是 jvm 中最为重要的局部,执行引擎频繁操作的就是它。类的初始化,以及前面咱们讲的对象空间的调配、垃圾的回收都是在这块区域产生的。
2)区域划分
依据《Java 虚拟机标准》中的规定,在运行时数据区将内存细分为几个局部
线程公有的:Java 虚拟机栈(Java Virtual Machine Stack)、程序计数器(Program Counter Register)、本地办法栈(Native Method Stacks)
大家共享的:办法区(Method Area)、Java 堆区(Java Heap)
接下来咱们分块具体来解读,每一块是做什么的,如果溢出了会产生什么事件
1.1 程序计数器
1.1.1 概述
程序计数器(Program Counter Register)
- 每个线程一个。是一块较小的内存空间,它示意以后线程执行的字节码指令的地址。
- 字节码解释器工作时,通过扭转这个计数器的值来选取下一条须要执行的字节码指令,所以整个程序无论是分支、循环、跳转、异样解决、线程复原等根底性能都须要依赖这个计数器来实现。
- 因为线程是多条并行执行的,相互之间执行到哪条指令是不一样的,所以每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。
- 如果是 native 办法,这里为空
1.1.2 溢出异样
没有!
在虚拟机标准中,没有对这块区域设定内存溢出标准,也是惟一一个不会溢出的区域
1.1.3 案例
因为它不会溢出,所以咱们没有方法给它造一个,然而从 class 类上能够找到痕迹。
回顾下面 javap 的反汇编,其中 code 所对应的编号就能够了解为计数器中所记录的执行编号。
1.2 虚拟机栈
1.2.1 概述
- 也是线程公有的!生命周期与线程雷同。
- 它形容的是 Java 办法执行的以后线程的内存模型,每个办法被执行的时候,Java 虚拟机都会同步创立一个栈帧,用于存储局部变量表、操作数栈、动静连贯、办法进口等信息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
1.2.2 溢出异样
1)栈深度超出设定
如果是创立的栈的深度大于虚拟机容许的深度,抛出
Exception in thread “main” java.lang.StackOverflowError
2)内存申请有余
如果栈容许内存扩大,然而内存申请不够的时候,抛出 OutOfMemoryError
留神!这一点和具体的虚拟机无关,hotspot 虚拟机并不反对栈空间扩大,所以单线程环境下,一个线程创立时,调配给它固定大小的一个栈,在这个固定栈空间上不会呈现再去扩容申请内存的状况,也就不会遇到申请不到一说,只会因为深度问题超出固定空间造成下面的 StackOverflowError
如果换成多线程,毫无节制的创立线程,还是有可能造成 OutOfMemoryError。然而这个和 Xss 栈空间大小无关。是因为线程个数太多,栈的个数太多,导致系统调配给 jvm 过程的物理内存被吃光。
这时候虚构机会附带相干的提醒:
Exception in thread “main” java.lang.OutOfMemoryError: unable to create native thread
ps: 每个线程默认调配 1M 空间(64 位 linux,hotspot 环境)
疑难:是不是改小 Xss 的值就能够失去栈空间溢出呢?
答:依据下面的剖析,hotspot 下不能够,还是会抛出 StackOverflowError,无非深度更小了。
1.2.3 案例一:进出栈程序
1)代码
package com.itheima.jvm.demo;
/**
* 程序模仿进栈、出栈过程
* 先进后出
*/
public class StackInAndOut {
/**
* 定义方法一
*/
public static void A() {System.out.println("进入办法 A");
}
/**
* 定义方法二; 调用办法一
*/
public static void B() {A();
System.out.println("进入办法 B");
}
public static void main(String[] args) {B();
System.out.println("进入 Main 办法");
}
}
2)运行后果:
进入办法 A
进入办法 B
进入 Main 办法
3)栈构造:
main 办法 —->B 办法 —->A 办法
1.2.4 案例二:栈深度溢出
1)代码
这个容易实现,办法嵌套本人就能够:
package com.itheima.jvm.demo;
/**
* 通过一个程序模仿线程申请的栈深度大于虚拟机所容许的栈深度;* 抛出 StackOverflowError
*/
public class StackOverFlow {
/**
* 定义方法,循环嵌套本人
*/
public static void B() {B();
System.out.println("进入办法 B");
}
public static void main(String[] args) {B();
System.out.println("进入 Main 办法");
}
}
2)运行后果:
Exception in thread "main" java.lang.StackOverflowError
at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
3)栈构造:
1.2.5 案例三:栈内存溢出
始终不停的创立线程就能够堆满栈
然而!这个很危险,到 32 零碎的 winxp 上怯懦的小伙伴能够试一试,机器卡死不负责!
package com.itheima.jvm.demo;
/*
* 栈内存溢出,留神!很危险,审慎执行
* 执行时可能会卡死零碎。直到内存耗尽
* */
public class StackOutOfMem {public static void main(String[] args) {while (true) {new Thread(() -> {while(true);
}).start();}
}
}
1.3 本地办法栈
1.3.1 概述
- 本地办法栈的性能和特点相似于虚拟机栈,均具备线程隔离的特点
- 不同的是,本地办法栈服务的对象是 JVM 执行的 native 办法,而虚拟机栈服务的是 JVM 执行的 java 办法
- 虚拟机标准里对这块所用的语言、数据结构、没有强制规定,虚拟机能够自在实现它
- 甚至,hotspot 把它和虚拟机栈合并成了 1 个
1.3.2 溢出异样
和虚拟机栈一样,也是两个:
如果是创立的栈的深度大于虚拟机容许的深度,抛出 StackOverFlowError
内存申请不够的时候,抛出 OutOfMemoryError
1.4 堆
1.4.1 概述
与下面的 3 个不同,堆是所有线程共享的!所谓的线程平安不平安也是出自这里。
在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,Java 世界里“简直”所有的对象实例都在这里分配内存。
须要留神的是,《Java 虚拟机标准》并没有对堆进行粗疏的划分,所以对于堆的解说要基于具体的虚拟机,咱们以应用最多的 HotSpot 虚拟机为例。
Java 堆是垃圾收集器治理的内存区域,因而它也被称作“GC 堆”,这就是咱们做 JVM 调优的重点区域局部。
1.4.2 jdk1.7
jvm 的内存模型在 1.7 和 1.8 有较大的区别,尽管 1.7 目前应用的较少了,然而咱们也是须要对 1.7 的内存模型有所理解,所以接下里,咱们将先学习 1.7 再学习 1.8 的内存模型。
-
Young 年老区(代)
Young 区被划分为三局部,Eden 区和两个大小严格雷同的 Survivor 区
其中,Survivor 区间中,某一时刻只有其中一个是被应用的,另外一个留做垃圾收集时复制对象用
在 Eden 区间变满的时候,GC 就会将存活的对象移到闲暇的 Survivor 区间中,依据 JVM 的策略,在通过几次垃圾收集后,任然存活于 Survivor 的对象将被挪动到上面的 Tenured 区间。
-
Tenured 年老区
Tenured 区次要保留生命周期长的对象,个别是一些老的对象,当一些对象在 Young 复制转移肯定的次数当前,对象就会被转移到 Tenured 区,个别如果零碎中用了 application 级别的缓存,缓存中的对象往往会被转移到这一区间。
-
Perm 永恒区
hotspot 1.6 才有这货,当初曾经成为历史
Perm 代次要保留 class,method,filed 对象,这部份的空间个别不会溢出,除非一次性加载了很多的类,不过在波及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError : PermGen space 的谬误,造成这个谬误的很大起因就有可能是每次都重新部署,然而重新部署后,类的 class 没有被卸载掉,这样就造成了大量的 class 对象保留在了 perm 中,这种状况下,个别重新启动应用服务器能够解决问题。另外一种可能是创立了大批量的 jsp 文件,造成类信息超出 perm 的下限而溢出。这种重启也解决不了。只能调大空间。
-
Virtual 区:
jvm 参数能够设置一个范畴,最大内存和初始内存的差值,就是 Virtual 区。
1.4.3 jdk1.8
由上图能够看出,jdk1.8 的内存模型是由 2 局部组成,年老代 + 年轻代。永恒代被干掉,换成了 Metaspace(元数据空间)
年老代:Eden + 2*Survivor(不变)
年轻代:OldGen(不变)
元空间:原来的 perm 区(重点!)
须要特地阐明的是:Metaspace 所占用的内存空间不是在虚拟机外部,而是在本地内存空间中,这也是与 1.7 的永恒代最大的区别所在。
1.4.4 溢出异样
内存不足时,抛出
java.lang.OutOfMemoryError: Java heap space
1.4.5 案例:堆溢出
1)代码
调配大量对象,超出 jvm 规定的堆范畴即可
package com.itheima.jvm.demo;
import java.util.ArrayList;
import java.util.List;
/**
* 堆溢出
* -Xms20m -Xmx20m
*/
public class HeapOOM {Byte[] bytes = new Byte[1024*1024];
public static void main(String[] args) {List list = new ArrayList();
int i = 0;
while (true) {System.out.println(++i);
list.add(new HeapOOM());
}
}
}
2)启动
留神启动时,指定一下堆的大小:
2)输入
1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.itheima.jvm.demo.HeapOOM.<init>(HeapOOM.java:7)
at com.itheima.jvm.demo.HeapOOM.main(HeapOOM.java:13)
1.5 办法区
1.5.1 概述
同样,线程共享的。
它次要用来存储类的信息、类里定义的常量、动态变量、编译器编译后的代码缓存。
留神!办法区在虚拟机标准里这是一个逻辑概念,它具体放在那个区域里没有严格的规定。
所以,hotspot 1.7 将它放在了堆的永恒代里,1.8+ 独自开拓了一块叫 metaspace 来寄存一部分内容(不是全副!定义的类对象在堆里)
具体方法区次要存什么货色呢?粗略的分,能够划分为两类:
- 类信息:次要指类相干的版本、字段、办法、接口形容、援用等
-
运行时常量池:编译阶段生成的常量与符号援用、运行时退出的动静变量
(常量池里的类变量,如对象或字符串,比拟非凡,1.6 和 1.8 地位不同,上面会讲到)
小提示:
这里常常会跟下面堆里的永恒代一概而论,实际上这是两码事
永恒代是 hotspot 在 1.7 及之前才有的设计,1.8+,以及其余虚拟机并不存在这个货色。
能够说,永恒代是 1.7 的 hotspot 偷懒的后果,他在堆里划分了一块来实现办法区的性能,叫永恒代。因为这样能够借助堆的垃圾回收来治理办法区的内存,而不必独自为办法区再去编写内存管理程序。懈怠!
同时代的其余虚拟机,如 J9,Jrockit 等,没有这个概念。起初 hotspot 意识到,永恒代来做这件事不是一个好主见。1.7 曾经从永恒代拿走了一部分数据,直到 1.8+ 彻底去掉了永恒代,办法区大部分被移到了 metaspace(再强调一下,不是全副!)
论断:
办法区是肯定存在的,这是虚拟机规定的,然而是个逻辑概念,在哪里虚拟机本人去决定
而永恒代不肯定存在(hotspot 1.7 才有),已成为历史
1.5.2 溢出异样
1.6:OutOfMemoryError: PermGen space
1.8:OutOfMemoryError: Metaspace
1.5.3 案例:1.6 办法区溢出
1)原理
在 1.6 里,字符串常量是运行时常量池的一部分,也就是归属于办法区,放在了永恒代里。
所以 1.6 环境下,让办法区溢出,只须要可劲造往字符串常量池中造字符串即可,这里用到一个办法:
/*
如果字符串常量池里有这个字符串,间接返回援用,不再额定增加
如果没有,加进去,返回新创建的援用
*/
String.intern()
2)代码
/**
* 办法区溢出,留神限度一下永恒代的大小
* 编译的时候留神 pom 里的版本,要设置 1.6,否则启动会有问题
* jdk1.6 : -XX:PermSize=6M -XX:MaxPermSize=6M
*/
public class ConstantOOM {public static void main(String[] args) {ConstantOOM oom = new ConstantOOM();
Set<String> stringSet = new HashSet();
int i = 0;
while (true) {System.out.println(++i);
stringSet.add(String.valueOf(i).intern());
}
}
}
3)创立启动环境
4)异样信息:
...
19118
19119
19120
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:19)
1.5.4 案例:1.8 办法区溢出
1)到了 1.8,状况产生了变动
能够测试一下,1.8 下无论指定上面的哪个参数,常量池运行都不会溢出,会始终打印上来
-XX:PermSize=6M -XX:MaxPermSize=6M
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
2)配置运行环境
3)控制台信息
不会抛出异样,只有你 jvm 堆内存够,实践上能够始终打上来
4)为什么呢?
永恒代咱们加了限度,后果没意义,因为 1.8 里曾经没有这货了
元空间也加了限度,同样没意义,那阐明字符串常量池它不在元空间里!
那么,它在哪里呢?
jdk1.8 当前,字符串常量池被移到了堆空间,和其余对象一样,承受堆的管制。
其余的运行时的类信息、根本数据类型等在元空间。
咱们能够验证一下,对下面的运行时参数再加一个堆下限限度:
-Xms10m
-Xmx10m
运行环境如下:
运行没多久,你会失去以下异样:
……
84014
84015
84016
84017
84018
84019
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:18)
阐明:1.8 里,字符串 inter()被放在了堆里,受最大堆空间的限度。
5)那如何能力让元空间溢出呢?
既然字符串常量池不在这里,那就换其余的。类的根本信息总在元空间吧?咱们来试一下
cglib 是一个 apache 下的字节码库,它能够在运行时生成大量的对象,咱们 while 循环同时限度 metaspace 试试:
附:https://gitee.com/mirrors/cglib(想深刻理解这个工具的猛击右边,这里不做过多探讨)
package com.itheima.jvm.demo;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* jdk8 办法区溢出
* -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class ConstantOOM8 {public static void main(final String[] args) {while (true) {Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.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(objects,args);
}
});
enhancer.create();}
}
static class OOM{}}
6)运行设置
7)运行后果
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
论断:
jdk8 引入元空间来存储办法区后,内存溢出的危险比历史版本小多了,然而在类超出管制的时候,仍然会打爆办法区
1.6 一个案例
为便于大家了解和记忆,上面咱们用一个案例,把下面各个区串通起来。
假如有个 Bootstrap 的类,执行 main 办法。在 jvm 里,它从 class 文件到跑起来,大抵通过如下步骤:
- 首先 JVM 会先将这个 Bootstrap.class 信息加载到内存中的办法区
- 接着,主线程开拓一块内存空间,筹备好程序计数器 pc,虚拟机栈、本地办法栈
- 而后,JVM 会在 Heap 堆上为 Bootstrap.class 创立一个 Bootstrap.class 的类实例
- JVM 开始执行 main 办法,这时在虚拟机栈里为 main 办法创立一个栈帧
- main 办法在执行的过程之中,调用了 greeting 办法,则 JVM 会为 greeting 办法再创立一个栈帧,推到虚拟机栈顶,在 main 的下面,每次只有一个栈帧处于活动状态,以后为 greeting
- 当 greeting 办法运行实现后,则 greeting 办法出栈,以后流动帧指向 main,办法持续往下运行
1.7 演绎总结
1)独享 / 共享的角度:
- 独享:程序计数器、虚拟机栈、本地办法栈
- 共享:堆、办法区
2)error 的角度:
- 程序计数器:不会溢出,比拟非凡,其余都会
- 两个栈:可能会产生两种溢出,一是深度超了,报 StackOverflowError,空间有余:OutOfMemoryError
- 堆:只会在空间有余时,报 OutOfMemoryError,会提醒 heapSpace
- 办法区:空间有余时,报 OutOfMemoryError,提醒不同,1.6 是 permspace,1.8 是元空间,和它在什么中央无关
3)归属:
- 计数器、虚拟机栈、本地办法栈:线程创立必须申请配套,真正的物理空间
- 堆:真正的物理空间,然而内部结构的划分有变动,1.6 有永恒代,1.8 被干掉
- 办法区:最没归属感的一块,起因就是它是一个逻辑概念。1.6 被放在了堆的永恒代,1.8 被拆分,一部分在元空间,一部分(办法区的运行时常量池外面的类对象,包含字符串常量,被设计放在了堆里)
- 间接内存:这块实际上不属于运行时数据区的一部分,而是间接操作物理内存。在 nio 操作里 DirectByteBuffer 类能够对 native 操作,防止流在堆内外的拷贝。咱们下一步的调优不会波及到它,理解即可。
本文由
传智教育博学谷
教研团队公布。如果本文对您有帮忙,欢送
关注
和点赞
;如果您有任何倡议也可留言评论
或私信
,您的反对是我保持创作的能源。转载请注明出处!