JAVA虚拟机 JVM
JAVA的大阶段
1.5以前
1.6-1.7-1.8
1.9之后
jvm分为
内存区域(栈,堆,程序计数器,本地方法区,本地方法栈),类加载系统,字节码执行引擎,直接内存
栈
线程私有,生命周期和线程一致。
每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
一个栈帧就是一个方法,就是一个线程
栈空间是动态分配的,java默认规定的栈空间为1M
栈中如果有对象的话,栈中存对象的地址,堆中存对象的实例
栈帧分为局部变量表,操作数栈,动态链接,方法出口
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。
操作数栈:进行操作,赋值等的栈空间
动态链接:将类元信息返回的代码存入
方法出口:记的是程序接下来要执行的行号
永久代
永久代不回收
由于常量池与静态池容易爆满,以及方法区很多方法没用还不回收,造成永久代各种问题
jdk1.6-1.7 存在,其中包括方法区,static池(静态池),常量池,计数器
1.7常量池移到了metaspace中,1.8之后将方法区,静态池,计数器都移到了堆的metaspace(元空间),等于都不在永久代了,所以现在永久代为metaspace
方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
javac编译出的代码都放在方法区中
常量,静态变量,类元信息,及时编译器编译后的代码
方法区线程共享,内存回收效率低
静态池:自己写的
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError
常量池:字符串常量
计数器:引用计数器 1.4之后就取消了
程序计数器 相当于CPU的CS:IP的IP,CS:段基址,IP:段偏移量
堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
堆分为metaspace,年轻代,老年代,GCRoots
堆是线程共享,垃圾回收的主要场所,在虚拟机启动时创建
GCRoots为垃圾不回收的对象:java虚拟机栈引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中的静态属性引用的对象
垃圾对象:没有GCRoots指向的对象为无用对象
full gc(old区满后的垃圾回收)
新生代:发生在新生代的垃圾回收动作,频繁,速度快。
老年代:发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。
先将内存存在年轻代中,gc每扫一次,不回收则+1,16次后进入老年代
年轻代与老年代一般为3:7的比例或者2:8的比例
年轻代分为Eden区与幸存代,S0:S1:Eden比例为1:1:8,8为Eden区
Eden区满一次,minor gc一次
对象
对象的创建
遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。
前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。
对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头
Test t = new Test();
t指向栈,栈帧;new Test();指向堆内存,指向对象头
对象头如哈希编码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程 ID,偏向时间戳,也可包含类型指针