目录
JVM
JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
常见面试题
- 请你谈谈你对JVM的理解?java—class—jvm
- java8虚拟机和之前那的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件
- 谈谈JVM中,类加载器你的认识
一、JVM的概念
1. JVM 的位置
2. JVM 的体系结构
3. 类加载器
作用:加载Class文件,Java运行时编译源码(.java)成字节码(.class),由jre运行。jre由java虚拟机实现.JVM分析字节码,解释并执行
- 加载顺序
(1)虚拟机自带的加载器
(2)启动类(根)加载器
(3)扩展类加载器
(4)应用程序(系统类)加载器 【往上走】
二、双亲委派机制
概念:
当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委托给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载
2.1 JVM中提供了三层的ClassLoader
- 启动类加载器Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader
- 扩展类加载器ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar
- 应用程序加载器AppClassLoader:主要负责加载应用程序的主函数类
- 用户自定义加载器:负责加载用户自定义路径下的包
为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入
三、沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱就是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源的访问
,包括CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样
所有的Java程序运行都可以指定沙箱,可以指定安全策略
3.1 JDK1.0安全模型
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于(sandbox)机制,如图所示JDK1.0安全模型
3.2 JDK1.1安全模型
但如此严格的安全机制也给程序的功能扩展带来了阻碍,比如当用户希望远程代码访问本地系统的文件时,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图JDK1.1安全模型
3.3 JDK1.2安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模式
3.4 JDK1.6安全模型
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有的代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模式(jdk1.6)
3.5 组成沙箱的基本组件
-
字节码校验器(bytecode verifier):
确保Java类文件遵循Java语言规范。这样可以帮助程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类java、javax -
类加载器(class loader):(或类装载器)
其中类加载在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码 //双亲委派机制
- 它守护了被信任的类库边界
- 它将代码归入保护域,确定了代码可以进行哪些操作
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由唯一系列唯一的名称组成,每一个被加载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类加载器维护的,它们互相之间设置不可见
类加载器采用的机制是双亲委派模式
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效
- 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高
- 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
四、Native(重点)
public class TestNative {
public static void main(String[] args) {
new Thread(() ->
{
},"myThread").start();
}
// native: 凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层C语言的库!
// 会进入本地方法栈
// 调用本地方法`本地接口` JNI
// JNI作用: 扩展Java的使用,融合不同的编程语言为Java所用! 最初想融合: C、C++
// Java诞生的时候 C C++ 横行,想要立足,必须要有调用C、C++的程序
// 它在内存区域中,专门开辟了一块标记区域: Native Method Stack,登记 native 方法
// 在最终执行的时候,加载本地方法库中的方法通过JNI
// Java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见
private native void start0();
}
五、PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
六、方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区存着:static final Class模板 常量池
JDK1.8之后,永久代被元空间取代,里面存放类型信息,字段、方法、运行时常量池在本地内存的元空间,而静态变量、字符串常量池在堆空间
七、栈
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧
(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的
栈特点:
- 先进后出 后进先出,每个方法执行,伴随着进栈(入栈、压栈)
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- 对于栈来说不存在垃圾回收问题。栈不需要GC,但是可能存在OOM
队列特点:先进先出(FIFO First Input First Output)
栈存放内容:8大基本类型 + 引用对象 + 实例方法
栈运行原理:栈帧
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收(GC)问题
7.1 虚拟机栈的异常
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个
StackoverflowError
异常 - 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个
OutofMemoryError
异常
7.2 设置栈内存大小
我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform
设置线程堆栈大小(以字节为单位)。附加字母k或k表示KB,m或m表示MB,g或g表示GB。默认值取决于平台
八、三种JVM
-
Sun公司:HotSpot
Java HotSpot(TM) 64-Bit Server VM (build 15.0.2+7-27, mixed mode, sharing)
-
BEA
JRockit
:Oracle JRockit (原来的 Bea JRockit)电脑软件,系列产品是一个全面的Java运行时解决方案组合 -
IMB j9 VM
九、堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的
类加载器读取类文件后,一般会把什么东西放在堆中呢?类,方法,常量,变量… 保存我们所有引用类型的真实对象
堆内存中给还要细分为三个区域
9.1 GC垃圾回收(Garbage Collection)
GC垃圾回收.主要是在伊甸园区
和老年区
假设内存满了,OOM,对内存不够!
java.lang.OutOfMemoryError:Java heap space
public class TestGC {
public static void main(String[] args) {
// 给jvm堆内存调小为8M
// -Xms8m -Xmx8m -XX:+PrintGCDetails
String str = "vinjcent";
while(true){
str += str + new Random().nextInt(123456789) + new Random().nextInt(999999999);
}
}
}
在JDK8以后,永久区存储改了个名字(元空间)
9.2 新生区(伊甸园区)
- 类:诞生和成长的地方,甚至死亡
- 伊甸园,所有的对象都是在
伊甸园
区new出来的 - 幸存者区(0,1)
新生区主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。新生区分成1个Eden 区
和2个幸存区
(命名为幸存0区和幸存1区,三个区域默认在新生区的内存比例是8:1:1)。当对象在堆创建时,将进入新生区的Eden 区
,如果Eden 区满了,会触发一次轻GC(Young GC,复制算法),清空Eden区
垃圾回收器进行垃圾回收时,扫描Eden 区和幸存0区,如果对象(大多数为引用对象)仍然存活,则复制到幸存1区,并将两个幸存区交换身份,幸存0区为幸存1区、幸存1区为幸存0区,如此往复。同时,在扫描幸存区时,如果对象已经经过了几次的扫描仍然存活(默认15次),JVM认为其为一个持久化对象,则将其移到老年区
当新生区都满了,就会触发一次重GC(Full GC,会使整个Java程序暂停,标记整理算法),扫描完毕后,JVM将Eden区和幸存区清空。这么做主要是为了减少内存碎片的产生。当新生区和老年区都满了,就会OOM
真理:经研究,99%的对象都是临时对象
9.3 老年区
这个区域常驻内存中,用来存放 JDK 自身携带的 Class 对象 Interface 元数据,存储的是Java运行时的一些环境或信息,这个区域不存在垃圾回收
,关闭VM虚拟机就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多应用,大量动态生成的反射类。不断地被加载,知道内存满,就会出现OOM
- jdk1.6之前:永久代,常量池是在方法区中
- jdk1.7:永久代,但是已经慢慢的退化了,
去永久代
,常量池在堆中 - jdk1.8之后:无永久代,常量池在元空间
9.4 永久区
元空间:逻辑上存在,物理上不存在
十、堆内存调优
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
// OOM:
// 1. 尝试扩大堆内存,看结果 如果扩大了还报错,那就是代码问题
// 2. 分析内存,看一下哪个地方出现了问题(专业工具)
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails
// 305664K + 699392K = 年轻代 + 永久代 = 1,005,056K = 981.5M
}
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
在一个项目中,突然出现了OOM故障,如何排除?
- 能够看到代码第几行出错:内存快照分析工具, MAT, Jprofiler
- Debug
MAT, Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象
测试
- 安装插件
- 安装Java剖析工具JProfiler
- 同样在VM Option中将堆的内存大小设置一下,以免太大影响电脑运行
测试代码
// Dump
public class TestJProfiler {
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
// Xms 设置初始化内存分配大小,默认 1/64
// Xmx 设置最大分配内存,默认 1/4
// -XX:+PrintGCDetails //打印GC垃圾回收信息
// -XX:+HeapDumpOnOutOfMemoryError //oom错误 DUMP
byte[] array = new byte[1*1024*1024]; // 设置大小为1M
public static void main(String[] args) {
ArrayList<TestJProfiler> list = new ArrayList<>();
int count = 0;
try{
while (true){
list.add(new TestJProfiler());
count += 1;
}
}catch (Error e){
System.out.println("count===>"+count);
}
右键在文件夹中打开(或双击这个文件,前提是已经安装好了这个JProfiler工具)
JProfiler是一个商业授权的Java剖析工具,由EJ技术有限公司,针对的Java EE和Java SE应用程序开发的
十一、GC
作用区域
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生区
- 新生区
- 幸存区(from to)
- 老年区
GC的两种类型:轻GC(普通的GC) 重GC(全局GC)—>释放内存
GC 题目常见面试题
- JVM的内存模型和分区~写详细到每个区放什么
- 堆里面的分区有哪些? Eden、from、to老年区,说说他们的特点
- GC的算法有哪些? 标记清除法、标记压缩、复制算法,引用计数器?怎么用
- 轻GC和重GC分别在什么时候发生
11.1 引用计数法
11.2 复制算法
好处:没有内存的碎片
坏处:浪费了内存空间:多了一半空间永远是to。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候:新生区
11.3 标记清除法
优点:不需要额外的空间
缺点:两次扫描,严重浪费时间,会产生内存碎片
11.4 标记压缩
总结
- 内存效率:复制算法 > 标记清楚算法 > 标记压缩算法(时间复杂度)
- 内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
- 内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
没有最好的算法,但是只有最合适的—>GC:分代收集算法
-
年轻代
存活率低
复制算法 -
老年代
区域大:存活率高
使用标记清除(内存碎片不是太多) + 标记压缩混合实现
十二、JMM:Java Memory Model
什么是 JMM
【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile
或synchronized
明确请求了某些可见性的保证
JMM 是干什么的
作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题:volatile
如何学习
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 规则来进行分析
volatile 在 java 语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序
volatile关键字保证可见性、有序性。但不保证原子性