江苏建站管理系统信息,网站主题的分类,安徽省安徽省建设工程信息网站,专门做验收报告的网站深入理解java虚拟机第二章 二、运行时数据区域2.2.1 程序计数器2.2.2 Java虚拟机栈2.2.3 本地方法栈2.2.4 Java堆2.2.5 方法区2.2.6 运行时常量池2.2.7 直接内存 三、HotSpot虚拟机对象解密2.3.1 对象的创建对象如何分配内存#xff1f;对象的创建如何处理并发问题#xff1f… 深入理解java虚拟机第二章 二、运行时数据区域2.2.1 程序计数器2.2.2 Java虚拟机栈2.2.3 本地方法栈2.2.4 Java堆2.2.5 方法区2.2.6 运行时常量池2.2.7 直接内存 三、HotSpot虚拟机对象解密2.3.1 对象的创建对象如何分配内存对象的创建如何处理并发问题 2.3.2 对象的内存布局2.3.3 对象的访问定位 四、实战OutOfMemoryError异常2.4.1 Java堆溢出2.4.2 虚拟机栈和本地方法栈溢出2.4.3 方法区和运行时常量池溢出2.4.4 本机直接内存溢出 参考资料《深入理解Java虚拟机》 二、运行时数据区域 2.2.1 程序计数器
程序计数器记录了当前线程所运行的字节码以及之后要运行的字节码。在jvm中字节码解释器通过改变程序计数器的值来执行下一条字节码命令。
jvm是通过线程轮流切换达到多线程效果的
2.2.2 Java虚拟机栈
方法被执行时java虚拟机会同步创建一个栈帧。栈帧包括局部变量表、操作数栈、动态链接、方法出口等信息
每一个方法被执行直到被执行完毕的过程对应着一个栈帧在虚拟机中入栈到出栈的过程
局部变量表存放了基本数据类型以及对象引用所以当进入一个方法时这个方法需要在栈帧中分配多大的局部变量空间是完全确定的在方法运行期间不会改变局部变量表的大小 StackOverflowError线程请求的栈深度大于虚拟机所允许的栈深度就会抛出此异常 OutofMemoryError虚拟机栈容量动态扩容申请内存时无法申请到适当的内存就会抛出此异常。由于HotSpot无法动态扩容所以基本上不会抛出OOM。但如果一开始申请空间就失败也会抛出OutofMemoryError
2.2.3 本地方法栈
本地方法栈是为虚拟机执行本地方法服务的
HotSpot虚拟机将本地方法栈与虚拟机栈合二为一
2.2.4 Java堆
Java堆是被所有线程共享的一块区域在虚拟机启动时创建。
The heap is the runtime data area from which memory for all class instances and arrays is allocated
堆是为所有类实例对象和数组分配空间的一块运行时内存。
此处的“所有”不是那么严谨的因为由于栈上分配、标量替换等优化手段已经导致一些微妙的变化悄然发生所以Java对象实例都分配在堆上也渐渐变得不那么绝对了。栈上分配即有一些对象没有必要存储在堆内存中将其存储在栈中的内存优化方式
Java堆是垃圾收集器管理的内存区域现代的垃圾收集器大部分是采用分代收集理论设计的但到了今天HotSpot里面也出现了不采用分代设计的新垃圾收集器
Java堆既可以被实现成固定大小的也可以是可扩展的。如果Java堆中没有内存完成实例分配并且堆也无法扩展时Java虚拟机会抛出OutOfMemoryError异常
2.2.5 方法区
方法区与Java堆一样是各个线程共享的内存区域它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。但是现在来看这种设计导致了Java应用更容易遇到内存溢出的问题所以到了JDk7的HotSpot已经把原本放在永久代的字符串常量池、静态变量等移植到Java堆中。到了JDK8完全废弃了永久代的概念改用在本地内存中实现的元空间来代替把JDK7中永久代还剩余的内容主要是类型信息全部移到元空间中。
方法区主要存放的是Class信息而堆中主要存放的是实例化的对象多个线程同时加载一个类时只能有一个线程加载该类其他线程要等该线程加载完毕之后直接使用该类类只能加载一次方法区的大小决定了系统可以保存多少个类如果系统定义了太多的类导致方法区溢出虚拟机同样会抛出OOM
2.2.6 运行时常量池
运行时常量池是方法区的一部分Class文件中有一项信息是常量池表用于存放编译器生成的各种字面量和符号引用这部分内容在类加载后存放到方法区的运行时常量池中。
一般来说除了保存Class文件中描述的符号引用外还会把有符号引用翻译出来的直接引用也存储在运行时常量池中。
2.2.7 直接内存
直接内存并不是虚拟机运行时数据区的一部分但是这部分内存被频繁地使用而且也可能导致OutOfMemoryError异常出现。
简单来讲就是使用Java堆里面的DirectByteBuffer对象作为一块堆外内存的引用对堆外内存进行操作这样可以在一些场景中显著提高性能因为避免了在Java堆和Native堆中来回复制数据。
三、HotSpot虚拟机对象解密
本节将深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程
2.3.1 对象的创建
检查这个指令的参数是否能在常量池中定位到一个类的符号引用检查这个符号引用代表的类是否已经被加载为新生对象分配内存—将分配到的内存空间不包括对象头对对象进行必要的设置如对象所属类、如何找到类的元数据信息、对象的hash码、对象的GC分代年龄信息存放在对象头中执行构造函数对对象的实例字段赋值
对象如何分配内存
指针碰撞
假设Java堆中的内存是绝对规整的所有被使用过的内存都被放在一边空闲的内存都被放在另一边。中间放着一个指针作为分界点的治时期分配内存时仅仅把指针向空闲空间方向移动一段对象大小的距离这种方式就称为“指针碰撞”。
空闲列表
假设Java堆内存并不规整虚拟机就必须维护一个列表记录哪块内存是可用的再分配的时候从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录。
选择
选择哪种对象根据Java堆是否规整决定Java堆是否规整由所采用的垃圾收集器是否带有空间压缩整理的能力决定。当Serial、ParNew等带压缩整理过程的收集器时系统采用的分配算法是指针碰撞既简单又高效。当使用CMS这种基于清除算法的收集器时理论上就只能采用较为复杂的空闲列表来分配内存。
对象的创建如何处理并发问题
对分配内存空间的动作进行同步处理
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
分配缓冲
为每个线程在Java堆中预先分配一小块内存成为本地线程分配缓冲哪个线程要分配内存就在哪个线程的本地缓冲区中分配只有本地缓冲区用完了分配新的缓冲区时才需要加锁。
2.3.2 对象的内存布局
对象在堆内存中的存储布局可以划分为三个部分对象头、实例数据、对齐填充
对象头Mark Word
对象需要存储的运行时数据很多已经超出了32、64位Bitmap结构所能记录的最大限度但对象头里的信息是与对象自身定义的数据无关的额外存储成本考虑到虚拟机的空间效率Mark Word被设计成一个有着动态定义的数据结构以便在极小的空间内存储尽量多的数据根据对象的状态复用自己的存储空间
HotSpot虚拟机对象对象头部分包括两类信息
1、对象自身运行时数据哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
2、类型指针即对象指向它的类型元数据的指针Java虚拟机通过这个指针来确定该对象是哪个类的实例
3、如果对象是一个Java数组那在对象头中还必须有一块用于记录数组长度的数据
实例数据
HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops对象指针 Ordinary Object Pointers
JVM重新分配字段的排列顺序受-XX:FieldsAllocationStyle参数的影响默认值为1实例字段的重新分配策略遵循以下规则
如果一个字段占用X个字节那么这个字段的偏移量OFFSET需要对齐至NX 偏移量是指字段的内存地址与Java对象的起始内存地址之间的差值。比如long类型的字段它内存占用8个字节那么它的OFFSET应该是8的倍数8N。不足8N的需要填充字节。 在开启了压缩指针的64位JVM中Java类中的第一个字段的OFFSET需要对齐至4N在关闭压缩指针的情况下类中第一个字段的OFFSET需要对齐至8N。
JVM默认分配字段的顺序为long / doubleint / floatshort / charbyte / booleanoops(Ordianry Object Point 引用类型指针)并且父类中定义的实例变量会出现在子类实例变量之前。当设置JVM参数-XX CompactFields 时默认占用内存小于long / double 的字段会允许被插入到对象中第一个 long / double字段之前的间隙中以避免不必要的内存填充。
对齐填充
占位符
2.3.3 对象的访问定位
主流的访问方式主要有使用句柄和直接指针两种
句柄
如果使用句柄访问的话Java堆中可能会划分出一块内存来作为句柄池reference中存储的就是对象的句柄地址句柄中包含了对象实例数据与类型数据各自具体的地址信息 直接指针
reference中存储对象地址对象中存储类型地址不需要多一次间接的访问开销 使用直接指针可以节省一次指针定位的时间开销对于HotSpot而言它主要使用第二种方式进行对象访问
四、实战OutOfMemoryError异常
2.4.1 Java堆溢出
Java堆内存溢出异常测试
VM Options: -Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError
最小/大堆容量20mb 导出第一次堆转存储快照文件
public class Main{static class OOMObject{int anInt;public OOMObject(int anInt){this.anInt anInt;}}public static void main(String[] args){ListOOMObject list new ArrayList();while (true){list.add(new OOMObject(1));}}
}java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid17568.hprof ...
Heap dump file created [31422169 bytes in 0.076 secs]、
堆转储快照文件查看内存的分布情况
Exception in thread main java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3210)at java.util.Arrays.copyOf(Arrays.java:3181)at java.util.ArrayList.grow(ArrayList.java:267)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)at java.util.ArrayList.add(ArrayList.java:464)at 算法复杂度.Main.main(Main.java:18)2.4.2 虚拟机栈和本地方法栈溢出
两种栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度抛出StackOverflowError异常如果虚拟机的占内存允许动态扩展当扩展栈容量无法申请到足够的内存时抛出OutOfMemoryError异常
虚拟机栈和本地方法栈测试
参数-Xss128k
public class Main{private int stackLength 1;public void stackLeak(){stackLength;stackLeak();}public static void main(String[] args){Main main new Main();try {main.stackLeak();}catch (Throwable e){System.out.println(stack length: main.stackLength);throw e;}}
}stack length:983
Exception in thread main java.lang.StackOverflowError虚拟机栈和本地方法栈测试
public class Main{private static int stackLength 0;public static void test() {long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9,unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17,unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25,unused26, unused27, unused28, unused29, unused30, unused31, unused32, unused33,unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41,unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49,unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57,unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65,unused66, unused67, unused68, unused69, unused70, unused71, unused72, unused73,unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81,unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89,unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97,unused98, unused99, unused100;stackLength;test();unused1 unused2 unused3 unused4 unused5 unused6 unused7 unused8 unused9 unused10 unused11 unused12 unused13 unused14 unused15 unused16 unused17 unused18 unused19 unused20 unused21 unused22 unused23 unused24 unused25 unused26 unused27 unused28 unused29 unused30 unused31 unused32 unused33 unused34 unused35 unused36 unused37 unused38 unused39 unused40 unused41 unused42 unused43 unused44 unused45 unused46 unused47 unused48 unused49 unused50 unused51 unused52 unused53 unused54 unused55 unused56 unused57 unused58 unused59 unused60 unused61 unused62 unused63 unused64 unused65 unused66 unused67 unused68 unused69 unused70 unused71 unused72 unused73 unused74 unused75 unused76 unused77 unused78 unused79 unused80 unused81 unused82 unused83 unused84 unused85 unused86 unused87 unused88 unused89 unused90 unused91 unused92 unused93 unused94 unused95 unused96 unused97 unused98 unused99 unused100 0;}public static void main(String[] args) {try {test();} catch (Error e) {System.out.println(stack length: stackLength);throw e;}}
}stack length:52
Exception in thread main java.lang.StackOverflowError结果无论是由于栈帧太大还是虚拟机栈容量太小当新的栈帧内存无法分配的时候HotSpot虚拟机抛出的都是StackOverflowError异常。
2.4.3 方法区和运行时常量池溢出
运行时常量池导致的内存溢出异常
public class RuntimeConstantPollOOM{public static void main(String[] args){SetString set new HashSetString();short i 0;while(true){set.add(String.valueOf(i).intern());}}
}Exception in thread main java.lang.OutOfMemoryError: PermGen space此方法使用JDK6运行原因是在JDK7以后原本存放在永久代的字符串常量池被移到Java堆中所以在JDK7及以上版本限制方法区的容量对测试用例来说是毫无意义的
String.intern()返回引用的测试
public class RuntimeConstantPollOOM{public static void main(String[] args){String str1 new StringBuilder(计算机).append(软件).toString();System.out.println(str1.intern() str1);String str2 new StringBuilder(ja).append(va).toString();System.out.println(str2.intern() str2);}
}JDK6中会得到两个false
JDK7中会得到一个true和一个false
原因是JDK6中intern方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储返回的也是永久代中的字符串实例的引用但是由StringBuilder创建的字符串对象实例在Java堆上所以必然不是同一个引用 JDK7的intern方法实现不需要拷贝字符串实例既然字符串常量已经移到了Java堆中到了JDk7的HotSpot已经把原本放在永久代的字符串常量池、静态变量等移植到Java堆中那只需要在常量池里记录一下首次出现的实例引用即可因此intern返回的引用和StringBuilder创建的字符串实例就是同一个
借助CGLib使得方法区出现内存溢出异常
public class JavaMethodAreaOOM{public static void main(String[] args){while(true){Enhancer enhancer new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor(){public Object intercept(Object obj , Method method , Object[] args, MethodProxy proxy)throws Throwable{return proxy.invokeSuper(obj,args);}});enhancer.create();}}static class OOMObject{}
}java.lang.OutOfMemoryError:PermGen space在JDK8以后永久代便完全退出了历史舞台到了JDK8完全废弃了永久代的概念改用在本地内存中实现的元空间来代替把JDK7中永久代还剩余的内容主要是类型信息全部移到元空间中。
2.4.4 本机直接内存溢出
使用unsafe分配本机内存
public class DirectMemoryOOM{private static final int _1MB 1024 * 1024;public static void main(String[] args)throws Exception{Field unsafeField Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe (Unsafe) unsafeField.get(null);while(true){unsafe.allocateMemory(_1MB);}}
}Exception in thread main lava.lang.OutOfMemoryError由直接内存导致的内存溢出一个明显特征是Heap Dump文件中不会看见有什么明显的异常情况。