1.JVM-运行时数据区
JVM体系结构预览
Class Loader类加载器
负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,值与他是否可以允许,则由Execution Engine决定
Execution Engine执行引擎 负责解释命令,提交操作系统执行
1. 运行时数据区
程序计数器PCR
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。**(因为JVM执行代码是一行一行的执行,所以需要计数器来记录当前执行的行数)**
栈(包括虚拟机栈、本地方法栈)
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配。
先进后出,后进先出即为栈
栈帧中主要保存3类数据
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack):记录出栈、入栈的操作;
- 栈帧数据(Frame Data):包括类文件、方法等。
** 栈运行原理**
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存去块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,
当一个方法A被调用时就产生一个栈帧F1,并被压入到栈中,
A方法调用了B方法,于是产生栈帧F2也被压入到栈,
B方法调用了C方法,于是产生栈帧F3也被压入到栈。。。
执行完毕后,先弹出F3,再弹出F2,再弹出F1.。。。
遵循“先进后出/后进先出”的原则。
图示在一个栈中有两个栈:
栈2是最先被调用的方法,先入栈,
然后方法2调用了方法1,栈帧1处于栈顶的位置,
栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,
线程结束,栈释放。
每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈。
虚拟机栈VM stack
每个方法在执行的同时都会创建一个栈帧,用来存放局部变量,对象的引用之类的方法信息
线程私有、生命周期与线程相同
每个方法执行会创建一个栈帧
栈帧包括
- 局部变量表:编译期可知的各种基本数据结构、对象引用、返回地址,存放方法参数及方法内的局部变量
- 操作数栈
- 动态链接:指向运行时常量池中该栈帧所属方法的引用
- 方法出口:储存返回地址
- 退出方法的方式
- 正常完成出口
- 异常完成出口,不会返回值,返回地址通过异常处理器表来确定
- 退出过程
- 1)恢复上层方法的局部变量表和操作数栈
- 2)把返回值压入调用者的栈帧的操作数栈中
- 3)调整PC计数器指向下一条指令
- 附加信息
本地方法栈 Native Method Stack
- 线程私有
- 与虚拟机栈一样,只是服务的方法类型不一样
堆Heap
- 线程共享
- 占内存最大的一块
- 存放对象实例
- 垃圾回收的主要区域
1. 堆内存示意图
2. 新生区
新生区是类的诞生、成长、消亡的区域,一个类再这里产生,应用,最后被垃圾回收器收集,结束生命。新生去又分欸两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是再伊甸区被new出来。幸存区又连个:0区和1区。当伊甸园的空间用完是,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园区中的生于对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。如果1区也满了,再移动到养老区。若养老区也满了,那么这时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了FullGC后发现依然无法进行对象保存,就会产生OOM异常(OutOfMemoryError)。
如果出现
java.lang.OutOfMemoryError:Java heap space异常,说明java虚拟机的堆内存不够。原因有二:Java虚拟机的对内存设置不够,可以通过参数-Xms、-Xmx来调整
默认最大内存是机器的四分之一大小
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
==JDK1.8之后,永久代取消了,由元空间取代==
3. 养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。
4. 永久区
永久存储区是一个常驻内存区域,用于存放JDK滋生所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
- 如果出现
java.lang.OutOfMemoryError:PermGen space,说明是Java虚拟机对永久带Perm内存设置不够,一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被沾满。- Jdk1.6之前:有永久代,常量值1.6在方法区
- Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
- Jdk1.8之后:无永久代,常量池1.8在元空间
5. 小总结
逻辑上堆由新生代、养老代、元空间构成、实际上堆只有新生和养老代;方法区就是永久代,永久代是方法区的实现
- 方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的类信息、普通常量、静态常量、编译器编译后的代码等,虽然JVM规范将方法去描述为堆的一个逻辑部分,但他却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
- 对于HotSpot虚拟机,很多开发者习惯将方法区成为“永久代”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于一个接口Interface)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走。
- 常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放
方法区Method Area
- 线程共享
- 储存已加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 常量池 —编译器生成的各种字面量和符号引用
方法区是线程共享的区域,它一般用于储存与类相关的信息,像编译后的代码、静态变量等等,在jdk1.7中它的实现就是放入永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出,即PermGen Space异常。在jdk8中,使用上图的metaspace代替,也就是元空间。元空间使用本地内存,理论上电脑有多少内存,它就有多少内存,避免的内存溢出问题
过程
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
元空间相比永久代的优势
- 字符串常量池在永久代中容易出现性能问题和内存溢出,而元空间使用本地内存,所以不用担心这个问题。
- 永久代会给gc带来不必要的复杂性
- 类和方法信息大小难以确定,给永久代的大小指定带来困难
深拷贝和浅拷贝的区别
深拷贝 -是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存
浅拷贝 -只是增加了一个指针指向已存在的内存地址,
堆栈的区别
- 物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
- 内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
- 存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
- 空间释放
堆由gc释放内存空间.
栈自动释放空间.






