JVM:内存模型

JVM内存模型

1、程序计数器

描述:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
1)在线程创建时创建

2)当前线程所执行的字节码的行号指示器

3)执行本地方法时,PC的值为undefined

4)每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存

5)此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域

2、虚拟机栈

描述:线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
设置大小:-Xss2M

1)线程私有,生命周期和线程相同

2)栈由一系列帧组成(因此Java栈也叫做帧栈)

3)帧保存方法参数,方法的局部变量、操作数栈、常量池,指针

4)每一次方法调用创建一个帧,并压栈

3、方法区

描述:方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。

简单说方法区用来存储类型的元数据信息

设置大小:-XX:PermSize=10M -XX:MaxPermSize=10M

1)保存装载的类信息

  1. 存储常量池

  2. 存储static变量,方法信息(方法名,返回类型,参数列表,方法的修饰符)

  3. 通常和永久区(Perm)关联在一起可以通过-XX:PermSize和-XX:MaxPermSize参数限制方法区的大小

5)方法区是线程安全的。由于所有的线程共享方法区,所以,方法区里的数据访问必须被设计成线程安全的

  1. 方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集

4、方法区内:运行时常量池

描述:运行时常量池是方法区的一部分.Class文件中除了有类的版本,字段,方法,接口等描述信息外还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区运行时常量池中存放

运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语

言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容

才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发

人员利用得比较多的便是String 类的intern() 方法

5、堆(heap)内存

描述:堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存

设置大小:Xms2g -Xmx2g -Xmn1g-Xss128k

-Xms2G 虚拟机内存最小2G

-Xms2G 虚拟机内存最大2G

-Xmn1G 年轻代内存1G

-Xss128 每个线程堆栈大小

  1. Java 堆是被所有线程共享的

  2. 存放对象实例,几乎所有对象的实例都在这里分配内存

3)堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。

4)堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G

  1. 默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列

  2. 从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代

新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小

.

  • -- 新生代

    • -- Eden Space
    • -- Survivor Space (S0/From)
    • -- Survivor Space (S1/To)
  • -- 老年代

老年代:用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:

1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。

2、大的数组对象,且数组中无引用外部对象。

老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。

6、本地方法栈

描述:本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现本地方法栈。

7、堆外内存:直接内存

  • NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。
  • DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;
  • DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
  • 直接内存的读写操作比普通Buffer快,但它的创建、销毁比普通Buffer慢。
  • 直接内存使用于需要大内存空间且频繁访问的场合,不适用于频繁申请释放内存的场合。

(Note:DirectBuffer并没有真正向OS申请分配内存,其最终还是通过调用Unsafe的allocateMemory()来进行内存分配。
不过JVM对Direct Memory可申请的大小也有限制,可用-XX:MaxDirectMemorySize=1M设置,这部分内存不受JVM垃圾回收管理。)

直接内存(堆外内存)与堆内存比较

1、 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显

2、直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

代码验证:

        package com.xnccs.cn.share;
        
        import java.nio.ByteBuffer;
        
        
        /**
        * 直接内存 与  堆内存的比较
        */
        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");
        }
        }

输出:

    在进行10000000次分配操作时,堆内存 分配耗时:12ms 
    在进行10000000次分配操作时,直接内存 分配耗时:8233ms 
    在进行1000000000次读写操作时,非直接内存读写耗时:4055ms 
    在进行1000000000次读写操作时,直接内存读写耗时:745ms

可以自己设置不同的time 值进行比较

从数据流的角度,来看

  • 非直接内存作用链: 本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
  • 直接内存作用链: 本地IO–>直接内存–>本地IO

直接内存使用场景

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

JVM核心参数设置

初始堆大小: -Xms

  • 默认值是物理内存的1/64(<1GB)
  • 空余堆内存小于40%时,JVM会增大堆到-Xmx的最大限制
  • 此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
  • 整改堆的大小=年轻代大小 + 年老代大小 + 持久代大小
  • 增大年轻代后,将减少年老代大小.

最大堆大小: -Xmx

  • 默认值:物理内存的1/4(<1GB)
  • 空余堆内存大于70%,JVM会减少直到-Xms的最小限制

年轻代大小:-Xmn

  • 整改堆的大小=年轻代大小 + 年老代大小 + 持久代大小增大年轻代后,将减少年老代大小,此值对系统的性能影响较大,
  • 官方推荐配置:堆的3/8

