官方文档规定的运行时数据区域
- 官方文档中规定的运行时数据区一共就几块: PC计数器, 虚拟机栈, 本地方法栈, 堆区, 方法区, 运行时常量池. 这里的官方规定是说, 若是你要作一个Java虚拟机的话, 必需要包含这几个区域, 可是这几个区域在你的虚拟机中是用哪块内存实现的, 这由虚拟机制做者决定.
程序计数器java
- The pc Register, 程序计数器. 若是了解过计算机系统, 对这个名词应该不陌生了, 它指向下一条指令的地址, 程序靠它跑起来.
- Java虚拟机支持多线程, 每条线程都有本身的程序计数器.
- 若是当前线程正在执行一个Java方法, 它的计数器记录的是正在执行的Java虚拟机指令的地址. 若是执行的是本地方法(好比系统的C语言函数), 计数器中的值为空(Undefined).
- 正由于程序计数器记录的是指令地址, 因此它占用的空间较少, Java虚拟机规范中并无规定这块内存有OutOfMemoryError(内存溢出)的状况.
Java虚拟机栈算法
- Java Virtual Machine Stacks, Java虚拟机栈.
- Java虚拟机栈是线程私有的, 生命周期与线程相同. 虚拟机栈存放栈帧, 栈帧用于存储局部变量表, 部分结果值, 方法的初始化参数和返回信息, 方法的执行经过栈帧的压栈和出栈实现.
本地方法栈数组
- 本地方法栈和上面的虚拟机栈是类似的, 从名字也看出, 虚拟机方法栈是用来执行Java代码的, 而本地方法栈则是用来执行本地系统代码的, 好比C代码.
- 也由于规范中没有规定本地方法栈执行的代码, 若是想执行Java代码也是能够的, 咱们能够看到Oracle官方的虚拟机HotSpot虚拟机把Java虚拟机栈和本地方法栈合二为一, 这么作避免了要为不一样的语言设计栈, 提升了虚拟机的性能.
虚拟机栈和本地方法栈溢出多线程
若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:787707172,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。jvm
- 那么当出现错误信息后, 咱们在什么错误信息下能够去排查是否虚拟机栈和本地方法栈这两块内存出错呢? 这里以HotSpot虚拟机为例讲解(HotSpot把两块栈结构合在一块儿实现了), 在JDK1.8的虚拟机规范中对这两块栈空间可能出现的错误给出了相同的描述.
- 一: 若是一条线程所须要的内存大于虚拟机所分配给它的内存, 将抛出StackOverflowError异常.
- 二: 若是栈内存能够扩展并尝试扩展时可用的内存不足, 或者建立新线程并为其分配栈内存时可能的内存不足, 会抛出OutOfMemoryError
- 下面先演示第一个StackOverflowError异常
//设置虚拟机参数 -Xss128k, 设置单个线程的栈空间大小为128k public class StackErrorTest1 { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { StackErrorTest1 set1 = new StackErrorTest1(); try{ set1.stackLeak(); }catch (Throwable e){ System.out.println("stack length:" + set1.stackLength); e.printStackTrace(); } } } //输出异常信息 stack length:1000 java.lang.StackOverflowError at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:7) at jvm.StackErrorTest1.stackLeak(StackErrorTest1.java:8) ...
- 因此当遇到StackOverflowError时能够考虑是不是是虚拟机的栈容量过小, 好比这里的无穷递归, 栈空间不够用. 固然生产环境中确定不会写无穷递归, 这时能够经过设置-Xss参数调整单条线程的栈内存大小.
- 上面描述的栈内存能够扩展并尝试扩展时可用的内存不足致使出现OutOfMemoryError的状况暂时没有好的演示代码, 在周志明的《深刻理解Java虚拟机》中提到"定义了大量本地变量,增大方法帧中本地变量表的长度, 结果仍抛出StackOverflowError". 不知道是否是没有触发虚拟机动态扩充栈空间, 因此仍然断定是栈所需的空间超出了虚拟机规定的大小. 总结来讲不管是栈帧太大仍是栈空间过小都会抛出StackOverflowError, 能够考虑调整-Xss参数.
- 上面还提到当建立新线程并分配新的栈空间时, 若是可用的内存不够, 会抛出OutOfMemoryError异常, 下面是这种状况的代码演示.
public class StackErrorTest2 { private void keepRunning(){ while(true){ } } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable() { @Override public void run() { keepRunning(); } }); thread.start(); } } public static void main(String[] args){ StackErrorTest2 set2 = new StackErrorTest2(); set2.stackLeakByThread(); } } //运行结果, 来源《深刻理解Java虚拟机》 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
- 这段代码也来自深刻理解jvm, 书中也说明跑这段代码要当心, 由于Java的线程是映射到内核线程上的, 果不其然个人机子一跑就死机了.
- 问什么会出现这样的错误? 32位Windows系统分配给一个进程的内存最大为2GB(32位能寻址4GB地址空间, 除去内核的空间剩2GB, 64位则大得多). 这2GB减去最大堆容量, 减去方法区的容量, 剩下的就是虚拟机栈和本地方法区栈的内存空间了. (补充: PC计数器占的空间很小, 运行时常量池在方法区中, HotSpot中虚拟机栈和本地方法栈一块儿实现, 因此能分红这么三大块内存).
- 了解了三大块内存区后(HotSpot下), 解决思路也出来了: 1. 减少最大堆内存, 腾出更多位置给栈空间. 2. 若是程序的线程数量不能够减小, 那么就看看是否能够减小每条线程的栈内存.
- 固然用一台配置高的机器, 该用64位的Java虚拟机也是一种方法.
Java堆分布式
- Java堆是随着虚拟机的启动而建立的, 用于存放对象实例, 全部的对象实例和数组都在堆内存分配, 它被全部线程共享. Java堆是Java虚拟机管理的内存中最大的一块, 也是垃圾回收器管理的主要区域. 从内存回收的角度看, Java堆内存还能够被继续划分, 而且和具体的虚拟机实现有关.
- 当前主流的虚拟机都是支持堆内存动态扩展的, 就是说当堆内存的大不够时, 它会扩充容量; 当不要太多的空间时, 它能本身进行压缩. 咱们能够人为地经过-Xmx和-Xms设定堆内存的最大值和最小值(初始大小). 若是咱们把-Xmx和-Xms设置为相同的值, 就等同于设定了固定大小的Java堆. (这是gc调优的一种手段)
- 若堆内存分配内存时发现已经没有更过可用空间时, 会抛出OutOfMemoryError.
演示堆内存溢出ide
- 堆内存是存放对象实例的地方, 这个应该比较好理解, 直接上代码
/** * VM Args: -Xms20m -Xmx20m */ public class HeapErrorTest { static class Object{ } public static void main(String[] args) { List<Object> list = new ArrayList<>(); while(true){ list.add(new Object()); } } } //运行结果 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181)
- 由结果能够看到当堆内存溢出后除了有java.lang.OutOfMemoryError外, 还会提示Java heap space. 在这个例子中, 咱们明确地知道了是因为堆内存不够大而形成的溢出. 然而在生产环境中, 当系统报出堆内存溢出时, 咱们首先要搞清楚是由于内存泄漏致使的内存溢出, 仍是纯粹的内存溢出.
- 内存溢出指的是分配内存的时候, 没有足够的空间供其使用. 内存泄漏指的是在分配一块内存使用完后没有释放, 在Java中对应的场景是没有被垃圾回收器回收. 一点点的内存泄漏用户可能感觉不到, 可是当泄漏的内存聚沙成塔的时候, 会耗尽内存, 致使内存溢出.
- 有一些经常使用的分析内存溢出的手段和工具, 这里就不详细叙述了, 能够参考书籍或网上的资料. 当咱们判断是内存泄漏致使的溢出后, 能够根据工具定位出现泄漏的代码位置; 若是不存在泄漏只是单纯的溢出的话, 能够经过设置虚拟参数调整堆内存大小(前提是机器的配置可以支持相应的内存大小), 或者看看代码中是否存在一些生命周期很长的对象实例, 看看可否做出修改.
方法区函数
- 方法区用于存储以被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码数据等, 它是全部线程共享的. 虚拟机规范中说方法区在逻辑上是堆的一部分, 可是它的别名叫"non-Heap"也就是非堆的意思, 代表它和堆内存是两块独立的内存. 至于说在逻辑上是堆区的一部分, 是由于在物理实现上, 方法区的内存地址包含于堆中, 因此说是逻辑上的一部分, 实际用的时候是彻底不一样的部分. 这么设计多是由于便于垃圾收集器统一管理吧.
运行时常量池微服务
- 运行时常量池的内存由方法区分配, 也就是说它属于方法区的一部分. 它用于存储Class文件中的类版本, 字段, 方法, 接口和常量池等, 也用于存放编译期生成的各类字面量和符号引用.
- 运行时常量池区别于Class文件常量池的一个重要特征是具有动态特性. 也就说并不是在Class文件中定义的常量才能进入运行时常量池, 在程序运行的过程当中也有可能将新的常量放入池中.
演示方法区溢出工具
- 演示方法区溢出和堆区的思路同样, 不断往方法堆中加入东西使其溢出. 只是方法区中保存的是类信息, 咱们经过不断动态生成类演示
- 本代码示例来源于深刻理解jvm, 可是其中的参数须要改变, 该书的最新版本是基于JDK1.7的, JDK1.7中方法区是在永久代中实现的, 而JDK1.8中已经没有永久代了, 方法区中Metaspace元数据区中, 经过设置-XX:MetaspaceSize和-XX:MaxMetaspaceSize来指定方法区的大小
/** * VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m */ public class MethodAreaTest { static class Object{ } public static void main(String[] args) { int count = 0; while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Object.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public java.lang.Object intercept(java.lang.Object o, Method method, java.lang.Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(objects, objects); } }); enhancer.create(); System.out.println(++count); } } } 运行结果: Caused by: java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ... 8 more
HotSpot虚拟机的内存模型
- 在介绍完Java虚拟机运行时数据区域后, 接着以HotSpot虚拟机为例介绍虚拟机内存模型.
- 首先有一个重要的概念要搞清楚, 要否则容易犯晕.
- 在前面介绍Java运行时数据区域时咱们谈到PC计数器, 虚拟机栈, 本地方法栈这3块内存都是线程私有的, 它们的随线程的建立而分配, 随线程的结束而释放, 也就是说Java虚拟机是明确知道这三块内存是何时该被回收的, 只要线程没执行完就不能回收, 不然线程跑不起来.
- 而咱们在谈论虚拟机的内存模型时, 一般要和垃圾回收结合在一块儿讨论. 既然上面的三块内存回收的时间已定, 暂时不须要过多考虑, 虚拟机分配内存时给它们留有空间就行.
- 但另外的两块内存堆内存和方法区则不同, 它们是全部线程共享的, 在这里面内存的分配和释放具备不肯定性. 好比说在多态的状况下, 一个接口对应的实现类不一样, 具体的实现方法也不一样, 虚拟机只有在程序运行的过程当中才知道要建立哪些对象, 这部份内存的分配和释放都是动态的, 垃圾收集器关注的也是这部分的内容.
- 因此说咱们后续描述的虚拟机内存模型是创建在Java堆内存和方法区上的.
JVM实现的堆内存和方法区
- 正如上述所说, 当谈论JVM的内存结构时, 讨论的重点就由整个运行时数据区域转为对堆内存和方法区的讨论, 由于这两部分是垃圾回收的重点区域(若是二者要比较的话, 重点收集区域是堆区).
- 而HotSpot虚拟机的内存结构由三大部分组成: 新生代, 老年代和元数据区(JDK1.7及之前叫老年代). 其中新生代和老年代是虚拟机规范中Java堆内存的实现, 元数据区是规范中方法区的实现. 在讲述为何这么定义以前, 先明确这个关系对于理解概念是很重要的, 下面有幅图帮助理解.

