studyJava学习笔记技术干货

深入理解java虚拟机-JVM高级特性和最佳实现(二)——了解j

2018-02-01  本文已影响233人  6bc9f71c8f0c
每篇一叶

前言

上一回我们了解了java的历史背景和JVM的一些版本,这次我们要探索java的内存区域和内存溢出。
java和C++之间有一睹由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。java程序员将内存控制的权力交给了java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。


java虚拟机运行时数据区

基本概念

HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程。

HotSpot 虚拟机在java堆中对象分配、布局和访问的全过程
  1. new指令--检查指令参数是否能在常量池中定位到类的符号引用,检查这个符号代表的类是否已经加载、解析,初始化,没有,执行类加载过程
  2. 类加载检查通过--为新生对象分配内存--
    1. 堆内存绝对规整,指针碰撞分配方式,用过的内存放一边,空闲内存放一边,中间放一个指针作为分界点指示器。
    2. 堆内存不规整。空闲列表分配方式。
  3. 具体采用什么分配方式取决于java堆是否规整。java堆是否规准由采取的垃圾收集器是否带有压缩整理功能决定。
    1. Serial、ParNew等带Compact过程的收集器,分配采用指针碰撞分配
    2. 使用CMS这种基于Mark-Sweep算法的收集器通常采用空闲列表。
  4. 分配内存并发情况,
    1. 动作同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
    2. 内存分配按照线程划分在不同的空间进行。即每个线程在java堆中预先分配一小块内存,本地线程分配缓冲(TLAB),哪个线程需要分配内存就在哪个线程的TLAB伤分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
  5. 内存分配完后,虚拟机将分配的内存空间初始化为零值。使用TLAB,在TLAB分配时就直接进行这步。保证对象实例字段在java代码中不赋值就能直接使用。
  6. 设置对象。对象所属哪个类的实例,如何查找类的元数据信息,对象的哈希码,对象的GC分代年龄等,这些信息存放在对象头中。
  7. 上面完成后,虚拟机视角,一个新对象已经产生了,java程序视角,创建才刚刚开始,init还没执行,所有字段还为零。

建立对象-->使用对象。java程序通过栈上的reference数据来操作堆上的具体对象。java虚拟机规范规定reference类型指向一个对象。
句柄好处:reference中存储的是句柄地址,在对象呗移动时只会改变句柄中的实例指针,而reference本身不修改
指针好处:速度更快,节省了一次指针定位的时间开销。就Sun HotSpot而言,它是使用第二种方式进行对象访问的。但从整个软件开发范围来看,句柄访问的情况也十分常见。

  1. 句柄。reference存储句柄地址。句柄中包含对象实例数据与类型数据各自的具体地址
  2. 指针。java堆对象中必须考虑放置访问类型数据的相关信息。reference中存储的是对象地址。

实战OutOfMemoryError

先设置VM args -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

  1. 堆内存溢出程序
/**
* 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)

  1. 虚拟机栈和本地方法栈溢出
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)
...

  1. 方法区和运行时常量池溢出
/**
* 方法区和运行时常量池溢出
* 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

  1. 本机直接内存溢出
/**
* 本机直接内存溢出
*/
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上验证过。

上一篇下一篇

猜你喜欢

热点阅读