JVM是什么?
JVM(Java Virtual Machine):Java程序的运行环境(java二进制字节码的运行环境)
好处:
1.一次编写,到处运行
Java代码是如何做到一次编写,到处运行?
计算机的最底层是计算机硬件(如cpu,内存条),再往上是操作系统(如windows,linux),JVM就是运行在操作系统之上的,正是因为JVM,Java才成为一个跨平台的语言,因为JVM屏蔽了操作系统的差异,真正运行代码的并不是这些操作系统,而是JVM,所以才能做到一次编写,到处运行。
2.自动内存管理,垃圾回收机制
对于从事C、C++程序开发的开发人员来说,需要自行管理内存,所以很容易由于编码不当导致内存泄露等问题
而JAVA虚拟机的垃圾回收机制,则大大减轻了程序员的负担,减少了程序员出错的几率
JVM的运行流程是什么?
1.Java Source就是Java的源文件,将源文件编译为Java Class字节码文件
2.类加载到子系统,在加载的过程中会将Java代码转换成字节码
3.运行数据区会将字节码加载到内存,只有加载到内存程序才能运行
4.执行引擎将字节码翻译成底层系统指令,在这个过程中解释器会解释字节码的信息,即时编辑器会针对代码进行优化,GC垃圾回收主要针对运行数据区堆空间
5.由于Java代码有些时候并不能实现某些功能,所以需要借助系统提供的某些接口,也就是本地方法接口(由C或C++实现)。
JVM的组成
什么是程序计数器?
程序计数器(PC Register):线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址
线程私有就不存在线程的安全问题
在字节码文件中,就详细说明了代码的执行过程
我们可以通过下面的命令,查看字节码的反汇编信息,其中就详细记录了代码的执行过程,
javap-v xx.class 打印堆栈大小,局部变量的数量和方法的参数。
查看字节码文件中代码执行
(1)先编译一下该类
(2)执行命令
(3)查看反汇编生成的字节码文件
java程序的二进制字节码文件
程序计数器就是在线程的切换时,记录代码的执行行号
案例
线程一先执行,执行行号为10 ,时间片的时间到了,切换到线程二执行,此时程序计数器需要保存线程一执行的终止行号,以便下一次的执行。线程二执行到行号9,线程二时间片到了,线程二的程序计数器会保存线程二的终止行号,然后线程一再次开始,从程序计数器记录的行号10,继续往下执行,执行到了行号20,再次进行切换。。。
2.什么是Java堆
Java堆是一个线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError的异常
堆的年轻代和老年代
年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
老年代主要保存生命周期长的对象,一般是一些老的对象
什么是元空间?
元空间:保存类信息,静态变量,常量,编译后的代码。
java1.7 和 java1.8的堆的区别
java8将堆中的方法区/永久代放到了本地内存,也就是元空间中
1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
什么是虚拟机栈?
Java Virtual machine Stacks(java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
案例:
栈帧1中的方法,调用栈帧2中的方法,栈帧2方法调用栈帧3的方法
则活动栈帧的顺序:栈帧1-->栈帧2-->栈帧3-->栈帧2-->栈帧1
垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放,并不需要垃圾回收。
栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k。栈帧过大会导致线程数变少,例如,机器总内存为512m,目
前能活动的线程数则为512个,如把栈内存改为2048k,那么能活动的栈帧就会减半。
方法内的局部变量是否是线程安全的?
案例:
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出的情况
栈帧过多导致栈内存溢出,典型问题:递归调用
栈帧过大导致栈内存溢出
堆栈的区别是什么?
1.栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会GC垃圾回收,而栈不会。
2.栈内存是线程私有的,而堆内存是线程共有的。
3.两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常,
栈空间不足:java.ang.StackOverFlowError。
堆空间不足:java.ang.OutOfMemoryError。
什么是方法区?
方法区(Method Area)是各个线程共享的内存区域
主要存储类的信息、运行时常量池
虚拟机启动的时候创建,关闭虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
方法区的内存溢出
将元空间的内存大小改为 8M
执行结果:方法区的内存太小
什么是常量池?
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
javap -v Application.class
查看字节码结构(类的基本信息、常量池、方法定义)
类信息
常量池
根据字节码文件查询常量池表
什么是运行时常量池?
常量池是 *class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实内存地址,从而执行指令。
什么是直接内存?
直接内存:并不属于IM中的内存结构,不由M进行管理。是虚拟机的系统内存,常见于 NI0 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高
例子
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 + " 秒");
}
}
IO VS NIO
常规IO的数据拷贝流程
- 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次),提升了性能。