文章目录
1. JVM的位置
2. JVM的体系结构
3. 类加载器
作用:加载class文件
- new Student();
将抽象类Student实例化,其引用在栈里,实例放在堆里
类是抽象的,对象是具体的
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用类加载器
作业:1. 自己画类加载机制
2. 百度双亲委派机制
3.1 概念
**概念:**类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
-
在什么时候才会启动类加载器?
其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
-
从哪个地方去加载.class文件
在这里进行一个简单的分类。例举了5个来源
(1)本地磁盘
(2)网上加载.class文件(Applet)
(3)从数据库中
(4)压缩文件中(ZAR,jar等)
(5)从其他文件生成的(JSP应用)
3.2 类加载过程
类生存周期:加载至虚拟机内存—>卸载出内存
包含了:加载、验证、准备、解析、初始化、使用、卸载
类的加载:加载、验证、准备、解析、初始化
1. 加载
三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问接口
程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载
2. 验证
作用:确保加载类的正确性
四步:
(1)验证文件格式:验证 .class 文件字节流是否符合class文件的格式规范,并且能够被当前版本的虚拟机处理。主要校验:魔数、主版本号、常量池等
(2)验证元数据:对字节码描述的信息进行语义分析,保证信息符合java语言规范要求。例:此类是否有父类,字段方法是否与父类冲突等
(3)验证字节码:最复杂阶段。分析数据流与控制流,确定语义是否合法及符合逻辑。对类的方法进行验证。
(4)验证符号引用:发生在虚拟机将符号引用转化为直接引用的时候。主要对类自身以外的信息进行校验。目的在于确保解析动作能够完成。
3. 准备
作用:为类变量分配内存并设置初始值
注意:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随对象实例化才会一起分配到java堆中
(2)初始值指数据类型默认值
static final为初始值,非默认值
4. 解析
作用:JVM将常量池中的符号引用转化为直接引用
符号引用:标签
直接引用:指针、相对偏移量、直接或间接定位到目标的句柄。与虚拟机实现的内存有关
解析主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 7类符号引用
5. 初始化
作用:执行类构造器()方法
设定初始值:
(1)声明类变量时指定初始值
(2)使用静态代码块为类变量指定初始值
JVM初始化步骤:
(1)假如类未被加载和链接,则加载并连接该类
(2)假如类的直接父类未被初始化,则初始化器直接父类
(3)假如类中存在初始化语句,则依次执行初始化语句
3.3 类初始化时机
主动使用类的时候才会初始化类,主动使用分为6种
(1)创建类实例,即new
(2)访问类或接口的静态变量,或对静态变量赋值
(3)调用类的静态方法
(4)反射(Class.forName(“com.shengsiyuan.Test”))
(5)初始化类的子类,则其父类也会被初始化
(6)JVM虚拟机中的启动类
3.4 类加载器
位置:类加载器位于JVM的外部,方便程序员决定如何获取所需的类
1. 系统自带的三个加载器:
(1)Bootstrap ClassLoader:启动类(根)加载器
(2)Extention ClassLoader:扩展类加载器
(3)Appclass Loader:应用类加载器
加载顺序:APP(应用类加载器)< EXC (扩展类加载器)< BOOT(启动类(根)加载器)
层次关系:
2. 类加载的三种方式
(1)通过命令行启动启动应用时由JVM初始化加载含有main()方法的主类
(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name, initialize, loader)中的initlize可指定是否要执行初始化块
(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块
4. 双亲委派机制
保证安全
- APP(应用类加载器) --> EXC (扩展类加载器)–> BOOT(启动类(根)加载器)
过程
- 类加载器收到加载类的请求
- 将这个请求委托给父类加载器,一直向上委托,直到根加载器
- 根加载器检查是否能够加载当前类,能加载就结束,否则抛出异常,通知子加载器进行加载
- 重复加载 3
- 最经典错误:Class Not Found
null : java调用不到:java(c+±-)的底层为c、c++,去掉了c++的指针,内存交给了JVM
操作系统级的东西都还是调用c++执行
好处举例
比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象
归纳
(1)避免重复加载,父类已经加载了,子类就不需要再次加载
(2)更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
5. 沙箱安全机制
6. Native
凡是带了native 关键字的,说明java的作用范围达不到了,进入本地方法栈,调用底层c\c++了
(1)进入本地方法栈
(2)调用本地方法接口 JNI
(3)JNI作用:扩展java的使用。融合不同的编程语言为java所用,最初c、c++
(4)java诞生的时候,c、c++使用比较多,因此需要扩展调用c、c++程序
(5)他在内存区域中专门开辟了一块标记区域:Native Method Stack,登记 Native 方法
(6)在最终执行的时候,加载本地方法库中的方法通过JNI
调用其它接口:
(1)Socket, WebService, http
7. PC寄存器
程序计数器:Program Counter Register
每一个线程都有一个程序计数器,是线程私有的,指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
8. 方法区
Method Area 方法区
方法区是被所有线程共享,所有字节码和方法字节码,以及一些特殊的方法,如构造函数、接口的代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
包含:
static、final、Class模板、运行时常量池
9. 栈
栈:数据结构
程序=数据结构+算法:学习路线
程序=框架+业务逻辑:吃饭
栈:先进后出:LIFO
队列:先进先出:FIFO
例:main方法,最先进去方法区,最后结束(方法开始运行进栈,运行结束被弹出栈)
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存就释放。对于栈来说,不存在垃圾回收问题
栈内:8大基本类型 + 对象引用 + 实例的方法
栈运行原理:栈针
正在执行的方法,一定位于栈的顶部
栈满:StackOverflowError
栈 + 堆 + 方法区:交互关系
百度:栈的东西怎么存
画出对象实例化的过程
10. 三种JVM
- Sun公司 HotSpot Java HotSpot™ 64-Bit Server VM 18.9 (build 11.0.5+10-LTS, mixed mode)
- BEA JRokit 号称最快的JVM
- IBM J9VM
11. 堆Heap
一个JVM只有一个Heap,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中呢?类、方法、常量、变量,保存引用类型的真实对象
堆内存中还要细分三个区域:
11.1 新生区(伊甸园区)Young/New
类:诞生、成长、死亡的地方
伊甸园区:所有的对象都是在伊甸园区new出来的
幸存者(0/1)区:轻GC之后,仍然留下的对象会来到幸存者区,其中0区与1区是动态可交换的
当一个对象经历了15次(默认值)GC,还没有被清除,则会从新生区进入老年区
11.2 养老区 Old
重GC之后,仍然留下的对象回来到养老区
11.3 永久区 Perm
这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时环境或类信息。这个区域不存在垃圾回收。关闭虚拟机就会释放永久区。
当一个启动类加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM;
- JDK1.6之前:永久代,常量池在方法区中
- JDK1.7:永久代,但是慢慢的退化了(去永久代),常量池在堆中
- JDK1.8之后:无永久代->元空间,常量池在元空间
11.4 堆内存调优
GC垃圾回收主要针对伊甸园区与养老区
假设内存满了,OutOfMemoryError(OOM),堆内存不够
在JDK8以前,永久存储区有另外一个名字,叫(元空间)
经过研究:99%的对象都是临时对象
默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存是电脑内存的1/64
package jvm.heapDemo;
// 修改JVM堆内存的大小
public class HeapDemo01 {
public static void main(String[] args) {
// 返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
// 返回jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB"); // max=2107637760字节 2010.0MB
System.out.println("total="+total+"字节\t"+(total/(double)1024/1024)+"MB"); // total=132120576字节 126.0MB
// 默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存是电脑内存的1/64
/*
我的java版本
java -version
java version "11.0.5" 2019-10-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.5+10-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.5+10-LTS, mixed mode)
*/
/**
* 改变堆内存的大小:
* Run/Debug Configure-->VM Options:
* -Xmx2000m -Xms1000m -XX:+PrintGCDetails
*
* 但是出警告了:
* [0.002s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
* 修改之后:
* -Xmx2000m -Xms1000m -Xlog:gc*
*
* 注意:-Xmx的参数值>=-Xms的参数值
* Error occurred during initialization of VM
* Initial heap size set to a larger value than the maximum heap size
*/
/*
运行结果:
[0.005s][info][gc,heap] Heap region size: 1M
[0.021s][info][gc ] Using G1
[0.021s][info][gc,heap,coops] Heap address: 0x0000000083000000, size: 2000 MB, Compressed Oops mode: 32-bit
max=2097152000字节 2000.0MB
total=1048576000字节 1000.0MB
[0.098s][info][gc,heap,exit ] Heap
[0.098s][info][gc,heap,exit ] garbage-first heap total 1024000K, used 4096K [0x0000000083000000, 0x0000000100000000)
[0.098s][info][gc,heap,exit ] region size 1024K, 5 young (5120K), 0 survivors (0K)
[0.098s][info][gc,heap,exit ] Metaspace used 1107K, capacity 4611K, committed 4864K, reserved 1056768K
[0.098s][info][gc,heap,exit ] class space used 107K, capacity 432K, committed 512K, reserved 1048576K
*/
/*
当遇到OOM时:
1. 尝试扩大堆内存,当仍出错时,说明代码有问题
2. 分析内存,看一下那个地方出现了问题
*/
/*
对于java8:一般来说,新生代+老年代的内存=堆最大内存
因此,元空间的内存并不存在堆区,所以说:元空间 逻辑上存在,物理上不存在
对于java11:只有garbage-first,内存为1024000K=1024MB,等于jvm的初始化总内存的大小,因此Metaspace仍然不存在于堆内存中
*/
}
}
在项目中出现了OOM故障
- 能够看到第几行出错:内存快照分析工具,MAT,Jprofiler
- Dedug,一行行分析代码
MAT,Jprofiler作用:
- 分析Dump文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象
需要安装JProfiler
/**
* 设置大小,并将OOMError下载下来
* -Xms10m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class OOMDemo01 {
byte[] bytes = new byte[1 * 1024 * 1024]; // 1MB
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
int count = 0;
try{
while(true){
objects.add(new OOMDemo01()); // 问题所在
count++;
}
}catch (OutOfMemoryError oom){
System.out.println(count);
oom.printStackTrace();
}
}
}
// 10M大小,共运行了9次
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4648.hprof ...
Heap dump file created [12627317 bytes in 0.023 secs]
9
java.lang.OutOfMemoryError: Java heap space
at jvm.heapDemo.OOMDemo01.<init>(OOMDemo01.java:10)
at jvm.heapDemo.OOMDemo01.main(OOMDemo01.java:17)
15. GC垃圾回收机制
不能手动回收,只能自动回收
垃圾回收的区域只有在堆(Heap)和方法区(Method Area)中存在
JVM在垃圾回收时,并不是对三个区域统一回收,大部分时候都是针对新生代
-
新生代
-
幸存区(from/to),谁空谁是to
-
每次GC都会将Eden中存活的对象移到幸存区:一旦伊甸园区被GC后,就是空的
-
复制算法
-
-
老年区
GC两种类型:轻GC(MinorGC)、重GC(FullGC)
GC题目:
- JVM的内存模型和分区,详细到每个区放什么
- 堆里的分区都有哪些?Eden, from, to,, 老年区,说说他们的特点
- GC的算法有哪些?
- 标记清除法
- 标记压缩
- 复制算法
- 引用计数器
- 轻GC与重GC分别在什么时候发生
15.1 常用算法
15.1.1 引用计数法
对每个对象设置一个计数器,计数器本身也有消耗。当某个对象没用过时,即计数为0,则会被清除
现在JVM中不使用这种方法
15.1.2 复制算法
当幸存from, to区里面都有东西的话,则会使用复制算法,将其中一个区中的内容复制到另外一个区域,保证to区是空的
- 好处:没有内存碎片
- 坏处:浪费了很多内存,多了一半的空间永远是空to,假设对象(100%)成活
复制算法最佳使用场景:对象存活度较低,即新生区
15.1.3 标记清除算法
- 好处:不需要额外的空间
- 坏处:两次扫描,严重浪费时间,会产生内存碎片
15.1.4 标记清除压缩
可以先进行多次标记清除,再进行一次压缩,但是都无尽头
15.1.5 总结
- 内存效率:复制算法>标记清除>标记压缩(时间复杂度)
- 内存整齐度:复制算法=标记压缩>标记清除
- 内存利用率:标记压缩=标记清除>复制算法
思考:
难道没有最优的算法吗
结论:没有最好的算法,但是只有每种场景下最合适的算法----------------->GC:分代收集算法
年轻代:
- 存活率低,使用复制算法
老年代:
- 存活率高,区域大,使用标记清除+标记压缩