JVM划分内存之道

01. 概述

  • java由jvm执行的
  • jvm划分java内存区域。

02. java程序执行的流程

  1. java文件被java编译器编译为字节码文件(.class)。
  2. jvm类加载器加载类字节码文件。
  3. 加载完毕之后,交由jvm执行引擎执行。
  • 分析:在整个程序执行过程中,jvm会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),即 jvm内存。因此,在Java中常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
    在这里插入图片描述

03. jvm内存

jvm内存划分为:程序计数器(program counter register)、java栈(jvm stack)、本地方法栈(native method stack)、方法区(method area)、堆(heap)。
在这里插入图片描述

3.1. 程序计数器(program counter register)

  • 别名
    PC寄存器
  • 特点

    α. 线程创建时, 创建。
    β. 指向下一条指令执行的地址。
    γ. 线程私有
       原因:一个处理器,某一时刻只会执行一条线程的指令
    δ.执行native方法时,PC值为undefined。
    ε.Java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的区域。
       解释:只存储下一条指令执行的地址,因此占用的空间不会随程序的执行而发生改变,也就不会发生内存溢出现象。

3.2. java栈(jvm stack)

  • 概述
  • java方法执行的内存模型。
  • java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。在栈帧中包括:

    α. 局部变量表(local variables)
    β. 存放基本类型(boolean、byte、char、short、int、float、long、double),其中long、double占用2个局部变量空间。
    γ. 对象引用(refrence), 即对象起始地址的指针
    δ. returnAddress类型
    ε. 操作数栈(operand stack)
    ζ. 指向当前方法所属类的运行时常量池引用(Reference to runtime constant pool)
    ν. 方法返回地址(return address)
    ξ. 附加信息

  • 别名
    虚拟机栈
  • 特点

    α. 线程私有,一个线程对应一个java栈。
    β. 存放栈帧。每个栈帧对应一个被调用的方法。
    γ. 调用方法时,创建一个对应的栈帧, 压栈到栈顶。
    δ. 栈顶指向线程当前正在调用的方法。
    ε. 方法调用完毕,栈顶栈帧出栈。
    ζ. 抛异常的情况
        如果线程请求的栈深度超出虚拟机允许的深度,抛出StackOverflowError异常。
        如果无法申请到内存,抛出OutOfMemoryError异常。

  • JVM设置参数
    -Xss 栈内存在jvm中,默认为1mb

3.3. 本地方法栈(native method stack)

  • 概述
    与java栈类似。
    区别:本地方法栈为执行native方法的空间,java栈为执行java方法的空间。
  • 抛异常的情况
    如果线程请求的栈深度超出虚拟机允许的深度,抛出StackOverflowError异常。
    如果无法申请到内存,抛出OutOfMemoryError异常。

3.4. 方法区(method area)

  • 概述
    α. 被Java虚拟机描述为堆的一个逻辑部分。习惯叫永久代。永久代也会垃圾回收,主要针对常量池回收,类型卸载(比如反射生成大量临时使用的Class等信息)。
    β. java8中已经没有方法区了,取而代之的是元空间(metaspace)。
  • 别名
    永久代
  • 特点

    α. 线程共享。
    β. 存储虚拟机加载的类信息、常量、静态变量等。
    γ. 针对常量池的回收,以及类型的卸载。
    δ. 抛出异常的情况
        当方法区满时,无法再分配空间,就会抛出内存溢出的异常(OutOfMemoneyError)。

  • JVM配置参数
    -XX:MaxPermSize=64m 方法区占用的最大内存
    -XX:PermSize=64m 方法区分配的初始内存

3.5. 运行时常量池

  • 用途
    存放编译器运行期生成的字面量和符号引用,且类加载后存放到常量池中。
    jdk1.6及之前字符串常量池位于方法区之中。
    jdk1.7字符串常量池已经被挪到堆之中。

    α. 字面量
        常量,如字符串、final常量值。
    β. 符号引用
        编译原理方面的概念,包括:类和接口的完全限定名、字段名称和描述符、方法的名称和描述符。

  • 特点

    α. 方法区的一部分。
    β. 线程共享。
    γ. 动态性
        java语言,常量在编译期、运行期间产生,存放于运行时常量池中。常量包括:基本类型包装类(不管理浮点型,整型只管理-128 ~ 127)、String(也可通过String.intern()强制将String放入常量池)。

  • 优点

    α. 对象共享, 避免频繁创建和销毁对象而影响系统性能。
    β. 节省内存空间。
    γ. 节省运行时间。
        比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值地址是否相等。

  • 基本类型
    public final class Integer extends Number implements Comparable<Integer> {
        public static Integer valueOf(int i) {
            assert IntegerCache.high >= 127;
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }
    } 
    
    public final class String
    	implements java.io.Serializable, Comparable<String>, CharSequence {
    	// 将对象加入到常量池之中
    	public native String intern();
    }
    