- 这里有个小失误, 题目中明明讲的是JDK1.8, 为何还提永久代呢? 因为永久代存在的时间长, 永久代的说法通过这么多年可能已经深刻人心, 因此先并列讲, 要知道永久代和元数据区是有本质的差异的, 这留到后面讲, 先认清概念.
- 但愿图片加描述可以帮助你当即规范定义的数据区域和JVM内存结构之间的关系. 下面将对HotSpot虚拟机的内存模型作进一步分析.
新生代和老年代.
- Java堆内存被实现为新生代和老年代, 是为了更方便地进行垃圾回收. 咱们知道对象是存储在堆内存中的, 从字面上理解新生代就是新建立的对象区域, 老年代就是使用屡次生命周期长的对象区域. 新生代对象生命周期一般较短, 不少用完便可以释放; 老年代对象的生命周期较长, 可能在整个程序的运行过程当中都是有用的.
- 因为新对象和老对象具备不一样的性质, 为对这两种对象设计的垃圾回收算法也不一样, 因此要把它们分开.
新生代中的内存划分
- 新生代的内存被分为一个Eden区和两个Survivor区. 为了讲述为何要这么分, 需简单引入垃圾回收算法.
- 首先最基础, 最简单的垃圾回收算法叫标记-清除算法. 算法流程和算法名彻底一致: 首先标记出哪些是能够回收的对象, 标记完后把对象清除. 若是按照这么个流程, 新生代应该就是一块简单的内存就行, 现实结论告诉咱们这个算法是能够优化的.
- 标记清除算法的不足在于一块完整的内存在通过标记-清除算法后有些内存会被释放掉, 这时会形成内存空间不连续, 可能不可以存放一些较大的对象.
- 标记-清除算法的升级版是复制算法, 它在标记-清除的思路上做出了些改变. 首先将内存分为两块, 当建立新对象分配内存的时候只用两块中的一块A. 当进行垃圾回收的时候只对有对象的一块A内存使用标记-清除算法进行回收, 回收后剩余的存活对象从内存A移到另外一块空的内存B中, 这样A内存从新变为空内存, 继续重复此分配回收过程. 这个算法彷佛更好一些, 可是也只是两块内存, 说明还不是现实中的最优解.
- 考虑新的算法, 把内存分配成均等两块, 等同于可以使用的内存变为原来的二分之一了, 根据IBM专门部分研究新生代中百分之98%的对象都是"朝生夕死"的, 也就是说在进行垃圾回收时98%的对象都被回收掉, 只有2%会从A内存移动到B内存. 这么一想咱们把两块内存割为相同的两块是否是有点太亏了?
- 下面揭晓答案: HotSpot虚拟机回收虚拟机时使用的是复制算法, 可是它分红三块内存, 一个占80%内存的Eden区(堆内存), 两个分别占10%的Survivor区. 具体操做是这样的: 程序运行时, 用Eden区和一个Survivor区A存放新建立的对象. 当发生垃圾回收时, 把存活下来的对象(不多)复制到另外一块Survivor区B中, 使得Eden区和Survivor区A从新为空, 而后继续重复这个分配回收的过程.
- 因此说详细点的Jvm的内存模型是下面这样的

