狂神视频地址: https://www.bilibili.com/video/BV1iJ411d7jS
1. JVM的位置
2. JVM的体系结构
JVM 调优百分之99都是在堆里面调优,方法区是特殊的堆。
3. 类加载器
- 作用:加载Class 文件,~ new Student();
- 类似模板,是抽象的
- 对象是实现,是具体的
类是模板(抽象的),而对象是具体的
- 分类:
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
4. 双亲委派机制
// 双亲委派机制:安全
// APP–>EXC–>BOOT(最终执行)
// BOOT
// EXC
// APP
- 第一步:类加载器收到类加载的请求
- 第二步:将这个请求向上委托给父类加载器去完成 ,一直向上委托,直到启动类加载器(Boot)
- 第三步:启动类加载器检查是否能够加载当前和这个类 ,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载。
- 第四步:重复 第三步 步骤。
null : java调用不到 ~ C 、C++
Java = C++ : 去掉繁琐的东西,指针,内存管理。(Java = C++ --)
5. 沙箱安全机制
参考:https://blog.youkuaiyun.com/qq_30336433/article/details/83268945
6. Native
凡是带了native 关键字的,说明java 的作用范围达不到了,会去调用底层C 语言的库。
- 会进入本地方法栈。
- 调用本地接口:JNI
- JNI作用:扩展Java的使用,融合不同的编程语言为Java 所用!最初是想融合C、C++
- Java诞生的时候,C和C++横行,要想立足,必须要有调用C/C++的程序。所以它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native 方法
- 在最终执行的时候去加载本地方法库的方法,通过JNI
本地方法接口(JNI)Java Native Interface
本地方法库
private native void start0();
调用其他接口:Scoket 、WebService~…http
7. PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
8. 方法区
Method Area 方法区
- 方法区是被线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是,实例变量存在堆内存中,和方法区无关 - 方法区里面存以下内容
static,final,Class 类模板,常量池 - 类加载过程(面试)
new 一个类的时候,先在方法区有一个类的模板
类模板完了 ,方法区还有个常量池
引用在栈内存
真实的 对象在堆内存
引用指向堆内存真实的地址
(参考Java基础)
9. 栈
栈:数据结构
程序 = 数据结构 +算法 : 持续学习~
程序 = 框架 + 业务逻辑 : 吃饭
栈:先进后出、后进先出
队列:先进先出(FIFO:First Input First Output)
- 为什么main 方法 先执行,最后结束!
栈:栈内存,主管程序的运行,生命周期与线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题 - 一旦线程结束,栈就over了
栈:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧
程序正在执行的方法,一定在栈的顶部
栈 + 堆 + 方法区:的一些交互关系
- 一个对象在内存中的实例化过程
public class People{
String name; // 定义一个成员变量 name
int age; // 成员变量 age
Double height; // 成员变量 height
void sing(){
System.out.println("人的姓名:"+name);
System.out.println("人的年龄:"+age);
System.out.println("人的身高:"+height);
}
public static void main(String[] args) {
String name; // 定义一个局部变量 name
int age; // 局部变量 age
Double height; // 局部变量 height
People people = new People() ; //实例化对象people
people.name = "张三" ; //赋值
people.age = 18; //赋值
people.stuid = 180.0 ; //赋值
people.sing(); //调用方法sing
}
}
在程序的执行过程中,首先类中的成员变量和方法体会进入到方法区,如图:
程序执行到 main() 方法时,main()函数方法体会进入栈区,这一过程叫做进栈(压栈),定义了一个用于指向 Person 实例的变量 person。如图:
程序执行到 Person person = new Person(); 就会在堆内存开辟一块内存区间,用于存放 Person 实例对象,然后将成员变量和成员方法放在 new 实例中都是取成员变量&成员方法的地址值 如图:
接下来对 person 对象进行赋值, person.name = “小二” ; perison.age = 13; person.height= 180.0;
先在栈区找到 person,然后根据地址值找到 new Person() 进行赋值操作。
如图:
当程序走到 sing() 方法时,先到栈区找到 person这个引用变量,然后根据该地址值在堆内存中找到 new Person(),找到方法地址 进行方法调用。
在方法体void sing()被调用完成后,就会立刻马上从栈内弹出(出栈 )
最后,在main()函数完成后,main()函数也会出栈 如图:
10. 三种JVM
- Sun公司 HostSpot Java HotSpot™ 64-Bit Server VM (build 25.101-b13, mixed mode)
- BEA Jrockit
- IBM J9 VM
我们学习的都是HotSpot
11. 堆
Heap
一个JVM 只有一个堆内存,堆内存的大小是可以调节的。
- 类加载器读取了类文件后,一般会把什么东西放到堆中?
类的实例、方法、常量、变量~,保存我们所有引用类型的真实对象 - 堆内存中还要细分为三个区域:
- 新生区 (伊甸园区) Young/New
- 养老区 old
- 永久区 Perm
- GC 垃圾回收主要是在伊甸园区和养老区~
- 假设内存满了,OOM ,堆内存不够!
- 在JDK 8以后,永久存储区改了个名字(元空间)
11.1 新生区
- 类:诞生 和 成长的地方、甚至死亡。
- 伊甸园区,所有的的对象都是在伊甸园区new 出来的!
- 幸存者区(0,1)
- 假如伊甸园区满了,就触发一次轻GC,这次GC有以下情况:
- 有的对象可以还被引用,就幸存下来了。
- 有的对象没有被引用了,就死了、没了。
- 幸存的下来的对象就移动到幸存区。
- 当伊甸园区和幸存区都满了,就会触发一次重GC。
- 重gc 清理一次后,能活下来的对象就进入养老区了。
(就跟一场战争一样,不断的活下来)
- 重gc 清理一次后,能活下来的对象就进入养老区了。
真理:经过研究,99%的对象都是临时对象! new
11.2 永久区
- jdk 1.6之前:永久代,常量池是在方法区中
- jdk 1.7 :永久代,但是慢慢的退化了,去 永久代,常量池在堆中
- jdk 1.8 之后:无永久代,常量池在元空间
永久区常驻内存的,用来存放JDK自身携带的class对象,interface元数据,存储的是Java运行时的一些环境或类信息这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存
OOM出现条件:一个启动器,加载了大量的第三方jar包;Tomcat部署了太多应用,大量动态生成的反射类,不断被加载,直到内存满,就会出现OOM。
元空间:逻辑上存在,物理上不存在。
11.3 OOM排查
- 在一个项目中,突然出现OOM 故障,那么该如何排除研究为什么出错
- 能够看到代码第几行出错:内存快照分析工具,Eclipse MAT,Jprofiler
- Debug,一行行分析代码~
- MAT,Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象~
- 当出现OOM
- 尝试扩大堆内存看结果
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- 尝试扩大堆内存看结果
// -Xms1024m -Xmx1024m -XX:+PrintGCDetails
public static void main(String[] args) {
// 返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory(); //字节 1024*1024
// 返回JVM的总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("max="+maxMemory+"字节\t"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("total="+totalMemory+"字节\t"+(totalMemory/(double)1024/1024)+"MB");
// 默认情况下,分配的总内存 是电脑内存的1/4,而初始化内存 是1/64
}
// -Xms8m -Xmx8m -XX:+PrintGCDetails
public static void main(String[] args) {
String str = "jdskanvkdfjhaljfnds";
while (true) {
str += str + new Random().nextInt(888888888) + new Random().nextInt(999999999);
}
}
- 分析内存,看一下哪个地方出现了问题(专业工具)
idea 安装Jprofiler
。。。
12. GC 垃圾回收机制
垃圾回收的区域只有在堆里面(方法区在堆里面)
JVM 在进行垃圾回收(GC)时,并不是堆这三个区域统一回收。大部分时候,回收的都是新生代~
新生代
幸存区(form,to)
老年区
-
GC 两种类
- 轻GC(普通GC):只针对 新生代 和 偶尔走一下 幸存区。
- 重GC (全局GC): 全部清完。
-
面试题目:
- JVM的内存模型和分区~详细到每一个区放什么?
- 堆里面的分区有哪些?
- GC的算法有哪些?
- 标记清除法
- 标记整理
- 复制算法
- 引用计数法
- 怎么用的?
- 轻GC 和 重GC 分别再什么时候发生?
12.1 引用计数法
假设我对象A 用了 一次就给它加上1
假设我对象B 用了 两次就给它加上2
假设我对象C 没有使用 就是 0
引用计数法就是给每个对象分配一个计数器
假设C 对象为 0,它就要被清除出去了
JVM 现在一般不采用这种方式,不高效。
12.2 复制算法
每次GC 都会将 伊甸园区 活得对象 移动到 幸存区 中,如果幸存区放不下 ,就移到养老区中。
一旦伊甸园区被GC 后,就会是空的。
当某对象从伊甸园区 存活下来了。
谁空谁是to
假设这个对象还活着,它就把这个对象复制到另一个区域 要么是 form 要么是 to
当某个对象 经历15次(默认值)GC 都还没有死的时候,就会进入养老区。
- 图解复制算法:
- 假设 幸存区里面,to 是空的,form 里面有对象。
- 现在要做一次垃圾回收了
首先:伊甸园区 存活的对象往 to 里面走
其次,form 区里面的 对象也要往 to 里面走
每次清理完之后,伊甸园区是空的,to区是空的。
- 经历15次GC之后,会把幸存区 里面或者的对象 移到养老区,或者有一些没有到15次就被清理掉
- 假设 幸存区里面,to 是空的,form 里面有对象。
好处:没有内存的碎片
坏事:浪费一半内存的空间:多了一半空间用于是空的。
复制算法最佳使用场景:对象存活度较低的时候;新生区
12.3 标记清除算法
扫描这些对象,对活着的对象进行标记
清除:对没有标记的对象,进行清除
优点:不需要额外的空间
缺点:两次扫描严重浪费时间,会产生内存碎片。
12.4 标记压缩
上面的再优化:防止内存碎片的产生(再次扫描,向一端移动存活的对象)
但是多了一个移动成本
12.5总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率: 标记压缩算法 = 标记清除算法 > 复制算法
- 没有最优算法吗?
- 没有最好的算法,只有最合适的算法~ ---->GC: 分代收集算法
- 每一代用合适的算法就好了。
- 年轻代: (大部分的对象都在这里都死了)
- 存活率低
- 复制算法!
- 老年代:
- 存活率高,区域大
- 标记清除 + 标记压缩 混合实现
JMM 新知识快速学习