3.6. 堆(heap)

  • 概述
    存储Java实例和对象的地方,是GC的主要区域,线程共享的内存区域。
  • 特点

    α. 线程共享。
    β. 虚拟机启动,创建。
    γ. 存放对象实例和数组。
    δ. 垃圾收集器管理的主要区域。
    ε. 根据分带收集算法分
    在这里插入图片描述

  • 细分
    -XX:NewRatio 设置新生代与老年代比值。如:-XX:NewRatio=4,即新生代:老年代=1:4。
    • 新生代

      存放新创建的占用空间小的实例、以及新生代垃圾收集没有超过设置次数(默认15次)的实例。

      补充:占用空间大的实例(如Array),直接存放到老年代。优点:a. 防止占用空间大的对象创建,导致新生代还有大量剩余空间,而提前发生gc;b. 防止在eden区和survivor区的大对象复制造成性能问题。
      -XX:PretenureSizeThreshold 实例超过该值直接存储到老年代。

    • 老年代
      存放新生代垃圾收集超过设置次数(默认15次)的实例,以及一些占用空间大的实例(比如缓存)。

  • 更细致一点:
    • eden空间
      新创建的对象(不包括占用空间大的对象)存放在eden区。

    • from和to

      当eden区占满时,存活的实例复制到from区。
      当from区占满时,minor gc将eden、from存活的实例复制到to区,同时清空from和eden,以及交换from和to。
      注意:如果to区不能容纳minor gc之后的实例,那么超过占用to空间最多实例存活年代的对象将被移到old区。

      hotspot虚拟机新生代eden和survivor大小比值为4:1,因为有两个survivor,因此eden:from:to比值为4:1:1。

  • 抛异常的情况
    在堆中没有内存分配实例,并且无法再扩展时,将抛出OutOfMemoryError异常。
  • 设置参数
    -Xms 最小堆空间,如-Xms512m
    -Xmx 最大堆空间,如-Xmx1g
    -Xmn 设置新生代内存大小,如-Xmn2g
    -XX:SurvivorRatio 设置survivor:eden比值,如:-XX:SurvivorRatio=4,则2个survivor与一个eden的比值为2:4。
    在JVM中,最小堆空间默认物理内存的1/64,最大堆内存在JVM中默认物理内存1/4,且建议最大堆内存不大于4G。

3.7. 直接内存

  • 概述

    α. 直接内存不属于jvm运行时数据区,也不属于JVM规范中定义的内存区域。
    β. jdk1.4引入nio(new input/output)类,nio是基于通道(channel)与缓冲区(buffer)的IO方式,使用native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。能在一些场景中显著提高性能,避免在java堆和native堆中来回复制数据。
    γ. 直接内存的分配不会受到Java堆大小的限制,受到本机总内存大小限制。
    δ. 配置虚拟机参数时,不要忽略直接内存,防止出现OutOfMemoryError异常。

  • 特点

    α. 直接内存申请空间耗费更高的性能,频繁申请到一定量时尤为明显。
    β. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。

  • 使用场景

    α. 有很大的数据需要存储,它的生命周期很长。
    β. 适合频繁的IO操作,例如网络并发场景。

  • 直接内存和非直接内存对比
    /** 直接内存与堆内存的比较 **/
    public class ByteBufferCompare {
        public static void main(String[] args) {
            allocateCompare();   //分配比较
            operateCompare();    //读写比较
        }
        
    	/**
         * 直接内存和堆内存的分配空间比较
         * 结论: 在数据量提升时, 直接内存相比非直接内的申请, 有很严重的性能问题
         */
        public static void allocateCompare(){
            int time = 10000000;    //操作次数                           
            
            long st = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
                //ByteBuffer.allocate(int capacity) 分配一个新的字节缓冲区。
                ByteBuffer buffer = ByteBuffer.allocate(2); //非直接内存分配申请     
            }
            long et = System.currentTimeMillis();
            System.out.println("在进行"+time+"次分配操作时, 堆内存分配耗时:"+(et-st)+"ms" );
            long st_heap = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
               //ByteBuffer.allocateDirect(int capacity)   分配新的直接字节缓冲区。
               ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
            }
             long et_direct = System.currentTimeMillis();
             System.out.println("在进行"+time+"次分配操作时, 直接内存分配耗时:"+(et_direct-st_heap)+"ms" );
         }
         
        /**
         * 直接内存和堆内存的读写性能比较
         * 结论: 直接内存在直接的IO操作上, 在频繁的读写时会有显著的性能提升。
         */
         public static void operateCompare(){
            int time = 1000000000;
    
            ByteBuffer buffer = ByteBuffer.allocate(2 * time);  
            long st = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
                //  putChar(char value) 用来写入char值的相对put方法
                buffer.putChar('a');
            }
            buffer.flip();
            for (int i = 0; i < time; i++) {
                buffer.getChar();
            }
            long et = System.currentTimeMillis();
            System.out.println("在进行" + time + "次读写操作时, 非直接内存读写耗时:" + (et-st) +"ms");
    
            ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
            long st_direct = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
                //  putChar(char value) 用来写入char值的相对put方法
                buffer_d.putChar('a');
            }
            buffer_d.flip();
            for (int i = 0; i < time; i++) {
                buffer_d.getChar();
            }
            long et_direct = System.currentTimeMillis();
            System.out.println("在进行" +time+"次读写操作时,直接内存读写耗时:"+ (et_direct - st_direct) +"ms");
        }
    }
    
    • 分析:
      从数据流的角度来看
      非直接内存作用链:
      本地IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地IO
      直接内存作用链:
      本地IO -> 直接内存 -> 本地IO
    • 输出:
      在进行10000000次分配操作时,堆内存分配耗时:12ms
      在进行10000000次分配操作时,直接内存分配耗时:8233ms
      在进行1000000000次读写操作时,非直接内存读写耗时:4055ms
      在进行1000000000次读写操作时,直接内存读写耗时:745ms
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值