前面章节
JAVA-大白话探索JVM-类加载器(一)
JAVA-大白话探索JVM-类加载过程(二)
JVM运行时内存
通过之前的章节,我们知道.class类如何加载到内存中,如图红框
开始讲讲内存空间
先了解JVM的周期
- JVM在java程序执行时运行,结束时停止。
- 一个java程序对应开启一个JVM进程
- JVM的线程分为两种:守护线程和普通线程
- 守护线程属于JVM自己使用的线程,如GC
- 普通线程是java程序的线程
线程私有数据区
- Java栈(VM Stack)
- 本地方法栈(NM Stack)
- 程序计数器及隐含寄存器(Program Counter Register)
线程共享数据区
- 方法区(Method Area)
- Java堆(Heap)
- 执行引擎
- 本地方法接口
- 本地方法库
你会发现,这都是些什么?????。。。。。。呃
不着急,一步一步来
首先,就是你了,方法区(Method Area,线程共享)
- 类的结构信息和类静态变量都保存在方法区(这样说会不会很抽象,举个例,例如运行时常量池,成员变量和方法数据,构造函数和普通函数的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。开发人员在程序中通过Class对象中的getName、isInstance等方法获取信息时,这些数据都来自方法区。)
- 程序中的所有线程共享一个方法区,简称全局共享
- 对于HotSpot虚拟机,方法区对应为永久代(Permanent Generation),但本质上,两者并不等价,仅仅是因为HotSpot虚拟机的设计团队是用永久代来实现方法区而已,对于其他的虚拟机(JRockit、J9)来说,是不存在永久代这一概念的。
- 使用永久代来实现方法区并不是一个好注意,由于方法区会存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,在某些场景下非常容易出现永久代内存溢出。如Spring、Hibernate等框架在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。在JSP页面较多的情况下,也会出现同样的问题。
- 在JDK1.8下并没有出现我们期望的永久代内存溢出错误,而是Metaspace内存溢出错误。这是因为Java团队从JDK1.7开始就逐渐移除了永久代,到JDK1.8时,永久代已经被Metaspace取代,因此在JDK1.8并没有出现我们期望的永久代内存溢出错误。在JDK1.8中,JVM参数-XX:PermSize和-XX:MaxPermSize已经失效,取而代之的是-XX:MetaspaceSize和XX:MaxMetaspaceSize。注意:Metaspace已经不再使用堆空间,转而使用Native Memory
- 还有一点需要说明的是,在JDK1.6中,方法区虽然被称为永久代,但并不意味着这些对象真的能够永久存在了,JVM的内存回收机制,仍然会对这一块区域进行扫描,即使回收这部分内存的条件相当苛刻。
呃。。。。。。。有点多,慢慢吸收,这方法区也需要好好琢磨琢磨,一不小心溢出就麻烦了。
其次,Java堆(Heap,线程共享)
- Java堆是JVM所管理的最大一块内存,所有线程共享这块内存区域,几乎所有的对象实例都在这里分配内存,因此,它也是垃圾收集器管理的主要区域。
- 从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以Java堆又可以细分成:新生代和老年代,新生代里面有分为:Eden空间、From Survivor空间、To Survivor空间。
- 有一点需要注意:Java堆空间只是在逻辑上是连续的,在物理上并不一定是连续的内存空间。
- 默认情况下,新生代中Eden空间与Survivor空间的比例是8:1,可以使用参数-XX:SurvivorRatio对其进行配置。大多数情况下,新生对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,则触发一次Minor GC,将对象Copy到Survivor区,如果Survivor区没有足够的空间来容纳,则会通过分配担保机制提前转移到老年代去。
- 何为分配担保机制?在发送Minor GC前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果是,那么可以确保Minor GC是安全的,如果不是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于,直接进行Full GC,如果大于,将尝试着进行一次Minor GC,Minor GC失败才会触发Full GC。注:不同版本的JDK,流程略有不同。
- Survivor区作为Eden区和老年代的缓冲区域,常规情况下,在Survivor区的对象经过若干次垃圾回收仍然存活的话,才会被转移到老年代。JVM通过这种方式,将大部分命短的对象放在一起,将少数命长的对象放在一起,分别采取不同的回收策略。
Java栈(Stack,线程私有)、本地方法栈
Java栈
- java栈中只保存基础数据类型(四类八种)和自定义对象引用
- 存取类型:先进后出
- 栈内数据在超出其作用域将自动释放
- 每个栈是线程私有,它们的生命周期与线程相同。
- 每个线程建立一个操作栈,每个栈又包含若干个栈帧,每个栈帧对应每个方法调用
- 栈帧:
- 局部变量区(方法内基本类型变量、变量对象指针)
- 操作数栈区(存放方法执行过程中产生的结果)
- 运行环境区(动态链接、方法返回相关信息、异常捕捉)
本地方法栈
- 与JAVA栈类似
- 本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用
- 本地方法非java语言编写,不受JVM管理
- HotSpot VM将本地方法栈和JVM栈合并了。
程序计数器(线程私有)
概念:在JVM概念模型里,字节码解释器工作时就说通过改变这个计算器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- Java虚拟机可以支持多条线程同时执行,多线程是通过线程轮流切换来获得CPU执行时间的,每条线程都会有独立的程序计数器
- 如果执行java方法,程序计数器记录JVM字节码指令的地址,如果执行 native,计数器为空(Underfined)
- 程序计数器这个内存区域在JVM规范中是唯一没有规定任何OutOfMemoryError的区域
运行时常量池(Runtime Constant Pool)
- 方法区的一部分,用于存放编译期间生成的各种字面量(int,short等等)和符号引用(对象符号引用Integer,String)
- 除了编译产生能存入,运行期间也能将新的常量放入池中(String.intern())
- 节省内存空间:常量池中如果有对应的字符串,那么则返回该对象的引用,从而不必再次创建一个新对象。
- 节省运行时间:比较字符串时,比equals()快。对于两个引用变量,判断引用是否相等,也就可以判断实际值是否相等
- Byte、Short、Integer、Long、Character这5种包装类都默认创建了数值[-128 , 127]的缓存数据。当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。
- Oracle对Java 7中的常量池做了一个非常重要的改变 — 常量池被重新定位到堆中。这意味着你不再受限于单独的固定大小内存区域。所有字符串现在都位于堆中,与大多数其他普通对象一样,这使你可以在调整应用程序时仅管理堆大小。