- 1.**什么是** **JVM ?**
- 2.JVM学习路线
- 3.JVM内存结构
- 3.1程序计数器
- 3.2虚拟机栈
- 3.3栈内存溢出
- 3.4线程运行诊断
- 3.5本地方法栈
- 3.6堆
- 3.7堆内存溢出
- 3.7.1堆内存诊断
- 3.8方法区
- 3.8.1方法区--常量池
- 3.8.2方法区--运行时常量池
定义: Java Virtual Machine :java 程序的运行环境(java 二进制字节码的运行环境) 好处: (1)一次编写,到处运行 (2)自动内存管理,垃圾回收功能 (3)数组下标越界检查 (4)多态 比较: jvm jre jdk关系图2.JVM学习路线
(1)classLoader(类加载器):把java源代码编译成二进制字节码 (2)Method Area(方法区):类都是放在方法区的 (3)Heap(堆):类创建的实例都是放在堆里面的 (4)堆里面的实例化对象调用方法时候,又会用到JVM(虚拟机栈) PC Register(程序计数器) Native Method Stacks(本地方法栈) (5)Interpreter(解释器):方法执行时,每行代码由执行引擎中的解释器逐行解释 (6)JIT Complier:针对于频繁编译的代码进行优化执行 (7)GC(垃圾回收):会对堆里面一些不再被引用的对象进行垃圾回收 (8)本地方法接口:java代码不方便实现的功能,必须调用底层操作系统的功能3.JVM内存结构 3.1程序计数器
定义:Program Counter Register程序计数器(寄存器)
作用:
如上图所示:
java源代码经过类加载器编译成二进制字节码,再经过编译器编译成机器码,最后交给CPU 处理,在此过程中,程序计数器就记住下一条要执行的地址3,3的下一条4,没有程序计数器,就不知道下一条要执行什么,程序计数器使用的是CPU的寄存器
程序计数器的特点:
(1)是线程私有的
解释:学java都知道多线程,所以每个线程都有自己私有的程序计数器 如下图:有两个线程,数字代表他们的地址,线程一执行到地址4的时候,时间片用完了, 自然会去执行线程二,这时程序计数器1记住下一条要执行的地址是5,等线程二的 时间片用完了,线程一就会从计数器的5开始执行,如此反复
(2)不会存在内存溢出
3.2虚拟机栈特点:先进后出
栈—线程运行需要的内存空间
一个栈可以看成由多个栈帧组成的
栈帧:每个方法运行时需要的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题讨论 (1)垃圾回收是否设计栈内存? 不需要,因为栈内存无非就是一次次的方法调用所产生的栈帧内存,栈帧内存在每一次方法调用完,都会被弹出栈,也就是会被自动回收掉,根本不要用垃圾回收 (2)栈内存分配的越大越好吗? 不是,栈内存分配过大,会导致线程数变少,一般采用系统默认的大小就可以了 (3)方法内的局部变量是否线程安全? 看一个变量是不是线程安全的,其实我们就要看多个线程对这个变量是共享的还是私有的
得出结论: 如果方法局部变量没有逃离方法的作用访问,他是线程安全的 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全3.3栈内存溢出
1.栈帧过多导致栈内存溢出
public class TestStack {
private static int count;
public static void main(String[] args) {
try {
method();
}catch (Throwable e){//抛出异常
e.printStackTrace();
System.out.println(count);//看多少次栈会溢出
}
}
private static void method(){
count++;
method(); //自己调用自己,且没有停止条件
}
}
可以看到执行了36341次,栈溢出
2.栈帧过大(不太容易出现)
3.4线程运行诊断上面将了许多关于虚拟机栈的知识,我们知道,线程和栈是息息相关的,所以接下来讲讲 线程 案例一:CPU占用过多(陷入死循环) linux环境下 用top定位哪个进程对cpu的占用过高 ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高) jstack 进程id 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号 案例二:程序运行很长时间没有结果(死锁情况)3.5本地方法栈
java虚拟机调用一些本地方法时,需要为为本地方法提供的内存空间
本地方法:java代码是有一定限制的,所以就需要一些C或者C++写的本地方法来与本地系统或者底层API打交道,java来调用
常见的本地方法:
java.Object.clone
hashcode()
notify()
notifyAll()
wait()
3.6堆前面讲的程序计数器,虚拟机栈,本地方法栈,他们都有一个共同的特点,那就是 线程私有的,而这里讲的堆是线程共享的
Heap堆
通过new关键字,创建对象都会使用堆内存
特点:
它是线程共享的,堆中的对象都需要考虑线程安全问题
有垃圾回收机制
-Xmx8m:设置堆空间为8MB
3.7堆内存溢出堆内存虽然有垃圾回收机制,但是如果我们不断创建新的对象,并且被引用,那就回涉及到堆内存溢出
import java.util.ArrayList;
import java.util.List;
public class Heap {
public static void main(String[] args) {
int i = 0;
try {
List list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
结果:26次就溢出
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
在java控制台输入jconsole,就可以查看当前运行,堆内存的使用情况
代码举例:
```java
public class Die {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
案例:垃圾回收后,内存占用依然很高
使用jvisualvm命令,可视化虚拟机内容
import java.util.ArrayList;
import java.util.List;
public class HeapStudent {
public static void main(String[] args) throws InterruptedException {
List students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());//不断往ArrayList添加对象
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024 * 1024];
}
可以看出java.util.ArrayList占用内存最大
3.8方法区组成对比1.6–1.8
可以看出1.8以前用的是永久代
1.8用的是元空间,而且用的是本地内存
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space * -XX:MaxPermSize=8m * 演示元空间内存溢出 java.lang.OutOfMemoryError: metaspace * -XX:MaxmetaspaceSize=8m
场景:spring,mybatis都会涉及到内存溢出(1.8以后就很难了,因为用的本地内存)
3.8.1方法区–常量池常量池,就是一张表,虚拟机指令很据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
二进制字节码(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
二进制字节码经过反编译,可以编译成人类基本看得懂东西
经过javac 编译然后 反编译命令:javap -v 类
常量池:
3.8.2方法区–运行时常量池运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池就会被放入运行时常量池,并把里面的符号地址变为真实地址



