JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。
程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;
程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。
一、JVM组织架构
JVM 大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。
类加载器,负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
运行时数据区,JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照 Java 虚拟机规范可以划分为方法区、堆、虚拟机栈、程序计数器和本地方法栈。
执行引擎,也是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器 JIT 和垃圾回收器。
二、内存区域划分
分为线程共享区域和线程独占区域
线程共享区域:方法区,java堆
线程独占区域:虚拟机栈,本地方法栈,程序计数器
不同JDK版本具体划分
JDK1.6
JDK 1.6 使用永久代来实现方法区
JDK1.7
JDK 1.7 时仍然是永久带,但发生了一些细微变化,比如将字符串常量池、静态变量存放到了堆上。
JDK1.8
在 JDK 1.8 时,直接在内存中划出了一块区域,叫元空间,来取代之前放在 JVM 内存中的永久代,并将运行时常量池、类常量池都移动到了元空间。
客观上,永久代会导致 Java 应用程序更容易出现内存溢出的问题,因为它要受到 JVM 内存大小的限制。
HotSpot 虚拟机的永久代大小可以通过 -XX:MaxPermSize
参数来设置,32 位机器默认的大小为 64M,64 位的机器则为 85M。
而 J9 和 JRockit 虚拟机就不存在这种限制,只要没有触碰到进程可用的内存上限,例如 32 位系统中的 4GB 限制,就不会出问题。
主观上,当 Oracle 收购 BEA 获得了 JRockit 的所有权后,就准备把 JRockit 中的优秀功能移植到 HotSpot 中。好处就是,元空间的大小不再受到 JVM 内存的限制
1、堆区:
存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 。
a、内存划分
Java 中“几乎”所有的对象都会在堆中分配,堆也是垃圾收集器管理的目标区域。由于垃圾收集器大部分都是基于分代收集理论设计的,所以堆又被细分为新生代
、老年代
、Eden空间
、From Survivor空间
、To Survivor空间
等。
新生代又被划分为 Eden 空间和两个 Survivor 空间(From 和 To)。默认 Eden 和 Survivor From和 Survivor To的大小比例是 8∶1:1。
新创建的对象会被分配到 Eden 空间。如果 Eden 区域没有足够的空间,那么就会触发 YGC(Minor GC),YGC 处理的区域只有新生代。因为大部分对象在短时间内都是可收回掉的,因此 YGC 后只有极少数的对象能存活下来,而被移动到 S0 区(采用的是复制算法)。当触发下一次YGC时,会将 Eden 区和 S0 区的存活对象移动到 S1 区,同时清空 Eden区和 S0 区。当再次触发 YGC 时,这时候处理的区域就变成了 Eden 区和 S1 区(即 S0和 S1 进行角色交换)。每经过一次 YGC,存活对象的年龄就会加1。
对象在新生代中经历多次 GC 后,如果仍然存活,会被移动到老年代。当老年代内存不足时,会触发 Major GC,对整个堆进行垃圾回收
随着 JIT 编译器的发展和逃逸技术的逐渐成熟,“所有的对象都会分配到堆上”就不再那么绝对了。从 JDK 7 开始,JVM 默认开启了逃逸分析,意味着如果某些方法中的对象引用没有被返回或者没有在方法体外使用,也就是未逃逸出去,那么对象可以直接在栈上分配内存。
public void testStackAllocation() {
Person p = new Person(); // 对象可能分配在栈上
p.name = "yyt";
p.age = 18;
System.out.println(p.name);
}
逃逸分析好处:
第一,如果确定一个对象不会逃逸,那么就可以考虑栈上分配,对象占用的内存随着栈帧出栈后销毁,这样一来,垃圾收集的压力就降低很多。
第二,线程同步需要加锁,加锁就要占用系统资源,如果逃逸分析能够确定一个对象不会逃逸出线程,那么这个对象就不用加锁,从而减少线程同步的开销。
第三,如果对象的字段在方法中独立使用,JVM 可以将对象分解为标量变量,避免对象分配。
b、对象什么时候会进入老年代?
长期存活的对象
JVM 会为对象维护一个“年龄”计数器,记录对象在新生代中经历 Minor GC 的次数。每次 GC 未被回收的对象,其年龄会加 1。当超过一个特定阈值,默认值是 15,就会被认为老对象了,需要重点关照。这个年龄阈值可以通过 JVM 参数-XX:MaxTenuringThreshold来设置。
大对象
大对象是指占用内存较大的对象,如大数组、长字符串等。其大小由 JVM 参数 -XX:PretenureSizeThreshold
控制,但在 JDK 8 中,默认值为 0,也就是说默认情况下,对象仅根据 GC 存活的次数来判断是否进入老年代。
G1 垃圾收集器中,大对象会直接分配到 HUMONGOUS 区域。当对象大小超过一个 Region 容量的 50% 时,会被认为是大对象。Region 的大小可以通过 JVM 参数
-XX:G1HeapRegionSize
来设置,默认情况下从 1MB 到 32MB 不等,会根据堆内存大小动态调整。
动态年龄判定
如果 Survivor 区中所有对象的总大小超过了一定比例,通常是 Survivor 区的一半,那么年龄较小的对象也可能会被提前晋升到老年代。这是因为如果年龄较小的对象在 Survivor 区中占用了较大的空间,会导致 Survivor 区中的对象复制次数增多,影响垃圾回收的效率。
2、栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的值和对象以及基础数据的引用
2.每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
3、方法区:
1.线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量,静态代码块、即时编译器(JIT Compiler)编译后的代码数据等,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
2.常量池是方法区的一部分
在Java7以前,HotSpot虚拟机中,方法区也被称为“永久代”
在Java8中,HotSpot虚拟机改变了原有方法区的物理实现,将原本由JVM管理内存的方法区的内存移到了虚拟机以外的计算机本地内存,并将其称为元空间(Metaspace)。这样一来,现在的方法区实际存储在于元空间,再也不用和Java堆共享内存了,“永久代”也就永久地被撤销了。
4、虚拟机栈:
每个java方法在执行时,会创建一个“栈帧(stack frame)”,栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”几个部分。我们常说的“堆内存、栈内存”中的“栈内存”指的便是虚拟机栈,确切地说,指的是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。
方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁
5、本地方法栈:
本地方法栈的功能和特点类似于虚拟机栈,不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。native 方法就是java调用非java代码方法
Java 通过 JNI 提供了一种机制,允许 Java 代码调用本地代码(通常是 C 或 C++ 编写的代码)。
当调用 Java 方法时,虚拟机会创建一个栈帧并压入虚拟机栈,而当它调用本地方法时,虚拟机会通过动态链接直接调用指定的本地方法。
6、程序计数器:
.java文件编译成.class字节码文件运行,来记录运行到哪儿(线程执行字节码文件的行号)
评论区