1、JVM的位置
2、JVM的体系结构
3、类加载器(Class Loader)
作用:加载Class文件。
1、虚拟机自带的加载器
2、启动类(根)加载器:BootStrapClassLoader:引导类加载器,负责java核心类的加载
3、扩展类加载器 :ExtensionClassLoader
4、应用程序加载器 : AppClassLoader:应用类加载器 负责加载我们写的一些类
Class Loader 加载过程:
1. 加载:是将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。
2. 连接:
(1)验证是否有正确的内部结构,并和其他类协调一致。
(2)准备负责为类的静态成员分配内存,并设置默认初始化值。
(3)解析将类的二进制数据中的符号引用替换为直接。
3. 初始化:就是我们以前讲过的初始化步骤。
4、双亲委派机制
1、类加载器收到类加载的请求 Application
2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器。
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器。
4、重复步骤3
Class Not Found (如果都没有就会抛出)
null:java调用不到
5、沙箱安全机制
參考:https://blog.csdn.net/qq_30336433/article/details/83268945
6、Native
认识 native 即 JNI,Java Native Interface
凡是一种语言,都希望是纯。比如解决某一个方案都喜欢就单单这个语言来写即可。Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。
package com.panghl.jvm;
/**
* @Author panghl
* @Date 2021/5/20 21:33
* @Description TODO
**/
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"my thread").start();
}
//native:凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层C语言的库!
//会进入本地方法栈-->调用本地方法接口 JNI
//JNI作用: 扩展java的使用,融合不同的编程语言为Java所用! 最初:C,C++
// Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
//它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记natice方法
//在最终执行的时候,加载本地方法库中的方法通过JNI
//JAVA程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见!
private native void start0();
//调用其他接口: Socket、WebService、http
}
点进去看start的源码可以发现:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
Native Interface 本地接口
本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++程序,于是就在内存中开辟了一块处理标记为native的代码,它的具体做法是在Native Method Stack中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在(Executing Engine)执行引擎执行的时候加载Native Libraies。【本地库】
7、PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
8、方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中和方法区无关
static ,final ,Class模块,常量池
9、栈
栈是运行时的单位,Java 虚拟机栈,线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表(形参也是局部变量)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;
(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
3. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。
对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
参考:
https://www.cnblogs.com/newAndHui/p/11168791.html
https://blog.csdn.net/qq_41517936/article/details/108502249
10、三种JVM
- SUN公司 Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
- BEA
- IBM-J9VM
11、堆
堆是存储时的单位,对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。
jvm堆内存划分如图所示:
- JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
- 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
堆和栈的对比
一、栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
二、栈因为是运行单位,因此里面存储的信息都是跟当前线程相关的信息。包括:局部变量(含形参)、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
三、在方法中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配(这一句里面的方法和函数感觉有点不对,方法和函数区别)。堆内存用于存放由new创建的对象和数组。
四、在Java中一个线程就会相应有一个线程栈与之对应,这点保证了程序的并发运行。
而堆则是所有线程共享的,也可以理解为多个线程访问同一个对象,比如多线程去读写同一个对象的值
五、栈内存溢出包括StackOverflowError和OutOfMemoryError。StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存;
堆内存溢出是OutOfMemoryError。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。
堆内存详解:https://blog.csdn.net/lingbo229/article/details/82586822
12、新生区、老年区
新生区
- 类:诞生和成长的地方,甚至死亡
- 伊甸园,所有的对象都是在伊甸园区new出来的!
- 幸存者区(0,1)
老年区
在清理整个堆空间(重GC)后还没销毁的对象进去老年区。
真理:经过研究:99%的对象都是临时对象!
永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收。关闭VM虚拟机就会释放这个区域的内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署太多的应用,大量动态生成反射类。不断的被加载。直到内存满,就会出现OOM。
- jdk1.6之前:永久代,常量池是在方法区中。
- jdk1.7 :永久代,但是慢慢的退化了。 “去永久代”,常量池在堆中
- jdk1.8之后:无永久代,常量池在元空间。
13、堆内存调优
逻辑上存在,物理空间不存在。
代码实践:
package com.panghl.jvm;
/**
* @Author panghl
* @Date 2021/5/23 14:58
* @Description TODO
**/
public class Demo01 {
public static void main(String[] args) {
//返回虚拟机视图使用的最大内存
long max = Runtime.getRuntime().maxMemory(); //字节 1024*1024
//返回jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max:"+max+"字节-》:"+(max/1024/1024)+"MB");
System.out.println("total:"+total+"字节-》:"+(total/1024/1024)+"MB");
// 默认情况下:分配的总内存 是电脑内存的四分之一,而初始化的内存: 1/64
}
//OOM
//1.尝试扩大堆内存看结果
//2.分析内存,看一下那个地方出现了问题(专业工具)
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails
// 年轻代+永久代 = 981.5M
}
JVM堆内存常用参数
参数 | 描述 |
---|---|
-Xms | 堆内存初始大小,单位m、g |
-Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
-XX:PermSize | 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了 |
-XX:MaxPermSize | 非堆内存最大允许大小 |
-XX:NewSize(-Xns) | 年轻代内存初始大小 |
-XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
-XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1 |
-Xss | 堆栈内存大小 |
在一个项目中,突然出现了OOM故障,那么该如何排除~研究为什么出错?
- 能够看到代码第几行出错:内存快照分析工具,MAT、Jprofiler
- Debug,一行行分析代码!
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄露;
- 获得堆中的数据
- 获得大的对象~
- ...
package com.panghl.jvm;
import java.util.ArrayList;
/**
* @Author panghl
* @Date 2021/5/23 15:33
* @Description Dump -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
**/
/**
* Xms 设置初始化内存分配大小 1/64
* Xmx 设置最大分配内存,默认1/4
* -XX:+PrintGCDetails //打印GC垃圾回收信息
* -XX:+HeapDumpOnOutOfMemoryError //oom DUMP
*/
public class Demo02 {
byte[] arr = new byte[1 * 1024 * 1024]; // 1m
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Demo02());
count++;
}
} catch (OutOfMemoryError error) {
System.out.println("count:"+count);
error.printStackTrace();
}
}
}
14、GC:垃圾回收
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代。
- 新生代
- 幸存区(form,to)
- 老年区
GC两种类型:轻GC(普通GC),重GC(全局GC)
GC题目:
- JVM的内存模型和分区~详细到每个区放什么?
https://www.cnblogs.com/infinity-zhang/p/13378598.html - 堆里面的分区有哪些?Eden、form、to、老年区,说说他们的特点!
https://blog.csdn.net/Soldier49Zed/article/details/102755507 - GC的算法有哪些?标记清除法、标记整理法、复制算法、引用计数法,怎么用的?
https://blog.csdn.net/lingbo229/article/details/82586822 - 轻GC和重GC分别在什么时候发生?
https://blog.csdn.net/lingbo229/article/details/82586822
引用计数法:
复制算法:
红色是标记的非活动对象,绿色是活动对象。
- 将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
- 好处:没有内存的碎屏
- 缺点:浪费了内存空间:多了一半空间永远是空(to)。假设对象100%存活(极端情况)--》OOM
复制算法最佳使用场景:对象存活度较低的时候;新生区
标记-清理:
GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
- 优点:不需要额外的空间。
- 缺点:两次扫描,严重浪费时间,会产生内存碎片,
标记-整理:
也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题,但也多了一个移动成本。
一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。
15、JMM(Java内存模型)
JMM :Java 内存模型
【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。
作用:缓存一致性协议,用于定义数据读写的规则(遵守)
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题:volilate
https://juejin.cn/post/6919350421685288973
BAT 经典面试题:
https://blog.csdn.net/weixin_43759452/article/details/90294670
16、垃圾收集器
https://blog.51cto.com/lizhenliang/2164876?wx=
总结
内存效率: 复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法---> GC:分代收集算法
年轻代:
- 存活率低
- 复制算法
老年代:
- 存活率高,区域大
- 标记-整理 + 标记清除 混合实现