JVM是什么?
JVM(Java Virtual Machine):Java程序的运行环境(java二进制字节码的运行环境)
好处:
1.一次编写,到处运行
![](https://i-blog.csdnimg.cn/direct/fdefc8020ede4ccb99bbb31b016d2a72.png)
Java代码是如何做到一次编写,到处运行?
计算机的最底层是计算机硬件(如cpu,内存条),再往上是操作系统(如windows,linux),JVM就是运行在操作系统之上的,正是因为JVM,Java才成为一个跨平台的语言,因为JVM屏蔽了操作系统的差异,真正运行代码的并不是这些操作系统,而是JVM,所以才能做到一次编写,到处运行。
2.自动内存管理,垃圾回收机制
对于从事C、C++程序开发的开发人员来说,需要自行管理内存,所以很容易由于编码不当导致内存泄露等问题
而JAVA虚拟机的垃圾回收机制,则大大减轻了程序员的负担,减少了程序员出错的几率
JVM的运行流程是什么?
![](https://i-blog.csdnimg.cn/direct/1ed182a534ba4128851514f872235bb2.png)
1.Java Source 就是Java的源文件,将源文件编译为Java Class字节码文件
2.类加载到子系统 ,在加载的过程中会将Java代码转换成字节码
3.运行数据区会将字节码加载到内存,只有加载到内存程序才能运行
4.执行引擎将字节码翻译成底层系统指令 ,在这个过程中解释器会解释字节码的信息 ,即时编辑器会针对代码进行优化 ,GC垃圾回收主要针对运行数据区堆空间
5.由于Java代码有些时候并不能实现某些功能,所以需要借助系统提供的某些接口,也就是本地方法接口(由C或C++实现)。
JVM的组成
什么是程序计数器?
程序计数器(PC Register): 线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址
线程私有就不存在线程的安全问题
在字节码文件中,就详细说明了代码的执行过程
我们可以通过下面的命令,查看字节码的反汇编信息,其中就详细记录了代码的执行过程,
javap-v xx.class 打印堆栈大小,局部变量的数量和方法的参数。
查看字节码文件中代码执行
![](https://i-blog.csdnimg.cn/direct/fe034ec3e64b4435962f2addd7c9ef3b.png)
(1)先编译一下该类
![](https://i-blog.csdnimg.cn/direct/438db51e2c4e4ed38058c09971f40323.png)
![](https://i-blog.csdnimg.cn/direct/6689a5c4b15547708bf8c110c17645d7.png)
(2)执行命令
![](https://i-blog.csdnimg.cn/direct/38345f7c14844d84a1030f3314c29e94.png)
(3)查看反汇编生成的字节码文件
![](https://i-blog.csdnimg.cn/direct/4868b6e57bf44221994765b7933bfc80.png)
java程序的二进制字节码文件
![](https://i-blog.csdnimg.cn/direct/0108f47e096b4ffcb8b0ce4543570a08.png)
程序计数器就是在线程的切换时,记录代码的执行行号
案例
![](https://i-blog.csdnimg.cn/direct/bc44b8b377664ded9a797ecf1feb2cf3.png)
线程一先执行,执行行号为10 ,时间片的时间到了,切换到线程二执行,此时程序计数器需要保存线程一执行的终止行号,以便下一次的执行。线程二执行到行号9,线程二时间片到了,线程二的程序计数器会保存线程二的终止行号,然后线程一再次开始,从程序计数器记录的行号10,继续往下执行,执行到了行号20,再次进行切换。。。
2.什么是Java堆
Java堆 是一个线程共享的区域:主要用来保存对象实例,数组 等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError的异常
![](https://i-blog.csdnimg.cn/direct/30817293d7a04709bbad7e931fee1090.png)
堆的年轻代和老年代
年轻代 被划分为三部分,Eden区 和两个大小严格相同的Survivor区
根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
老年代主要保存生命周期长的对象,一般是一些老的对象
什么是元空间?
元空间: 保存类信息,静态变量,常量,编译后的代码。
java1.7 和 java1.8的堆的区别
![](https://i-blog.csdnimg.cn/direct/d0d2b8d9248d439ab15138852c7104a4.png)
java8将堆中的方法区/永久代放到了本地内存,也就是元空间中
1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
什么是虚拟机栈?
Java Virtual machine Stacks(java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
案例:
![](https://i-blog.csdnimg.cn/direct/f7b140c0d30d4e7382ba923cc5fe698e.png)
栈帧1中的方法,调用栈帧2中的方法,栈帧2方法调用栈帧3的方法
则活动栈帧的顺序:栈帧1-->栈帧2-->栈帧3-->栈帧2-->栈帧1
垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放,并不需要垃圾回收。
栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k。栈帧过大会导致线程数变少,例如,机器总内存为512m,目
前能活动的线程数则为512个,如把栈内存改为2048k,那么能活动的栈帧就会减半。
方法内的局部变量是否是线程安全的?
案例:
![](https://i-blog.csdnimg.cn/direct/031ae6f40e5e486a9f257b079418d0be.png)
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出的情况
栈帧过多导致栈内存溢出,典型问题:递归调用
栈帧过大导致栈内存溢出
堆栈的区别是什么?
1.栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会GC垃圾回收,而栈不会。
2.栈内存是线程私有的,而堆内存是线程共有的。
3.两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常,
栈空间不足:java.ang.StackOverFlowError。
堆空间不足:java.ang.OutOfMemoryError。
什么是方法区?
**方法区(Method Area)**是各个线程共享的内存区域
主要存储类的信息、运行时常量池
虚拟机启动的时候创建,关闭虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
![](https://i-blog.csdnimg.cn/direct/aea334323ade42f18d23890c6d0ec2c1.png)
方法区的内存溢出
![](https://i-blog.csdnimg.cn/direct/b46854035ad5455abf83898d1c58f570.png)
将元空间的内存大小改为 8M
![](https://i-blog.csdnimg.cn/direct/147a8cf1df334052ab3c66348f4fa338.png)
![](https://i-blog.csdnimg.cn/direct/b3e77dd3477e4aecbd14dedce02bf9b7.png)
执行结果:方法区的内存太小
![](https://i-blog.csdnimg.cn/direct/c697e757450541c18841685396dde33e.png)
什么是常量池?
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
javap -v Application.class
查看字节码结构(类的基本信息、常量池、方法定义)
类信息
![](https://i-blog.csdnimg.cn/direct/33bc28558db54d8da7b3f80fc1e80588.png)
常量池
![](https://i-blog.csdnimg.cn/direct/ee8d987f1c80491eb588295f700e80f7.png)
![](https://i-blog.csdnimg.cn/direct/0431f0b146f44a049077850b1f337c04.png)
根据字节码文件查询常量池表
![](https://i-blog.csdnimg.cn/direct/32966ea2a13348d69f0573a2c5f99cb7.png)
![](https://i-blog.csdnimg.cn/direct/0375254574684e3685afd5c4cc38fa3c.png)
什么是运行时常量池?
常量池是 *class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实内存地址,从而执行指令。
![](https://i-blog.csdnimg.cn/direct/418f11155c7343d0b027e9ecf59d2cba.png)
什么是直接内存?
**直接内存:**并不属于IM中的内存结构,不由M进行管理。是虚拟机的系统内存,常见于 NI0 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高
例子
![](https://i-blog.csdnimg.cn/direct/2a74b8b8162e4a168b6031ddfff1f92c.png)
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class DirectMemoryDemo {
static final String FROM = "E:\\bak1\\01-java成神之路.mp4";
static final String TO = "E:\\bak2\\abc.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel()) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer耗时: " + (end - start) / 1e9 + " 秒");
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO)) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io耗时: " + (end - start) / 1e9 + " 秒");
}
}
![](https://i-blog.csdnimg.cn/direct/336b61aa411b405cbeaa72e5c820f40d.png)
IO VS NIO
常规IO的数据拷贝流程
![](https://i-blog.csdnimg.cn/direct/1b6199e60cfd4365b59dbe7c8dfa8305.png)
- Java程序(用户态)调用read方法发起从磁盘文件读取数据的请求;
- 操作系统从内核态将磁盘文件的数据读取到系统缓存区;
- 内核态把系统缓存区的数据拷贝到Java堆内存中的缓冲区;
- Java程序在用户态对Java堆内存中的数据进行处理;
- Java程序调用 `write` 方法发起将数据写入目标位置的请求;
- 内核态将Java堆内存缓冲区中的数据拷贝回系统缓存区;
- 操作系统从内核态将系统缓存区的数据写入目标位置(如另一个磁盘文件)。
NIO数据拷贝流程 ![](https://i-blog.csdnimg.cn/direct/169ac81f2c4b410693ecde79baf9c2f9.png)
- Java程序(用户态)调用 `read` 方法发起从磁盘文件读取数据的请求;
- 操作系统从内核态将磁盘文件的数据直接读取到直接内存(也叫堆外内存);
- Java程序在用户态可以直接访问和处理直接内存中的数据;
- Java程序调用 `write` 方法发起将数据写入目标位置的请求;
- 操作系统从内核态将直接内存中的数据写入目标位置(如另一个磁盘文件)。
二者区别:
常规IO数据在内核态的系统缓存区和用户态的Java堆内存之间多次拷贝,至少有4次数据拷贝操作**(读2次,写2次)**;
NIO使用直接内存,减少了数据在内核态和用户态之间的拷贝次数,通常只需2次数据拷贝**(读1次,写1次)**,提升了性能。
![](https://i-blog.csdnimg.cn/direct/eca10e68fc9c433ab97605d1136be453.gif)