简述
"Write Once,Run Anywhere"(一次编译,到处运行)是sun宣传Java语言所提出的口号。Java语言跨平台的特性与Java虚拟机的存在密不可分。Java源代码通过编译生成.class文件字节码后再被JVM解释转化为目标机器代码,到处运行的关键与前提就是JVM。所以并不是Java语言本身可以可以跨平台,而是在不同的平台都有可以让Java语言运行的环境而已。在可以运行Java虚拟机的地方都内含一个JVM操作系统,从而使JAVA提供了各种不同平台上的虚拟机制,由此可见导出运行的关键就是JVM。
JVM内存区域
JVM结构
线程共享区域
堆 ①Java堆是Java虚拟机管理的内存中最大的一块。 ②堆中存放对象实例,几乎所有的对象实例都在这里分配内存,所以是GC主要区域。 ③不需要连续的内存 ④若堆中没有内存完成实例分配,则会抛出OutOfMemoryError异常 方法区 ①存储已被虚拟机加载的类信息(类名、访问修饰符、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码等数据 ②运行时常量池是方法区的一部分 ③这区域的内存回收目标主要是针对常量池的回收和对类型的卸载 ④不需要连续的内存,方法区gc性价比较低,gc频率远没有堆中高 ⑤若方法区无法满足内存分配需求时,则会抛出OutOfMemoryError异常线程隔离区域(随线程而生,随线程而灭)
虚拟机栈 ①生命周期与线程相同,管理Java方法执行的内存模型 ②方法执行创建栈帧(栈帧所需内存在类结构确定下来时就被已知,不考虑JIT优化)用于存储局部变量表、操作数栈、动态链接、方法出口等信息 ③栈的大小决定了方法调用的深度(递归多少层次,或嵌套调用多少层其他方法) ④若线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常 ⑤若栈中无忧内存可分配,则抛出OutOfMemoryError异常 本地方法栈 ①与虚拟机栈功能类似,但管理的不是Java方法,而是本地native方法 ②同样也会出现StackOverflowError、OutOfMemoryError异常 程序计数器 ①线程执行Java方法,计数器记录正在执行的虚拟机字节码指令地址;执行native方法,计数器为空(Undefined) ②此区域不会出现OutOfMemoryError异常OOM
堆溢出不断创建对象
/** * VM Args:-Xmx10m */ public static void main(String[] args) { List list = new ArrayList(); while (true){ list.add(new Object()); } } 复制代码
结果:
java.lang.OutOfMemoryError: Java heap space 栈溢出StackOverflowError异常:
递归调用
private void test(){ test(); } public static void main(String[] args) { new StackTest().test(); }复制代码
结果:
java.lang.StackOverflowErrorOutOfMemoryError异常:
/** * VM Args:-Xss2M * @author zzm */public class JavaVMStackOOM { private void dontStop(){ while (true){ } } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); }}复制代码
本人运行了一下果然像书(深入理解Java虚拟机)中所说电脑重启了,Windows系统的同学谨慎运行
方法区溢出/** * VM Args:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M (jdk8) * -XX:PermSize=10M -XX:MaxPermSize=10M(jdk7) * @author zzm */public class Test { static class OOMOjbect{} public static void main(String[] args) { while(true){ Enhancer eh = new Enhancer(); eh.setSuperclass(OOMOjbect.class); eh.setUseCache(false); eh.setCallback(new MethodInterceptor(){ @Override public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { return arg3.invokeSuper(arg0, arg2); } }); eh.create(); } }}复制代码
本人用的是jdk8,输出结果:
java.lang.OutOfMemoryError: Metaspace对象的内存布局
对象在内存中布局可以分成三块区域:对象头、实例数据和对齐填充
对象头
对象头包括两部分信息:
运行时数据: 运行时数据包括哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID和偏向时间戳等,这部分数据在32位和64位虚拟机中的长度分别为32bit和64bit,官方称为"Mark Word"。Mark Word被设计成非固定的数据结构,以实现在有限空间内保存尽可能多的数据。Mark Word的存储内容如下:存储内容 | 标志位 | 状态 |
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
实例数据
实例数据就是在程序代码中所定义的各种类型的字段,包括从父类继承的,这部分的存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响
对齐填充
并不是必然存在的,没有特别的含义。由于HotSpot的自动内存管理要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,对象头的数据正好是8的整数倍,所以当实例数据不够8字节整数倍时,需要通过对齐填充进行补全。
对象访问
句柄
使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:
直接指针
使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址
这两种对象访问方式各有优势:
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本oracle JDK官方默认虚拟机HotSpot采用第二中方式进行对象访问
感谢
《极客时间——杨晓峰Java核心技术36讲第一讲》
《深入理解Java虚拟机》