本篇所以知识点均来自B站up主: 遇见狂神说
视频链接: https://www.bilibili.com/video/BV1iJ411d7jS
JVM的位置
JVM体系结构
JVM垃圾产生的位置
详细图
类加载器
作用:加载Class文件
new 一个对象的时候,是对一个抽象的对象使用new关键字,new完后就是使这个类变成一个具体的实例,引用地址在栈中,具体的实例对象是在堆中
public class Car {
public static void main(String[] args) {
// 类是模板,对象是具体的
// 当一个类被new的时候,这个类的名字也就是引用地址存放在栈内存中,具体的对象地址存放在堆内存中
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
// 运行结果:
//356573597
//1735600054
//21685669
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
// 运行结果:
// 1956725890
// 1956725890
// 1956725890
// 结论: 类是模板,对象是具体的,
// 通过getClass可以得到一个具体类的模板,如果是通过一个相同的模板创建的类话,在创建成功后,他们的模板类也是相同的不会发生改变
// new 完对象后得到的类的是不一样的,是对象模板类的一个实例化
}
}
实例化过程:
创建对象时栈和堆的关系:
类加载器的种类:
- 系统自带的加载器
- 启动类加载器(根加载器)
- 扩展类加载器
- 应用程序加载器
public class Car {
public static void main(String[] args) {
Car car = new Car();
Class<? extends Car> aClass = car.getClass();
// 打印这个类加载器
System.out.println(aClass.getClassLoader());
// 结果:sun.misc.Launcher$AppClassLoader@18b4aac2 应用加载器 java.lang.ClassLoader
// 打印上面的类加载器的父类
System.out.println(aClass.getClassLoader().getParent());
// 结果:sun.misc.Launcher$ExtClassLoader@1540e19d 扩展加载器 /jre/lib/ext包下
// 打印上面的类加载器的父类
System.out.println(aClass.getClassLoader().getParent().getParent());
// 结果:null null的原因有两种可能:1.确实不存在 2.java抓取不到,例如是用其他语言写的,java就抓取不到了 rt.jar包
}
}
双亲委派机制
执行机制
package java.lang;
public class String {
// 双亲委派机制
// 加载一个类的时候会按照这个顺序进行查询:
// APP(应用程序类加载器)-->EXT(扩展程序类加载器)-->BOOT(根加载器)
// 会发生两种情况:
// 1.APP和EXC和BOOT中都存在的话,会执行BOOT中的类,所以会发生下面的 在类 java.lang.String 中找不到 main 方法 的错误
// 2.如果BOOT中不存在这个类的话,就会执行EXC中的类,EXC也不存在的话就会执行APP中的这个类.
// 所以当你创建一个BOOT和EXT都不存在的类时就是发生下面的错误
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
// 结果:
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
}
}
情况2的示例
package com.kuang;
/**
* @author Zhm
* @date 2020/11/10 11:25
**/
public class Student {
@Override
public String toString() {
return "hello";
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.toString());
// 结果:hello
Class<? extends Student> aClass = student.getClass();
System.out.println(aClass.getClassLoader());
// 验证结果:sun.misc.Launcher$AppClassLoader@18b4aac2 执行的是应用加载器
}
}
结论:
- 类加载器收到加载类的请求
2. 类加载器会一层一层向上委托加载,直到启动类加载器(根加载器)(APP–>EXC–>BOOT)
3. 启动类加载器会去寻找这个类是否存在
1. 有的话就加载,然后结束
2. 没有的话就抛出异常,然后委派子类进行加载
4. 子类收到委派时,按照第三步进行执行
5. 全部类加载器都找不到的时候会报 Class Not Found 这个错误
作用:
1.防止加载同一个类,通过委托问一问,加载过的就不进行加载了,保证数据的安全
2.保证核心.class
文件不被篡改,通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
执行机制图:
沙箱安全机制
基本组件:
- 字节码校验器: 确保java文件遵循java语言规范,可以帮助java实现内存保护,但是不是所有的java文件都经过字节码校验器,比如说核心类就不会经过字节码校验器
- 类装载:其中在3个方面对java沙箱起作用
- 它防止恶意对代码去干涉核心代码 // 双亲委派机制
- 它守护了被信任类库编辑 // 双亲委派机制
- 它将代码归入了保护域,确定代码可以进行哪些操作 //沙箱安全机制
Native
native 关键字: 只要是带了natvie关键字的,说明java范围达不到了,需要去调用底层的C语言的库
执行情况:
- 会进入到本地方法栈
- 调用本地方法接口 JNI
- JNI的作用就是扩展java的使用,融合其他编程语言为java所用
JVM在内存区域中专门开辟了一块空间,就是本法方法栈(Native Method Stack) 用来登记native方法
在最终执行的时候,加载本地方法库中的方法通过JNI
java之所以要这样做是因为:
- 当初java刚兴起的时候是C和C++的市场,这样做可以调用C和C++的语言为java所用,以便java在市场上立足
- 更加扩展java 的功能,融合其他编程语言为自己所用
现在可以使用其他技术手段调用其他编程语言:
例如: Socket,WebService,http
PC寄存器
程序计数器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区方法字节码,在执行引擎读取下一条指令,是一个非常小的空间,几乎可以忽略不计
方法区
方法区是被所有的线程共享,所有的字段和方法字节码,以及一些特殊的方法,如:构造函数,接口代码也在这里定义
简单的说:所有定义的方法信息都保存在这个区域,属于共享区间
静态变量(static),常量(final),类信息(构造方法和接口定义)Class和运行时常量池都存在于方法区,但是实例变量存在于堆内存,与方法区无关
栈
栈:先进后出 后进先出
队列:先进先出,后进后出
栈内存溢出
由于栈的执行机制是先进后出,后进先出的,所以可以写一个递归:
方法A调用方法B,方法B调用方法A
package com.kuang.jvm;
/**
* @author Zhm
* @date 2020/11/10 20:46
**/
public class Test {
public static void main(String[] args) {
// 在主方法中执行方法A或者方法B
new Test().A();
}
// 定义一个方法A中调用方法B
public void A(){
B();
}
// 定义一个方法B中调用方法A
public void B(){
A();
}
// 执行结果:
// Exception in thread "main" java.lang.StackOverflowError 栈内存溢出
}
造成这个情况的发生是因为:由于是死递归,一直在栈内存中压栈,没有弹栈,无法释放栈内存空间,所以报栈内存溢出的错误
示例图:
栈生命周期:
主管程序的运行,生命周期和线程同步;
线程结束后栈内存就会释放,对于栈来说,不存在垃圾也就不会有垃圾回收
栈内存存储:
8大基本类型+对象的引用地址+实例的方法
栈运行原理: 栈帧
栈满了就会报:栈内存溢出的错误:StackOverflowError
栈+堆+方法区交互关系:
三种JVM
- HotSpot
- IBM
- BEA
堆
**概念:**heap 一个jvm只有一个堆内存空间,堆的内存大小可以进行调节
**存放内容:**类加载器加载类后会把类,方法,常量。保存我们的引用类型的真实对象
三个存储空间:
-
年轻代: (轻GC)
- 伊甸园区(Eden)
- 幸存0区
- 幸存1区
-
老年代: (重GC)
-
永久代:
GC垃圾回收主要是回收伊甸园区和老年区的中的数据
加入堆内存满了,OOM,会报出堆内存溢出错误 java.lang.OutOfMemoryError: Java heap space
模拟错误:
public class HeapTest {
public static void main(String[] args) {
String s = "1231";
while (true) {
s += s + new Random().nextInt(999999999) + new Random().nextInt(999999);
}
}
}
// 执行结果:java.lang.OutOfMemoryError: Java heap space
JDK1.8之后永久区发生了变化,在JDK1.8之后永久区没有了,元空间替代了永久区
新生代
- 类:诞生,成长,死亡的地方
- 伊甸园区:
- 幸存区
- 幸存区0
- 幸存区1
**GC机制:**首先当伊甸园区内存满了时候会触发的是轻GC,然后GC结束后,没有被垃圾回收的对象会被放在幸存区,当幸存区内存满了会触发重GC,然后GC结束后,会将没有被回收的对象放在老年区,当老年区也满了的话就会报OOM堆内存溢出的异常
永久代
这个区域是常驻在内存中,存放java自身的类和接口元数据,不用的静态变量
这个区域的垃圾回收是在回收老年代的时候会回收这个区域,在关闭VM虚拟机的时候也会释放该区域的内存
元空间中的出现OOM异常的情况:
- 一个启动类中加载了大量的第三方的jar包
- Tomcat部署了过多的应用
- 大量动态生成反射类,不断的加载则会造成这个内存溢出
- JDK1.6之前 : 永久区,常量池在方法区
- JDK1.7:永久代慢慢退化,提出一个
去永久代
常量池在堆中 - JDK1.8:没有永久代,常量池在元空间
堆内存空间示例图
元空间逻辑上存在,物理上不存在
假设在项目中出现了OOM故障,应该如何排查?
- 能看到代码第几行出错:内存快照工具:MAT,Jprofiler
- Debug,一行行分析
MAT,Jprofiler工具的作用:
- 分析Dump内存文件,快递定位内存泄露问题
- 获得堆中的数据
- 获得大的对象
使用JProfiler工具定位到出现故障的代码
常见的命令:
package com.kuang.jvm;
import java.util.Random;
/**
* @author Zhm
* @date 2020/11/11 18:11
* 常用的命令:
* -Xms 初始化内存
* -Xmx 最大使用内存
* -XX:PrintGCDetails 打印GC处理过程
* -XX:+HeapDumpOn 产生Dump文件
* -Xms1m -Xmx8m -XX:+PrintGCDetails 初始化内存1m 最大内存8m 打印GC处理过程
* -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
**/
public class HeapTest {
public static void main(String[] args) {
String s = "1231";
while (true) {
s += s + new Random().nextInt(999999999) + new Random().nextInt(999999);
}
}
}
GC常见问题:
- JVM的内存模型和对应存放的东西
- 堆内存中分区有哪些? 并说一下他们的特点
- GC算法有哪些?以及对应的特点
- 轻GC和重GC分别在什么时候发生
GC常见算法
引用计数算法
就是给每一个对象分配一个计数器,每调用一次计数器就加一,计数器一直没有变化的就会被垃圾回收
复制算法
年轻代主要用的是复制算法进行的GC
演变过程:
优点:
- 没有内存的碎片
缺点:
- 浪费的一块幸存区的空间 (幸存区to区始终是空的),在极端的情况下,比如说对象100%存活,这个时候采用复制算法的话就会很降低效率
复制算法的最佳使用场景:
是对象存活度较低的情况,这种情况就是对象在新生区的情况,所以在新生区使用的是复制算法
标记清除算法
缺点:
- 两次扫描,严重浪费时间
- 产生内存碎片
优点:
- 不需要额外的空间
标记压缩算法
说白了就是对标记清楚算法的再优化,解决了标记清除算法中的产生内存碎片的缺点
算法总结
- 内存效率:复制算法(每次执行一次)>标记清除算法(每次执行两次)>标记压缩算法(每次执行三次)
- 内存整齐度:复制算法(没有内存碎片)=标记压缩算法(没有内存碎片)>标记清除算法(有内存碎片)
- 内存利用率:标记压缩算法=标记清除算法>复制算法(会使幸存区to区空着)
没有最好的算法,只有最合适的算法
GC : 分代收集算法
- 年轻代: 由于对象存活率低,所以使用的是复制算法效率高
- 老年代:在内存碎片不多的时候使用
标记清除算法
内存碎片多的时候使用过标记压缩算法
GC调优老年代的GC的话可以按照上述的情况改变采用标记清除或者标记压缩使用的时机实现调优
JMM
什么是JMM
JMM:Java Memory Model的缩写
JMM的作用
缓存一致性协议,用来定义数据速写的规则
JMM定义了线程工作与主内存之间的关系:
线程之间的共享变量存在于主内存(Main Memory)中.每个线程都有一个私有的本地内存(Local Memory)
解决对象可见性的问题可以使用volilate关键字或者使用synchronized同步锁