引言
jvm 是java可以实现跨平台部署的关键,即一次编译处处执行。jvm 是在操作系统和java程序之间的一层内存管理机制,使java程序员能够不关心操作内存,使编程更加高效。
jvm,jre,jdk 的关系
jvm:jvm 是编译环境。
jre:在jvm 中添加了基础类库。
jDK:它是面向 Java 开发者的完整工具包。JDK = JRE + 开发工具(如编译器 javac、调试器 jdb、打包工具 jar 等)。
jvm 指令执行流程
首先编译成.class 文件,然后拿到指令交给解释器,然后解释器转化为机器码,交给cpu执行。
对于热点代码,JIT 编译器会将其整体编译成机器码,并缓存起来。下次再执行这段代码时,JVM 就不再经过解释器了,而是直接调用已经编译好的机器码,这大大提高了运行效率。
java8 和1.8 是啥关系
- Java 8:这是产品名称(Product Name)。
从 Java 9 开始,Oracle 改变了命名规则,不再使用 "1.x" 的前缀,直接叫 Java 9、Java 10、Java 11 等。但为了照顾历史习惯,Java 8 这个叫法被保留并广泛使用。 - JDK 1.8:这是内部版本号(Internal Version Number)。
在 Java 9 之前,Java 的内部版本号一直是1.x的格式(比如 Java 7 是 1.7,Java 6 是 1.6)。虽然对外宣传叫 Java 8,但它的内部版本号依然延续了传统,叫1.8.0_xxx。
jvm基本结构
JVM的位置
三种JVM:
JVM 只是一套规范,不同的公司有不同的实现,以下是几种常见的JVM。
- Sun公司:HotSpot 用的最多
- BEA:JRockit
- IBM:J9VM
我们学习都是:HotSpot
JVM的体系结构
- jvm调优:99%都是在方法区和堆,大部分时间调堆。 JNI(java native interface)本地方法接口。
程序计数器
概述
先编译成.class 文件,然后拿到指令交给解释器,然后解释器转化为机器码,交给cpu执行。这就是指令执行的过程。
程序计算器就是记录下一条执行指令的地址。硬件上是通过寄存器实现的,就是cpu的取址执行流程上的取下一条指令。
特点
- 线程私有的,每一个线程有一个程序计数器。
栈
栈与队列
- 在计算机流传有一句废话: 程序 = 算法 + 数据结构
- 但是对于大部分同学都是: 程序 = 框架 + 业务逻辑
- 栈:后进先出 / 先进后出
- 队列:先进先出(FIFO : First Input First Output)
栈管理程序运行
- 存储一些基本类型的值、对象的引用、方法等。
- 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
思考:为什么main方法最后执行!为什么一个test() 方法执行完了,才会继续走main方法!
喝多了吐就是栈,吃多了拉就是队列。
栈的特点
栈里面会放什么东西那?
- 8大基本类型 + 对象的引用 + 实例的方法。栈里面的东西也叫栈帧。换个说法就是每个方法执行时,所占用的内存。
栈运行原理
- 栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是线程私有的,**一个线程拥有一个独立的虚拟机栈,而这个栈中包含了多个栈帧。栈帧是线程私有的,不存在跨线程共享栈帧的情况。**栈帧中包含了封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
- 第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 栈是先进后出的,先被加入的方法后执行。
当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生了栈帧F2也被压入栈中,B方法又调用了C方法,于是产生栈帧F3也被压入栈中 执行完毕后,先弹出F3, 然后弹出F2,在弹出F1........
- 遵循 “先进后出” / "后进先出" 的原则。
- 栈满了,抛出异常:stackOverflowError
- 对象实例化的过程。
栈涉及到的问题
栈内存的设置
可以通过设置 -Xxs 来设置栈内存。
栈内存不要设置过大的栈内存,栈内存越大,线程占用的内存越大,但是机器的内存大小是一定的,所以栈内存越大线程数越少。栈内存越大只是能提高方法中可以调用更多的方法。
方法内的局部变量是否是线程安全的?
- 如果没有逃离方法的作用访问,是线程安全的。
- 如果是局部变量引用了对象,并逃离了方法的作用,需要考虑线程安全。
这里解释了为啥引用了对象会有线程安全问题,而基本类型不会?基本类型只存在栈帧中,方法结束,变量就销毁了,不会被其他变量访问到。虽然 引用本身是线程安全的(存在当前线程的栈帧里),但它指向的对象实例存放在堆(Heap)中。堆是线程共享的。
对象逃逸:
- 作为返回值:
return obj;(调用方可能把它赋值给一个成员变量,或者传给其他线程)。 - 作为参数传递:
someOtherMethod(obj);(如果其他方法把它存到了全局的静态集合中)。 - 赋值给共享的成员变量:
this.globalObj = obj;(成员变量在堆中,所有线程可见)。
栈内存溢出
栈内存溢出报错信息:
原因如下:
- **栈帧过多:**方法递归调用没有正确的出口,导致栈溢出
- 栈帧过大
线程诊断
常用的线程诊断工具:jstack 是 JDK 自带的命令行工具,能直接生成线程快照并自动检测死锁。
cpu占用高
线程死锁
本地方法栈
Java调用C++ 程序所调用的方法,放在本地方法栈中。java 中用native 修饰的方法都是本地方法。
Native
- 编写一个多线程类启动。
public static void main(String[] args) {
new Thread(()->{ },"your thread name").start();
}
- 点进去看start方法的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 调用了一个start0方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// 这个Thread是一个类,这个方法定义在这里是不是很诡异!看这个关键字native;
private native void start0();
- 凡是带了native关键字的,说明 java的作用范围达不到,去调用底层C语言的库!
- JNI:Java Native Interface(Java本地方法接口)
- 凡是带了native关键字的方法就会进入本地方法栈;
- Native Method Stack 本地方法栈
- 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 在 Native Method Stack 中登记native方法,在 ( ExecutionEngine ) 执行引擎执行的时候加载Native Libraies。
- 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
堆
堆的结构
Java7之前
-
Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
-
类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
-
堆内存分为三部分:
-
- 新生区 Young Generation Space Young/New
- 养老区 Tenure generation space Old/Tenure
- 永久区 Permanent Space Perm
-
堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)。
-
是线程共享的。
Java8以及之后
java8 把永久区变成元空间,不在占用堆的空间,而是占用操作系统内存。
谁空谁是to
-
GC垃圾回收主要是在新生区和养老区,又分为轻GC 和 重GC,如果内存不够,或者存在死循环,就会导致
-
在JDK8以后,永久存储区改了个名字(元空间)。
主要存放内容
堆是 JVM 中最大的一块内存区域,几乎所有的对象实例和数组都在这里分配内存。
- 通过new 关键字创建的实例对象
- 对象内的成员变量:对象内部的非静态字段(引用类型或基本类型)都存放在该对象实例的内存块中。
- 数组
堆_内存诊断工具
jps常见的命令
jps -lv可以同时查看完整的类名/路径以及 JVM 启动参数,信息最全面且实用
jmap常见命令
jmap主要用于生成 Java 虚拟机进程的内存映射信息、查看堆内存使用情况或导出堆转储快照**-heap**:显示 Java 堆的详细信息,包括使用的垃圾回收器、堆配置(最大/最小堆大小)、以及各代(Eden、Survivor、Old Gen 等)的内存使用情况。**-histo**:显示堆中对象的直方图,按占用内存大小排序,列出类名、实例数量和占用字节数,常用于快速定位内存占用大户。**-histo:live**:仅显示存活对象的直方图。注意:此操作会触发一次 Full GC。
jconsole
可视化页面
在jdk\bin中有对应的入口 jconsole.exe
方法区
方法区结构
jdk1.6
jdk1.8
主要存放内容
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
方法区用于存储已被虚拟机加载的类信息、常量、静态变量等。它相当于类的“蓝图”或“模板”。而堆存放的是对象实例。
主要存放以下内容:
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中(也就是new 出来的对象),和方法区无关。
- static ,final ,Class ,常量池~
- 方法区在1.8之前是在堆中的,在1.8之后再元空间中,不占用堆内存,而是占用操作系统内存。
- 静态变量 (Static Variables):类级别的变量。
- JIT 编译后的代码:即时编译器优化后的本地机器码。
常量池
它类似于一张表,JVM 将各种常量信息以表的形式记录在其中。字节码指令在执行时,会根据这张表里的索引来查找对应的类名、方法名、参数类型等信息。存在于.class 文件中。
**核心作用:**作为数据的载体,将大量在代码中被引用的结构信息(如类、方法、字符串等)集中存储,避免将这些庞大的数据直接嵌入到字节码指令中,从而减小了 .class 文件的体积。
运行时常量池
JVM 加载 .class 文件时,会将 Class 文件常量池中的内容加载到内存中,这就形成了运行时常量池。它里面只存储指向字符串对象的引用,而不是字符串对象本身。
存储位置
它是 JVM **方法区(Method Area)**的一部分。在JDK 8以前是堆中的永久代。在 JDK 8 及以后的版本中,方法区由“元空间(Metaspace)”实现,使用的是操作系统的本地内存。
StringTable
位置
StringTable 是存放字符串常量的地方。位置见 jdk1.6 方法区结构 和 jdk1.8 方法区结构。
概述
它是一个全局唯一的哈希表,专门用来存储实际的字符串对象引用。字符串真正的内容是存在堆中的。使用StringTable 是为了节省空间,无论程序中有多少个"hello"这样的字符串,只有一个引用。
但是如果通过动态拼接生成了新的字符串,JVM 会实时检查 StringTable,如果没有,就会把新对象的引用动态添加进去。
请看一下代码:
String a="a";
String b="b";
String ab=a+b;
String stringab="ab";
System.out.println(ab==stringab); // false
原因:这个新生成的 "ab" 对象存在于堆内存(Heap)中,而不是字符串常量池中。因此,变量 ab 指向的是堆中的一个新对象。
想要把 ab 对象放入StringTable 中有两个方法:
方法一:使用 **final** 关键字触发编译期优化
final String a = "a";
final String b = "b";
String ab = a + b; // 编译期优化,等价于 String ab = "ab";
String stringab = "ab";
System.out.println(ab == stringab); // 输出 true
方法二:使用 **intern()** 方法手动入池
String a = "a";
String b = "b";
String ab = (a + b).intern(); // 将堆中的 "ab" 放入常量池,并返回池中的引用
String stringab = "ab";
System.out.println(ab == stringab); // 输出 true
关于new String("ab")
请看以下代码:
String s1 = new String("ab");
String s2 = s1.intern();
String s3 = "ab";
System.out.println(s1 == s2);
System.out.println(s2 == s3);
这道题的正确答案是:
第一个输出 **false**,第二个输出 **true**。
这行代码(String s1 = new String("ab");)发生了啥?
- 遇到字面量
"ab",JVM 会先检查字符串常量池(StringTable)。如果池中没有"ab",就会在池中创建一个"ab"对象。 - 遇到
new关键字,JVM 会强制在**堆内存(Heap)**中创建一个全新的 String 对象,其内容也是"ab"。 - 变量
s1指向的是堆内存中的那个新对象。
总结:new String("ab"),其实是两步一个是在堆中创建对象,另一步是去StingTable创建“ab”对象,为其创建缓存。
这行代码(String s2 = s1.intern();)的作用
- JVM 会拿着
s1的内容"ab"去字符串常量池中查找。 - 因为在第一步中,常量池里已经存在
"ab"这个对象了。 - 所以,
intern()方法会直接返回常量池中那个已有对象的引用。 - 变量
s2指向的是字符串常量池中的"ab"对象。
这行代码的作用(String s3 = "ab";)
- JVM 去字符串常量池中查找
"ab"。 - 发现池中已经有了(String s1 = new String("ab")这行代码创建的),于是直接把池中对象的引用赋给
s3。如果没有就在常量池中创建。 - 变量
s3指向的也是字符串常量池中的"ab"对象。
总结:
这就体现了StringTale 最大的价值,在于处理现实开发中的海量数据。想象一下这个场景:
你从数据库或文件中读取了 100 万条用户记录,每条记录里都有一个表示性别的字段 "男" 或 "女"。
- 如果没有 StringTable:JVM 会在堆内存中创建 100 万个
"男"对象和 100 万个"女"对象。这会瞬间撑爆内存,导致频繁的 GC 甚至 OOM(内存溢出)。 - 有了 StringTable:无论读取多少万条数据,JVM 在 StringTable 中永远只保留一个
**"男"**对象和一个**"女"**对象。那 100 万个变量,仅仅存储了指向这两个对象的引用(在 64 位 JVM 中,一个引用通常只占 4 到 8 个字节)。