JVM
1.JVM的位置
2.JVM体系结构
3.类加载器
作用
类加载器: 加载class文件 new Student();
1.虚拟器自带的加载器
2.启动类(根)加载器(rt.jar):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
3.扩展类加载器(jre\lib\ext):主要负责加载jre/lib/ext目录下的一些扩展的jar。
4.应用程序加载器(AppClassLoader):主要负责加载应用程序的主函数类
测试
package com.kuang;
public class Student {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<Student> studentClass = Student.class;
Student student = studentClass.newInstance();
Class<? extends Student> aClass = student.getClass();
ClassLoader classLoader = aClass.getClassLoader();
System.out.println(classLoader); //AppClassLoader
ClassLoader parent = classLoader.getParent(); // 扩展类加载器 ExtClassLoader jre\lib\ext
System.out.println(parent);
ClassLoader parent1 = parent.getParent(); // null java获取不到 rt.jar
System.out.println(parent1);
}
}
双亲委派机制(重要)
分析:
当一个Student.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
双亲委派机制的工作原理:
-
如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行;
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;
-
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制;
-
父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常;
-
即从应用程序加载器一层一层向上询问,是否已经加载过,若未加载,再从顶层向下加载。即根加载器内找此类,若已加载则结束,若无此类,再在扩展类加载器中找,若存在则结束。若不存在,则在应用程序加载器中找,若存在则结束,若不存在返回null。
沙箱安全机制(了解)
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
java中的安全模型:
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示
JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示
JDK1.1安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
JDK1.2安全模型
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 :
最新的安全模型
以上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。
组成沙箱的基本组件:
字节码校验器
(bytecode verifier):确保Java类文件遵循Java语言规范(格式)。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类(核心类已经测试过了,不需要再校验)。类装载器
(class loader):其中类装载器在3个方面对Java沙箱起作用- 它防止恶意代码去干涉善意的代码(核心类中的代码);
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
存取控制器
(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。安全管理器
(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。安全软件包
(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:- 安全提供者
- 消息摘要
- 数字签名 keytools
- 加密
- 鉴别
4.Native(重要)
package com.kuang;
public class Demo {
public static void main(String[] args) {
new Thread(()->{
}).start();
}
// native:凡是带了native 关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
// 会先进入本地方法栈
// 调用本地方法接口 JNI
// JNI的作用:扩展java的使用,融合不同的编程语言为java所用。
// java诞生的时候 c,c++ 横行,想要立足,必须要有调用c,c++的程序~
// 于是他在内存中专门划分了一块标记区域: Native Method Stack ,用来登记native方法
// 在最终执行的时候,加载本地方法库中的方法通过JNI
// java程序驱动打印机,管理系统,掌握即可,很少用!
private native void start0();
}
总结
Java在内存区域中专门开辟了一块标记区域——本地方法栈,用来登记native方法,凡是带了native关键字的,会进入到本地方法栈中,调用本地方法接口(JNI),在最终执行的时候,加载本地方法库中的方法通过JNI。
它的具体做法是在本地方法栈中登记 native 方法,在执行引擎执行的时候加载本地方法库。
5.PC寄存器
程序计数器:Program Counter Register
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
因为程序计数器的存在,线程从标号1排到8顺序才不会乱.
6.方法区
Method Area 方法区
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊的方法,如构造方法,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法,接口定义)、运行时的常量区也在方法区中,但是 实例变量存在堆内存中,和方法区无关。
方法区: static、final、Class,常量池
7.栈(Stack)
栈是一种数据结构
程序= 数据结构 + 算法
栈:先进后出、后进先出、相当于一个桶
队列:先进先出(FIFO:first input first output)、后进后出、相当于一个管子
为什么main方法先执行,最后结束?
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存就会释放了!对于栈来说, 不存在垃圾回收问题!
一旦线程结束了,栈就Over了!
栈: 8大基本类型 + 对象引用 + 实例的方法
栈的运行原理:栈帧
如果栈满了:StackOverFlowError
内存中对象实例化的过程
我们知道,java中在类的实例化的过程中,内存中会使用这三个区域,栈区,堆区和方法区。
在程序执行过程中,首先,类中的成员变量和方法体进入到方法区。
然后main()函数方法体进入到栈区,这个过程称为压栈,定义了一个用于指向Student类的实例
接下来在堆内存中创建一个实例 Student s = new Student()的实例。然后将 成员变量和成员方法放在new实例中。将成员方法放在new实例的过程就是取成员方法的地址值!
接下来,s.name = “张三”;s.age = 18;
现在栈区中找到s,然后根据地址值找到堆中存放具体的值进行赋值操作!
然后调用方法体void speak(),在调用方法体的过程中就是先去栈中找到对象s,后根据引用地址找到堆中存放这个对象,再根据这个对象的方法的地址值找到方法区中具体的方法进行执行!
在方法体void speak();被调用完成后,就会立刻马上从栈内弹出(方法出栈)
最后,在main()函数完成后,main函数也出栈
以上便是完整的java类实例化时内存中所发生的变化。
8.三种JVM
- Sun HotSpot
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
- BEM
JRockit
- IBM
Technology for Java Virtual Machine
J9 VM
我们所用的 : HotSpot
9.堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的 !
类加载器读取了类文件后,一般会把什么东西放在堆中呢? 类,方法引用,运行时的常量,变量~,保存我们所有引用类型的真实对象;
堆内存中还要细分为3个区域:
- 新生区(伊甸园区) Young/New
- 养老区 old
- 永久区 Perm
GC垃圾回收 主要是在 新生区 + 养老区
假设内存满了,OOM,堆内存不够!
在JDK8 以后,永久存储区改了个名字,叫元空间
新生区
- 类 : 诞生 和 成长的地方,甚至死亡;
- 伊甸园区,所有的对象都是在伊甸园区 new 出来的!
- 幸存者区(1,2),伊甸园区幸存下来就会转入这个区~
真理 : 经过研究,99%的对象都是临时对象!
永久存储区
这个区域是常驻内存的,用来存放JDK自身携带的class对象,Interface元数据,存储的时java运行时的一些环境或者类信息~,这个区域不存在垃圾回收!关闭虚拟机,就会释放这个区域的内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署了大多的应用,大量动态生成的反射类,不断被加载,直到内存满了就会出现OOM。
- JDK 1.6 之前:永久代,常量池是在方法区中
- JDK 1.7:永久代,但是慢慢的退化了,
去永久代
,常量池在堆中! - JDK 1.8 之后:无永久代,常量池在元空间
元空间逻辑上存在,物理上不存在!
package com.kuang;
public class Test {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory(); //字节
//返回JVM的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory="+maxMemory+"字节\t"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("totalMemory="+totalMemory+"字节\t"+(totalMemory/(double)1024/1024)+"MB");
}
// 结论:默认情况下,分配的总内存 是 电脑内存的1/4 , 而初始化的内存为总内存 1/64
// 15.9g
// ~1/4
// ~1/64
// -Xms1024m -Xmx1024m -XX:+PrintGCDetails
// OOM现象出现 : 堆内存满了,解决:
// 1.先尝试扩大堆内存,看结果
// 2.分析内存看一下哪个地方出现了问题(专业工具)
}
修改虚拟机的参数 :
查看结果:
计算: (305664+699392)/ 1024 = 981.5MB;
由此可见, 元空间逻辑上存在,物理上不存在!
出现了OOM之后,该怎么办?
使用JPofiler工具分析OOM原因
package com.kuang;
import java.util.ArrayList;
//Dump
public class TestOOM {
byte[] array = new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList<TestOOM> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new TestOOM()); //问题所在
count++;
}
}catch (Error e){
System.out.println("count"+count);
}
//Throwable
//Exception
//Error
}
}
查看结果:
出现了OOM,于是我们dump它的内存快照 -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
找到dump下来的内存快照并打开!
设置的参数
-Xms //设置初始化内存大小,默认1/16
-Xmx //设置最大分配内存,默认1/4
-XX:+PrintGCDetails : //打印GC垃圾回收信息
-XX:+HeapDumpOnOutOfMemoryError : //OOM dump内存快照
10.GC垃圾回收机制
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是在新生代~
- 新生代
- 幸存from区 幸存to区
- 老年区
GC两种类 : 轻GC (普通的GC), 重GC(全局GC)
GC面试题目:
- JVM的内存模型和分区~ 详细到每个区能做什么?
- 堆里面分区由哪些? Eden,from,to,老年区,说说他们的特点!
- GC的算法由哪些? 标记清除法,标记压缩,复制算法,引用计数法,怎么 用的?
- 轻GC和重GC分别在什么时候发生?
1、引用计数法(用的较少)
清除使用较少的!
2、复制算法(Eden)
- 好处:没有内存碎片~
- 坏处:浪费了内存空间~: 多了一半空间永远是空的(to区)。假设对象100%存活(极端情况)
复制算法最佳使用场景: 对象存活率较低的时候,在伊甸园区的时候~
3、标记清除算法
缺点:两次扫描严重浪费时间,会产生内存碎片~
优点:不需要额外的空间~
再优化!!!
标记清除压缩
使用场景:
先标记清除几次,再压缩~
总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记清除算法 = 标记压缩算法 > 复制算法
思考一个问题: 难道没有最优算法吗?
答案: 没有,没有最好的算法,只有最合适的算法 —> GC : 分代收集算法
分代收集算法
年轻代 : 存活率低,使用复制算法!
老年代:区域大,存活率高,使用标记清除算法(内存碎片不是很多) + 标记压缩(内存碎片达到一定量) 混合实现
11.JMM
1、什么是JMM?
JMM(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。
2、它干嘛的? 官方 + 博客 + 视频
作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)
JMM定义了工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享变量可见性这个问题: volatile
3、它该如何学习?
JMM : 抽象的一个概念
JMM对这八种指令的使用,制定了如下规则:
-
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
说明:笔记来自于上课随手记录的
视频教程来自于狂神说java 初识JVM:https://www.bilibili.com/video/BV1iJ411d7jS
欢迎大家前去学习:有条件请给狂神充个电,上个币!!!