设置年轻代大小: -XX:NewSize

  • 设置年轻代的初始值大小
  • 建议设置为整个堆大小的1/3或者1/4

年轻代最大值: -XX:MaxNewSize

  • 设置年轻代的最大值大小
  • 建议设置为整个堆大小的1/3或者1/4

持久代的初始值: -XX:PermSize

设置持久代最大值: -XX:MaxPermSize

每个线程的栈大小: -Xss

  • 在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

线程栈大小:-XX:ThreadStackSize

代码示例

堆溢出测试:

package com.yhj.jvm.memory.heap;
import java.util.ArrayList;
import java.util.List;
/**
 * @Described:堆溢出测试
 * @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
 * @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
 */
public class HeapOutOfMemory {
    public static void main(String[] args) {
       List<TestCase> cases = new ArrayList<TestCase>();
       while(true){
           cases.add(new TestCase());
       }
    }
}

Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内
存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap

space”。

要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse

Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是

否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢

出(Memory Overflow)。图2-5 显示了使用Eclipse Memory Analyzer 打开的堆转储快

照文件。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就

能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收

它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确

地定位出泄漏代码的位置。

如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查

虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上

检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期

的内存消耗。

以上是处理Java 堆内存问题的简略思路,处理这些问题所需要的知识、工具与经验

在后面的几次分享中我会做一些额外的分析。

java栈溢出

package com.yhj.jvm.memory.stack;
/**
 * @Described:栈层级不足探究
 * @VM args:-Xss128k
 * @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java
 */
public class StackOverFlow {
    private int i ;
    public void plus() {
       i++;
       plus();
    }
    public static void main(String[] args) {
       StackOverFlow stackOverFlow = new StackOverFlow();
       try {
           stackOverFlow.plus();
       } catch (Exception e) {
           System.out.println("Exception:stack length:"+stackOverFlow.i);
           e.printStackTrace();
       } catch (Error e) {
           System.out.println("Error:stack length:"+stackOverFlow.i);
           e.printStackTrace();
       }
    }
}

常量池溢出(常量池都有哪些信息,我们在后续的JVM类文件结构中详细描述)

package com.yhj.jvm.memory.constant;
import java.util.ArrayList;
import java.util.List;
/**
 * @Described:常量池内存溢出探究
 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
 * @FileNmae com.yhj.jvm.memory.constant.ConstantOutOfMemory.java
 */
public class ConstantOutOfMemory {
    public static void main(String[] args) throws Exception {
       try {
           List<String> strings = new ArrayList<String>();
           int i = 0;
           while(true){
              strings.add(String.valueOf(i++).intern());
           }
       } catch (Exception e) {
           e.printStackTrace();
           throw e;
       }
    }
}

方法区溢出

package com.yhj.jvm.memory.methodArea;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
 * @Described:方法区溢出测试
 * 使用技术 CBlib
 * @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
 * @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
 */
public class MethodAreaOutOfMemory {
    public static void main(String[] args) {
       while(true){
           Enhancer enhancer = new Enhancer();
           enhancer.setSuperclass(TestCase.class);
           enhancer.setUseCache(false);
           enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object arg0, Method arg1, Object[] arg2,
                     MethodProxy arg3) throws Throwable {
                  return arg3.invokeSuper(arg0, arg2);
              }
           });
           enhancer.create();
       }
    }
}

直接内存溢出

package com.yhj.jvm.memory.directoryMemory;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
 * @Described:直接内存溢出测试
 * @VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @FileNmae com.yhj.jvm.memory.directoryMemory.DirectoryMemoryOutOfmemory.java
 */
public class DirectoryMemoryOutOfmemory {
    private static final int ONE_MB = 1024*1024;
    private static int count = 1;
    public static void main(String[] args) {
       try {
           Field field = Unsafe.class.getDeclaredField("theUnsafe");
           field.setAccessible(true);
           Unsafe unsafe = (Unsafe) field.get(null);
           while (true) {
              unsafe.allocateMemory(ONE_MB);
              count++;
           }
       } catch (Exception e) {
           System.out.println("Exception:instance created "+count);
           e.printStackTrace();
       } catch (Error e) {
           System.out.println("Error:instance created "+count);
           e.printStackTrace();
       }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容