JVM学习笔记1:Java虚拟机内存模型
jvm学习笔记1:java虚拟机内存模型
学习jvm,java虚拟机对理解java程序执行过程和java程序性能调优具有很大帮助。本系列博客旨在由浅到深学习并理解jvm。参考阅读:《深入理解java虚拟机-jvm高级特性和最佳实践》。这个书写的非常好,推荐有条件的读者买一本来阅读,网上也有电子版的。本系列基于hotspot虚拟机。
欢迎转载,转载请注明出处;笔者水平有限,错误之处欢迎指正!
一、java虚拟机内存划分
java虚拟机内存区域按线程是否私有可以分为:
线程共享:方法区(含运行时常量池)、堆。
线程私有:虚拟机栈、本地方法栈、程序计数器。
下面的图可以帮助我们很直观的理解jmm。
下面图来自programcreek,这个网站也是很有意思的网站
第二个图来自《深入理解java虚拟机-jvm高级特性和最佳实践》
1.线程私有区
(1)程序计数器
线程是cpu调度的基本单位。每条线程使用一个独立的程序计数器去记录其正在执行的字节码指令地址。如果线程正在执行的是一个 java方法,计数器记录的是正在执行的字节码指令的地址;如果正在执行的是 native 方法,则计数器的值为空。程序计数器是唯一一个没有规定任何outofmemoryerror
的区域。异常简写:outofmemoryerror,oom;sof,*error。下同。
(2)虚拟机栈
线程私有,java方法执行的模型。(创建线程)执行每个方法时会创建一个栈帧。栈帧包含:局部变量表、操作数栈、动态链接、方法出口。
局部变量表:基本类型(int,short,long,byte,float,double,boolean,char)和对象句柄(引用)。
异常情况:-xss设置虚拟机栈大小,即深度(递归层次)当,栈深度>虚拟机最大栈深度,抛sof;当申请栈内存大小不够时,抛oom。
(3)本地方法栈
执行native方法,native方法不是java方法,由虚拟机实现,本地方法栈会抛sof和oom。
2.线程共享区
(1)java堆
存放对象(实例),包含对象和数组。按gc情况分为新生代和老年代。物理内存可以不连续只有逻辑连续就可。垃圾收集(gc)会在之后的博客详解。
堆相关虚拟机参数有:-xmx:最大对容量 -xms:最小堆容量。
异常情况:如果堆内存无法分配实例(对象),堆内存不够时,抛oom。
(2)方法区
线程共享,不需要物理连续内存,存放被jvm加载的类信息、常量、静态变量、即时编译的代码。有时会也称“永久代”。永久代已被移除,不再讨论。
异常情况:方法区内存不足,抛oom。
①运行时常量池
存放编译期生成的字面常量和符号引用。抛oom
字面常量:字符串、final常量值。
符号引用:类(接口)的全限定名称、字段的名称和描述符、方法的名称和描述符。
(3)方法区回收情况
常量池回收和对类型的卸载。
常量池回收判断条件:没有指向该常量(实例)的引用。
回收类型判断条件:①类的所有所有实例都被回收;②加载该类的classloader被回收;③该类的class对象没有任何地方被引用,无法通过反射访问该类
二.java对象在jvm的创建和访问定位
1.对象创建过程
(1)检查所要new的类是否加载,没有加载则执行类加载。类加载一般分为加载、链接、初始化,之后的博客我会讲类加载机制。
(2)类加载完成,为对象分配内存。
为对象分配内存可以分两种情况。
堆内存规整:指针碰撞,把分界指针向空闲内存移动一段对象内存大小的距离。
堆内存不规整:空闲列表,维护一个列表,从列表中查找足够的内存保存对象(实例)。
(3)jvm将分配的内存初始化为零值。
(4)执行
2.对象在虚拟机(堆)的访问定位
开发者通过操作引用(在栈上)来操作对象或实例(在堆上)。通过引用操纵对象的访问方式有句柄访问和直接指针访问。
句柄访问:堆上有句柄池,栈中的reference执行对象的句柄地址,句柄保存对象实例数据(在堆上)和类型数据(在方法区)的地址。如图:
直接指针访问:reference保存对象地址。如图:
三.内存异常情况分析
1、java堆溢出(oom)
不断生成大对象,并保证不被gc回收。看下面简单的实例:
jvm执行参数:-verbose:gc -xms20m -xmx10m-xx:+printgcdetails -xx:survivorratio=8。
/** * vm args:-xms40m -xmn10m */ public class oominheap { static class oominstance{} public static void main(string[] args){ list<oominstance> list=new arraylist<>(); while(true){ list.add(new oominstance()); } } }
测试结果:
exception in thread "main" java.lang.outofmemoryerror: java heap space at java.base/java.util.arrays.copyof(arrays.java:3720) at java.base/java.util.arrays.copyof(arrays.java:3689) at java.base/java.util.arraylist.grow(arraylist.java:237) at java.base/java.util.arraylist.grow(arraylist.java:242) at java.base/java.util.arraylist.add(arraylist.java:485) at java.base/java.util.arraylist.add(arraylist.java:498) at com.zafkiel.oominheap.main(oominheap.java:14)
2、栈溢出(sof/oom)
(1).sof
线程请求的栈深度大于虚拟机栈允许的最大深度,抛sof。比如,没有出口的递归方法调用。
测试实例:
/** * vm args: -xss128k * @author zafkiel */ public class sofinstack { private int depth=1; public void sof(){ depth++; sof(); } public static void main(string[] args) throws throwable{ sofinstack sof=new sofinstack(); try{ sof.sof(); }catch (throwable e){ system.out.println("stack lenth:"+sof.depth); throw e; } } }
测试结果:
exception in thread "main" java.lang.*error stack lenth:7205 at com.zafkiel.sofinstack.sof(sofinstack.java:11)
(2).oom
拓展栈时无法申请足够的内存,则抛oom。这种情况可以通过不停创建线程来测试。有兴趣的读者可以测试一下,注意windows可能会假死,丢虚拟机测比较安全。
3.方法区和运行常量池溢出(oom)
此区域可以通过不停生成类来填满方法区,也可以通过动态类生成相关技术来实现。下面用cglib动态代理来测试方法区溢出:
测试实例:
/** * vm args: -xx:metaspacesize=10m -xx:maxmetaspacesize=5m * @author zafkiel */ public class oominmethodarea { static class oom{ static int[] array=new int[1024*1024]; } public static void main(string[] args) throws throwable{ try{ while (true){ enhancer enhancer=new enhancer(); enhancer.setsuperclass(oom.class); enhancer.setusecache(false); enhancer.setcallback((methodinterceptor) (o, method, objects, methodproxy) -> methodproxy.invokesuper(o,objects)); enhancer.create(); } }catch (throwable e){ system.out.println("异常信息:"+e+":"+e.getmessage()); } } }
测试结果(部分输出):
异常信息:java.lang.outofmemoryerror: metaspace:metaspace
这里使用的是jdk11,永久代从jdk8之后被移到元数据区,所以jvm参数配置metaspacesize。
总结
对java内存模型作下简单总结: