美文网首页studyJava学习笔记技术干货
深入理解java虚拟机-JVM高级特性和最佳实现(二)——了解j

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

作者: 6bc9f71c8f0c | 来源:发表于2018-02-01 11:45 被阅读233次
    每篇一叶

    前言

    上一回我们了解了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堆中对象分配、布局和访问的全过程
    • 对象的分配
    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上验证过。

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/ypxdzxtx.html