说在前面:JVM~JVM,一个强敌,众多Java工程师的噩梦,众多大神们的必修秘籍之一,今天我就要向大神们看齐,希望能捞到一点经验。此文仅代表作者个人观点,在文中不时引入其它参考书籍的资料并引入少部分个人观点,如有严重错误,希望能毫不犹豫地指出并狠狠地diss
我!
先放一张作者对虚拟机的“自画像”,可以看到在我们某个Java程序运行的过程中,在JVM
中主要有如下的区域,跟着我一起来一个个剥开这些区域的皮。下面这张图你收好,如果对你有用,点赞是给予我最大的支持!
在上图可以看出,整个运行时数据区域分为两个部分:线程共享部分、线程私有部分
线程共享:顾名思义,就是所有线程都会享用这些空间,你用我也用、大家一起用
线程私有:每个线程都会独占自己本应该拥有的区域,河水不犯井水
从简单的开始看起~
下面先对线程私有部分的区域作简略的描述,让我们先对这三个区域有个大概的了解~
程序计数器:记录某个线程下一步应该执行的字节码指令
虚拟机栈:当方法被调用时,就会产生一个栈帧并放入虚拟机栈中,然后方法结束后,该栈帧就会弹出虚拟机栈。
本地方法栈:和虚拟机栈非常相似,只不过它只存放被调用的
native
方法,什么是native
方法?待会就知道了
是不是感觉一头雾水~嗯...我一开始也是这样的,别急,马上开始他们的自传秀。
《深入理解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.6、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…
如果这篇文章能给你带来一点点帮助,希望能够得到你的一个点赞,你的一个赞会让我开心很久,会让我更加努力的持续去输出好文章分享给你们!感谢你的阅读!