深入理解java虚拟机-JVM高级特性和最佳实现(二)——了解j
前言
上一回我们了解了java的历史背景和JVM的一些版本,这次我们要探索java的内存区域和内存溢出。
java和C++之间有一睹由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。java程序员将内存控制的权力交给了java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。
java虚拟机运行时数据区
基本概念
- 程序计数器
线程隔离,一块较小的内存空间,当前程序所执行的字节码的行号指示器。唯一一个在java虚拟机规范中没有指定任何OutOfMemoryError情况的区域 - java虚拟机栈
线程隔离,生命周期和线程相同,每个方法执行的同时都会产生一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表存放编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象的引用(reference类型,不等同与对象本身,有可能是指向起始地址的引用指针,也可能是一个代表对象的句柄活其他与对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
会出现的异常情况: StackOverflowError,线程请求的栈深度大于虚拟机所允许的深度;OutOfMemoryError,虚拟机栈可以动态扩展,扩展无法申请到足够内存。 - 本地方法栈
线程隔离,虚拟机栈为虚拟机指向java方法服务,本地方法栈为虚拟机提供Native方法服务。 - java堆
线程内存共享。虚拟机所管理内存中最大的一块,所有线程共享。虚拟机规范中描述:所有对象实例以及数组都要在堆上分配。JIT编译器的发展后这个也不是绝对了GC的主要区域。收集器基本上采用分代收集算法-->java堆分为新生代和老年代。 - 方法区
线程内存共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据,java虚拟机任务是堆的一个逻辑部分,但是别名为非堆。有人称为永久代,其实并不等价,只是HotSopt虚拟机将GC分代收集扩展到了方法区,其他虚拟机并不存在永久代的概率。目前JDK1.7的HotSpot中已经将原本放在永久代的字符串常量池移出。 - 运行时常量池
线程内存共享。方法区的一部分 - 直接内存
并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分内存也被频繁使用。JDK1.4映入NIO,基于通道与缓冲区的I/O方式。利用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免java堆和Natie堆中来回复制数据。
HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程。
HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程- 对象的分配
- new指令--检查指令参数是否能在常量池中定位到类的符号引用,检查这个符号代表的类是否已经加载、解析,初始化,没有,执行类加载过程
- 类加载检查通过--为新生对象分配内存--
- 堆内存绝对规整,指针碰撞分配方式,用过的内存放一边,空闲内存放一边,中间放一个指针作为分界点指示器。
- 堆内存不规整。空闲列表分配方式。
- 具体采用什么分配方式取决于java堆是否规整。java堆是否规准由采取的垃圾收集器是否带有压缩整理功能决定。
- Serial、ParNew等带Compact过程的收集器,分配采用指针碰撞分配
- 使用CMS这种基于Mark-Sweep算法的收集器通常采用空闲列表。
- 分配内存并发情况,
- 动作同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
- 内存分配按照线程划分在不同的空间进行。即每个线程在java堆中预先分配一小块内存,本地线程分配缓冲(TLAB),哪个线程需要分配内存就在哪个线程的TLAB伤分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
- 内存分配完后,虚拟机将分配的内存空间初始化为零值。使用TLAB,在TLAB分配时就直接进行这步。保证对象实例字段在java代码中不赋值就能直接使用。
- 设置对象。对象所属哪个类的实例,如何查找类的元数据信息,对象的哈希码,对象的GC分代年龄等,这些信息存放在对象头中。
- 上面完成后,虚拟机视角,一个新对象已经产生了,java程序视角,创建才刚刚开始,init还没执行,所有字段还为零。
-
对象的访问
通过句柄访问对象
通过指针访问对象
建立对象-->使用对象。java程序通过栈上的reference数据来操作堆上的具体对象。java虚拟机规范规定reference类型指向一个对象。
句柄好处:reference中存储的是句柄地址,在对象呗移动时只会改变句柄中的实例指针,而reference本身不修改
指针好处:速度更快,节省了一次指针定位的时间开销。就Sun HotSpot而言,它是使用第二种方式进行对象访问的。但从整个软件开发范围来看,句柄访问的情况也十分常见。
- 句柄。reference存储句柄地址。句柄中包含对象实例数据与类型数据各自的具体地址
- 指针。java堆对象中必须考虑放置访问类型数据的相关信息。reference中存储的是对象地址。
实战OutOfMemoryError
先设置VM args -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
- 堆内存溢出程序
/**
* VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOP {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true){
list.add(new OOMObject());
}
}
}
异常信息
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.zwq.heap.HeapOOP.main(HeapOOP.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
- 虚拟机栈和本地方法栈溢出
public class JavaVMStackSop {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackSop oom = new JavaVMStackSop();
try {
oom.stackLeak();
}catch (Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}
}
异常信息
Exception in thread "main" java.lang.StackOverflowError
stack length:11387
at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:7)
at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:8)
...
- 方法区和运行时常量池溢出
/**
* 方法区和运行时常量池溢出
* String.intern()是一个native方法,在字符串常量池有则直接返回,没有则添加到常量池中
* JDK1.6及以前版本常量池在永久代内,通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,
*报错java.lang.outofmemoryerror:PermGen space
* 从而限制常量池容量
* 而JDK1.7后不会出现这个问题,会一直执行下去
*/
public class RunntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i=0;
while(true){
list.add(String.valueOf(i).intern());
}
}
}
/**
* 方法区内存溢出
*/
public class JavaMethodAreaOp {
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 o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
}
static class OOMObject{
}
}
需要配置cglib包引用
<!-- [https://mvnrepository.com/artifact/cglib/cglib](https://mvnrepository.com/artifact/cglib/cglib) -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
附:由于jdk6.0及以下版本和JDK7.0以上版本存在差异,以下代码运行结果不一致
public class RunntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuffer().append("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuffer().append("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
jdk1.6结果
false
false
jdk1.8结果
true
false
出现差异原因:String.intern()是一个Native方法,作用:jdk1.6及以前版本,如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象否则,将String对象包含的字符串添加到常量池中,并返回此String对象的引用。jdk1.7后intern()方法不会再复制实例,只是在常量池中记录首次出现的实例引用,由于“java”之前就出现,不符合首次首先,所以返回false,“计算机软件”首次出现,则返回true
- 本机直接内存溢出
/**
* 本机直接内存溢出
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}
异常信息
java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.zwq.heap.DirectMemoryOOM.main(DirectMemoryOOM.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
附:文中流程图为原创,代码来源周志明《java虚拟机 Jvm高级特性和最佳实现》,代码结果在idea上验证过。