先想想一些问题
1 我们开发人员编写的Java代码是怎么让电脑认识的
首先先了解电脑是二进制的系统,他只认识 01010101
比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的 HelloWord.java是我们程序员编写的,我们人可以认识,但是电脑不认识
Java文件编译的过程 因此就需要编译:
- 程序员编写的.java文件
- 由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件)
- 在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切)
(这是一个大概的观念 抽象画的概念)
2 为什么说java是跨平台语言
这个夸平台是中间语言(JVM)实现的夸平台 java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统
难道 C 和 C++ 不能夸平台吗 其实也可以 C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样
3 Jdk和Jre和JVM的区别
看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM
Jvm在倒数第二层 由他可以在(最后一层的)各种平台上运行
Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库
Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具
4 为什么要学习JVM
为什么要学习Jvm,学习Jvm可以干什么
首先先想:为什么Java可以霸占企业级开发那么多年 因为:内存管理
我们在java开发中何时考虑过内存管理
不像c和c++还要考虑什么时候释放资源
我们java只需要考虑业务实现就行了
那就有些人可能又会要说了,Jvm都做完了这些操作,为什么我们还要学习,学习个屁啊
假如:内存出现问题了,出现了内存溢出 ,内存泄漏问题怎么办
这就好像一个人一样,我一般情况吃什么从来不用考虑进入了身体那一个部位,可是总有一天,假如吃了不该吃的也是要进医院的
深入学习JVM
注释:JVM就是Java虚拟机,Java虚拟机就是JVM
1 JVM运行时数据区
什么是运行时数据区(就是我们java运行时的东西是放在那里的)
2 解析JVM运行时数据区
2.1 方法区(Method Area)
- 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
2.2 Java堆(Java Heap)
- java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
- 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
- java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
- 从内存回收角度来看java堆可分为:新生代和老生代。
- 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
- 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
- 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
2.3 程序计数器(Program Counter Register)
- 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
总结:也可以把它叫做线程计数器
例子:在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。
假如:线程A在看直播
突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起 然后,视频电话结束,这时线程A究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:程序计数器来记录)
2.4 Java虚拟机栈(Java Virtual Machine Stacks)
- java虚拟机是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
解释:每虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
解析栈帧:
- 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
- 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
- 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
- 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落
思考:. 一个方法调用另一个方法,会创建很多栈帧吗? 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
栈指向堆是什么意思? 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址,堆中的数据等下讲
递归的调用自己会创建很多栈帧吗? 递归的话也会创建多个栈帧,就是一直排下去
2.5 本地方法栈(Native Method Stack)
- 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
- 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务
- native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
- 同理可得,本地方法栈中就是C和C++的代码
3 Java内存结构
上面已经讲了运行时数据区,这里就差几个小组件了
3.1 直接内存(Direct Memory)
- 直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是既然是内存,肯定还是受本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。
- 在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native(本地)堆中来回复制数据。
直接内存与堆内存的区别: 直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低 直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显
代码示例:(报错修改time 值)
package com.lijie;
import java.nio.ByteBuffer;
/**
* 直接内存 与 堆内存的比较
*/
public class ByteBufferCompare {
public static void main(String[] args) {
allocateCompare(); //分配比较
operateCompare(); //读写比较
}
/**
* 直接内存 和 堆内存的 分配空间比较
*/
public static void allocateCompare() {
int time = 10000000; //操作次数
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocate(2); //非直接内存分配申请
}
long et = System.currentTimeMillis();
System.out.println("在进行" + time + "次分配操作时,堆内存:分配耗时:" + (et - st) + "ms");
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = B