JVM系列之一:内存区域和内存溢出
1. 内存区域
如下图为JVM的运行时数据区,我们常说的堆指的Java堆、栈指的是虚拟机栈;
其中方法区和堆属于线程共享区;虚拟机栈、本地方法栈、PC属于线程独占区;
1.1 程序计数器
程序计数器是当前线程所执行字节码的行号指示器,记录字节码指令地址,来标识程序执行的顺序;
在多线程中,为了保证线程切换后能恢复到正确的执行位置,每条线程都要有一个独立的程序计数器,因此它位于线程独占区
程序计数器所在的区域是唯一一个没有OutOfMemortError情况的区域,因为程序员不需要擦去操作程序计数器;
1.2 虚拟机栈
1. 虚拟机栈描述的是Java方法的内存模型,它也是线程私有的,和线程的生命周期一样,每个方法在执行的时候,都会创建一个栈帧记录一些列信息
2. 虚拟机栈不可以扩展:如果线程请求的栈深度大于JVM所允许的深度,将会抛出StackOverflowError异常
3. 虚拟机栈可以扩展(大部门JVM都可以扩展):如果扩展时无法申请到足够的内存,抛出OutOfMemortError异常,栈大小可以通过-Xss指定
1.3 Java堆
1. 堆是JVM管理内存中最大的一块区域,被所有线程共享,这是多线程同步机制的原因
2. 堆唯一的目的就是存放实例对象,所有的对象实例都在堆里分配
3. 堆是垃圾收集器管理的主要区域,因此又被称为"GC堆"
4. 堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,像磁盘空间一样
5. 堆可以固定,也可以扩展,通过-Xmx、-Xms控制堆最大、最小内存大小
1.3.1 新生代/老年代
1. 堆可以分为新生代和老年代,新生代分为Eden区、From Survivor空间、To Survivor空间
2. 配置新生代和老年代在堆结构的占比
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
3. 现在的商业虚拟机(例如HotSpot)都采用[复制算法]回收新生代,在HotSpot中,三个空间所占的比例是8:1:1,可以通过选项 -XX:SurvivorRatio=8
4. 几乎所有的Java对象都是在Eden区被new出来的;98%的对象都销毁在了新生区
5. 可以使用选项-Xmn设置新生代最大内存大小
1.3.2 对象创建过程
JVM创建对象(限于普通Java对象,不包括数组和Class对象)要面临4个问题:
1. 怎么分配内存
- JVM遇到一个new指令时,首先会检查常量池中是否有引用,如果没有,则为新对象分配内存
- new创建的对象放在伊甸园区
- 内存分配方式有“指针碰撞”和“空闲列表”两种,具体和JVM采用的收集器有关
2. 怎么保证线程安全(修改指针指向的时候)
- CAS+失败重试保证操作原则性
- 本地线程分配缓冲方式(TLAB)可以通过-XX:+UseTLAB、-XX:-UseTLAB参数设置开启、关闭状态
3. 怎么初始化对象
- JVM要知道对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等
- 上述这些信息都存在对象头中
4. 执行初始化方法
1.3.3 对象分配过程
1.new的对象先放伊甸园区。此区有大小限制。
2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
3.然后将伊甸园中的剩余的幸存对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6.啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=进行设置。
7.在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常