说在前面:JVM~JVM,一个强敌,众多Java工程师的噩梦,众多大神们的必修秘籍之一,今天我就要向大神们看齐,但愿能捞到一点经验。此文仅表明做者我的观点,在文中不时引入其它参考书籍的资料并引入少部分我的观点,若有严重错误,但愿能坚决果断地指出并狠狠地diss
我!html
先放一张做者对虚拟机的“自画像”,能够看到在咱们某个Java程序运行的过程当中,在JVM
中主要有以下的区域,跟着我一块儿来一个个剥开这些区域的皮。下面这张图你收好,若是对你有用,点赞是给予我最大的支持!java
在上图能够看出,整个运行时数据区域分为两个部分:线程共享部分、线程私有部分数组
线程共享:顾名思义,就是全部线程都会享用这些空间,你用我也用、你们一块儿用缓存
线程私有:每一个线程都会独占本身本应该拥有的区域,河水不犯井水测试
从简单的开始看起~spa
下面先对线程私有部分的区域做简略的描述,让咱们先对这三个区域有个大概的了解~线程
程序计数器:记录某个线程下一步应该执行的字节码指令设计
虚拟机栈:当方法被调用时,就会产生一个栈帧并放入虚拟机栈中,而后方法结束后,该栈帧就会弹出虚拟机栈。指针
本地方法栈:和虚拟机栈很是类似,只不过它只存放被调用的
native
方法,什么是native
方法?待会就知道了code
是否是感受一头雾水~嗯...我一开始也是这样的,别急,立刻开始他们的自传秀。
《深刻理解Java虚拟机》原文:程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。
这部份内容并无介绍太多,程序计数器就是根据程序逻辑指示下一条该执行的语句是什么,程序的执行逻辑有顺序执行、if
语句、循环语句、方法调用·····它就是用来指明当前线程下一步该往哪一条语句执行。
《深刻理解Java虚拟机》原文:Java虚拟机栈(Java Virtual Machine Stack)描述的是Java方法执行的内存模型:每一个方法被执行的时候,Java虚拟机栈会同步建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态链接、方法出口等信息。
局部变量表:存放八大基本数据类型(byte
/short
/int
······)和对象引用指针
(指向对象在堆内存中的起始地址),还有最后返回的returnAddress
类型。returnAddress
就是指向下一条应该执行的字节码指令,如下面代码为例子解释对其的理解。
public class Test{
public void funcA(){
System.out.println("success execute funcA");
}
public void funcB(){
funcA();
System.out.println("success execute funcB");
}
public static void main(String[] args){
Test test = new Test();
test.funcB();
}
}
复制代码
调用funcB
时,方法funcB
会使用returnAddress
类型记录调用funcB
的字节码指令位置,当funcB
执行完毕时,就会调用returnAddress
类型返回到调用funcB
的字节码指令的位置,继续往下执行;一样,当funcB
调用funcA
时,funcA
内的returnAddress
也会记录调用者的字节码指令位置,当funcA
执行完后返回到funcB
调用的位置,继续往下执行。
本地方法栈:与Java虚拟机栈发挥的做用类似,两者的区别在于执行的方法类别不一样,本地方法栈专门为调用本地方法服务。
什么是本地方法?
本地方法是指由其余语言(如C、C++ 或其余汇编语言)编写,编译成和处理器相关的代码。至于如何加载和运行本地方法的,这里就再也不展开了,下面还有很长很长,继续继续~
方法区用于存储已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存·····
方法区也会被别人称做为永久代,由于它内部也是采用分代收集的回收方式进行GC,而在方法区中垃圾收集行为是比较少见的,这部分区域的垃圾回收主要是针对常量池的回收和对类型的卸载,通常来讲这部分区域的回收效果很难使人满意,由于知足垃圾收集的条件太苛刻了,因此处于方法区中的数据已经几乎是永久存在的了。
那为何会把方法区变成元空间了呢?
堆内存是全部对象和数组的分配区域,这个区域是GC的主要区域
现代大部分虚拟机都是基于分代收集理论设计的,因此不少虚拟机实现的堆上都会有“新生代”、“老年代”、“Eden”、“To Survivor”、“from Survivor”等概念出现,实际上这不是堆固有的东西。
如何加速对象的内存分配?
在HotSpot虚拟机实现中采用了TLAB
(本地线程分配缓冲)能够加速对象在堆上的内存分配效率,TLAB
就是在堆上给每一个线程开辟一小段内存缓冲区,线程建立对象时直接在本身的TLAB
中分配对象,当TLAB
用完后,再继续向堆申请内存,申请内存的过程当中须要同步机制。
/** * @author Zeng * @date 2020/4/5 15:34 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOutOfMemoryError * -Xms:堆的最小内存容量 * -Xmx:堆的最大内存容量 * -XX:虚拟机启动时指定的一些参数 */
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
复制代码
当堆上的对象一直分配分配分配·····直到对象占用的总内存大于堆最大可容纳对象的内存就会发生内存溢出
首先测试将栈的容量减少,使用
-Xss
参数测试
/** * @author Zeng * @date 2020/4/5 15:47 * VM Args: -Xss128k * -Xss:栈的最小内存容量 * 虚拟机栈溢出 */
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
//无限月读!!!!
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF sof = new JavaVMStackSOF();
try {
sof.stackLeak();
} catch (Exception e) {
System.out.println("stack length:" + sof.stackLength);
throw e;
}
}
}
复制代码
如上图所示抛出了StackOverFlowError
异常,栈帧不断地申请虚拟机栈内的内存,当虚拟机栈没有足够的内存放入这个栈帧时,就会发生栈内存溢出,比较常见的就是死递归。
而后测试一个栈帧的局部变量表内存大于虚拟机栈的内存容量,也就是说一个栈帧挤爆一个虚拟机栈,爽~
public class JavaVMStackSOF2 {
private static int stackLength = 0;
public static void test() {
long unused1, unused2···unused100 ;
unused1, unused2···unused100 = 0;
stackLength++;
test();
}
}
复制代码
两次测试能够看到,不管是虚拟机栈内存过小,仍是栈帧太大,都会致使虚拟机栈的内存溢出。
从JDK1.6开始,JDK1.六、JDK1.7和JDK1.8三个版本的HotSpot虚拟机中的方法区(JDK1.8已被替代为元空间)所包含的数据是不同的,下面逐一进行验证,因为笔者没有JDK1.6和JDK1.7的版本,借助网上其余做者的例子展现:
import java.util.HashSet;
import java.util.Set;
/** * @author Zeng * @date 2020/4/5 16:05 * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M * PermSize: 方法区的最小容量 * MaxPermSize:方法区的最大容量 * 在JDK1.6之后没法形成方法区内存溢出,由于常量池再也不处于方法区当中而在堆上分配 * -Xmx6M 指定堆的最大内存容量为6M则会由于常量池因内存不足而抛出堆内存溢出 * */
public class RuntimeConstantPoolOOM {
public void createString(){
Set<String> set = new HashSet<String>();
int i = 0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
public static void main(String[] args) {
RuntimeConstantPoolOOM constantPoolOOM = new RuntimeConstantPoolOOM();
constantPoolOOM.createString();
}
}
复制代码
JDK1.6运行结果:
JDK1.7运行结果:
JDK1.8运行结果:
从上面三幅图能够看出,JDK1.6版本中,运行时常量池会致使PermGen space
溢出,也就是永久代
空间溢出;而在JDK1.7中能够发现溢出的区域是堆而不是永久代了,因此能够验证从JDK1.7开始运行时常量池从方法区搬移至堆了。
参考资料:www.cnblogs.com/paddix/p/53…
若是这篇文章能给你带来一点点帮助,但愿可以获得你的一个点赞,你的一个赞会让我开心好久,会让我更加努力的持续去输出好文章分享给大家!感谢你的阅读!