由JDK1.7及之前的永久代到JDK1.8的元数据区
- 搞定完堆区在JVM内存模型中的实现, 下面谈论方法区的实现.
- 在JDK1.7及之前, JVM使用永久代来实现方法区. 这里用"实现"二字是通过斟酌的, 由于永久代并不等同于方法区. 从名字也能够看出它和新生代, 老年代是一脉相承的, 逻辑上是一体的, 命名为永久代是由于这部份内存不多几乎不被回收. 这一不多几乎不被回收的特性正好对应方法区中存储的类信息, 常量, 静态变量等元素. 因此说用永久代来实现方法区.
- 可是用永久代来实现方法区并非最优解, 好比容易出现内存溢出问题(具体分析去除永久代, 改用Metaspace的缘由能够参考文章末尾所列出的资料). 在JDK1.8中JVM改成使用元数据区来实现方法区.
- 元数据区和永久代有着本质的区别, 永久代属于虚拟机内存的一部分, 也就是说当在操做系统中启动虚拟机进程时为它分配了一块内存, 而虚拟机为永久代分配内存时用的是它本身分配得的内存.
- 而元数据区Metaspace是直接在本地内存(Native Memory)中申请的, 这样元数据区的大小(方法区大小)只会受本地内存大小限制, 和虚拟机进程所分得内存无关.
- 因此最后JVM内存模型图的终极版应该是这样子

若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级交流:787707172,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。