视频链接:https://www.bilibili.com/video/BV1iJ411d7jS
JVM探究
1、 请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
2、 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
3、 JVM常用的调优参数有哪些?(堆内存调优)
4、 内存快照如何抓取?怎么分析Dump文件?知道么?
5、谈谈JVM中,类加载器你的认识?(rt-jar/ext/application)
1. JVM 的位置
JVM在操作系统(Window,Linux,Mac)的上方,它是被包含在JRE里面的,而其他一些运行系统跟JVM是在同一个层,JVM层上方全都是运行的Java程序,而在操作系统下方还有一层硬件体系(Inter,Spac……)
2. JVM体系结构
.java–>Class File---->类装(加)载器Class Loader---->运行时数据区(Runtime Data Area),里面包括方法区(Method Area),Java栈(Stack Area),本地方法栈(Native Method Stack/Area),堆(Heap Area),程序计数器等---->本地方法库(Native Method Library),执行引擎(Execution Engine)---->本地方法接口(Native Method Interface–JNI)
Java栈(Stack),本地方法栈(Native Method Stack),程序计数器这三个里面没有垃圾,如果有垃圾的话程序要崩,所以不会有垃圾回收
调优:JVM99%都在调堆内存,而方法区算是特殊的堆内存,所以1%在调方法区。
3. 类加载器
作用(核心标识):加载Class文件。new Student();new完后变成具体的实例,在栈里面引用。
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
四类加载器
(1)虚拟机自带的加载器
(2)启动类(根)加载器(BootstrapClassLoader):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
(3)扩展类加载器(ExtensionsClassLoader):主要负责加载jre/lib/ext目录下的一些扩展的jar。
(4)应用(系统)程序加载器(AppClassLoader/ SystemClassLoader):主要负责加载应用程序的主函数类
类加载过程:加载链接(1、验证(文件格式、元数据、字节码、符号引用验证)2、准备3、解析)初始化
4. 双亲委派机制
工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
简单说分四步走
(1)检查是否已经被类加载器加载过(类加载器收到类加载请求)
(2)存在父加载器,递归的交由父加载器(将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器)
(3)直到最上面的Bootstrap类加载器(启动类加载器检查是否能够加载当前这个类,能加载就结束就使用当前加载器,否则抛出异常,通知子加载器进行加载)
(4)重复步骤(3)
参考网上的图,结合理解
双亲委派机制是为了保证安全的,防止修改rt.jar里面的资源。
优势:避免类的重复加载和保证安全(防止核心API库被随意篡改)。
5. 沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox)。
沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问。系统资源包括:CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制可以不一样
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在Java中执行程序分为本地代码和远程代码两种,本地代码默认视为可信任的,远程代码被看做是不受信任的。对于授信的本地代码,可以访问一切本地资源,对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。
JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。
JDK1.1安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到JVM中权限不同的运行空间,来实现差异化的代码执行权限控制
JDK1.2安全模型
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有的代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问,虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在不同域中的类文件就具有了当前域的全部权限
JDK1.6
组成沙箱的基本组件:
(1) 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范,这样可以帮助Java实现内存保护(假如字节码都没有通过编译,那整个系统都会崩)。但并不是所有的类文件都会经过字节码校验,如核心类。
新建test.java文件
写代码:
然后在目录栏上cmd打开运行javac .\test.java
(2) 类装卸器(class loader):其中类装卸器在3个方面对Java沙箱起作用
a) 防止恶意代码去干涉善意的代码 // 双亲委派机制
b) 守护了被信任的类库边界
c) 将代码归入保护域,确定了代码可以进行哪些操作 //沙箱
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每个被装载的类将有一个名字,这个命名空间是由java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
a) 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
b) 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效
(3) 存储控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
(4) 安全管理器(security manager):是核心AOI和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高
(5) 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括
a) 安全提供者
b) 消息摘要
c) 数字签名 //keytools 用https跑
d) 加密
e) 鉴别
6. Native
编写多线程类:
package com.changan.testnative;
public class NativeTest {
public static void main(String[] args) {
//点start进源码看
new Thread(()->{},"changan thread").start();
}
}
凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库
JNI:Java Native Interface(Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈,其他的就是java栈
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方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies(本地库)
7. PC寄存器
程序计数器:Program Counter Pegister
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,比如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间:
静态变量(final、static)、常量、类信息(构造方法、接口定义)、运行时的常量池存在的方法区中,但是实例变量存在堆内存中,和方法区无关。
方便理解的话:
package com.changan.Fruit;
public class ChangAn {
private int g;
private String name="changan";
public static void main(String[] args) {
ChangAn changAn = new ChangAn();
changAn.g=1;
changAn.name="西藏";
}
}
上面的name如果不赋值的话,会直接请求下面常量池里面的name值,如果赋值就直接从这个name里面替换
changan:常量池。ChangAn.Class:类
8. 栈:数据结构
程序 = 数据结构+算法
栈:先进后出、后进先出。如桶。
对列(FIFO:First Input First Output):先进先出。如排队
喝多了吐就是栈,吃多了拉就是对列。
为什么main()先执行,后结束。因为先调用了main方法,然后栈最下面就有了main方法,又调用一些其他的方法的话,这些其他的方法就在main方法的上方,就像桶。执行完毕之后栈会按照先进后出的方式弹出那些方法。
栈:栈内存,主管程序的运行,生命周期和线程同步。线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题。一旦线程结束,栈就挂掉了。
栈放的东西:8大基本类型+对象引用+实例的方法
运行原理:栈帧。栈有底栈和顶栈,每个运行的方法进去都有一个父帧和子帧,下一个子帧指向上一个父帧,上一个父帧指向下一个方法体。每个一正在运行的程序都在栈顶。
栈满了,即溢出了。StackNativeError
栈+堆+方法区:交互关系
9. 三种JVM
我们学的是Sun公司的Hotspot。其中还有Bea JRockit和IBM J9 VM
10. 堆
堆(Heap),一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中:类,方法,常量,变量保存我们所有引用类型的真实对象。
假设内存满了,OOM,堆内存不够。java.lang.OutOfMemoryError: Java heap space
模拟代码
分析:jdk1.8以前的
GC垃圾回收,主要是在伊甸园区和养老区。
在jdk1.8以后,永久存储区改名为元空间。
11. 新生区、老年区,永久区
新生区:类,诞生和成长甚至死亡的地方。分为伊甸园区和幸存者区。
伊甸园区:所有的对象都在伊甸园区new出来的
幸存者区(0区和1区)
真理:经过(GC)研究,99%的对象都是临时对象。
老年区:年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
永久区:这个区域是常驻内存的,用来存放JDK自身携带的Class对象,Interface(接口)元数据,存储的是Java运行时的一些环境或者类信息。这个区域不存在垃圾回收。关闭JVM虚拟机就会释放这个区域的内存。
一个启动类加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类,这些东西不断地被加载,直到内存被装满,就会出现OOM。
jdk1.6之前:永久代,常量池在方法区
Jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中
Jdk1.8之后:无永久代,常量池在元空间。
伊甸园区中创建创建很多的对象,全部在方法区中调用。方法区也有人称是永久区,用实现接口的方法把那些对象在方法区里面永久保存。常量池也在方法区中,属于方法区的一部分。
元空间:逻辑上存在,物理上不存在
整个图组成了堆
12. 堆内存调优
建一个class类
package com.changan.Fruit;
public class Dui {
public static void main(String[] args) {
//返回虚拟机视图使用的最大内存
//字节转化成兆(MB)10241*1024
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
//转化成double输出
//max=3773300736字节 3598.5MB
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
//total=3773300736字节 243.5MB
//16G 约等于4/1 64/1--》4到64差16倍所以,将243.5*16=3896。但是中间会有损失
//所以最终的最大内存为3598.5MB
System.out.println("total="+max+"字节\t"+(total/(double)1024/1024)+"MB");
/*默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存是:1/64*/
}
调优:
设置初始化和最大内存都为1024,然后打印出一些信息。
输入这一串:-Xms1024m -Xmx1024m -XX:+PrintGCDetails 注意:这些字体一个都不能写错,一旦写错则运行报错。
-Xms:设置初始化内存分配大小,默认1/64
-Xmx:设置最大分配内存,默认1/4
-XX:+PrintGCDetails:打印GC垃圾回收信息
解决OOM(Out Of Menmory Error) 问题:
1、尝试扩大堆内存看结果
2、(要是扩大内存还是报错)分析内存,看一下哪个地方出现了问题
点击apply然后点击OK。
输出:
上面的伊甸园区(PSYoungGen)+老年区(ParOldGen)=总内存(total)。305664k + 699392k = 1,005,056k ,再除以1024 = 981.5M
元空间是在堆里面的,但是物理上不存在。
然后如果想看一下堆内存溢出的过程,也就是伊甸园区跟老年区的内存都满了,轻GC和重GC都跑不动了,就会报内存溢出。
在这个类中
package com.changan.Fruit;
import java.util.Random;
public class DuiYiChu {
public static void main(String[] args) {
String str = "changan";
while (true){
str += str + new Random().nextInt(999999999)+
new Random(999999999).nextInt();
}
}
}
输入:-Xms8m -Xmx8m -XX:+PrintGCDetails
输出:
一个项目中,突然出现了OOM故障,该如何排除—》研究为什么出错
1)能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
2)Debug,一行行分析代码
MAT,Jprofiler作用
1.分析Dump内存文件,快速定位内存泄漏
2.获得堆中的数据
3.获得大的对象
4. ……
idea安装Jprofiler插件
JProfiler是一个全功能的Java剖析工具(profiler),专用于分析J2SE和J2EE应用程序。JProfiler是一个商业授权的 Java剖析工具,由EJ技术有限公司,针对Java EE和Java SE应用程序开发的。它允许两个内存剖面评估内存使用情况和动态分配泄漏和CPU剖析
JProfiler是由ej-technologies GmbH公司开发的一款性能瓶颈分析工具(该公司还开发部署工具)
File–>settings—>Plugins–>搜索
点击安装
点击重启,安装之后一定重启 让它生效
重启完成后还得安装一个客户端的 ,链接:
https://www.ej-technologies.com/download/jprofiler/version_92
下载完成就启动运行:
next—>next
next
之后就一路next即可,其他的不用动。
下面这些注册码是在网上搜索出来的,随便一个注册码应该都可以。
jprofiler 9.2 注册码
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
L-Larry_Lau@163.com#7009-14frku31ynzpfr#20176
把该填的都填写之后,点击next–>next–>next
然后把运行的勾去掉,完了点击finish
然后再次点击settings,找到Tools–>JProfioler–>找到安装的bin目录下的.exe文件
测试OOM
新建一个Class
package com.changan.Fruit;
import java.util.ArrayList;
public class YiChuOOM {
// 1M
byte[] array = new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList<YiChuOOM> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new YiChuOOM());
count = count +1;
}
}catch (Exception e){
System.out.println("count:"+count);
e.printStackTrace();
}
}
}
输出:
然后设置下面这个内容:
内容的意思:假设报出OutOfMemoryError这个异常就把文件给Dump下来
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError:打印OOM Dump
运行,然后以文件形式打开
输出:
可以直接双击打开
遇到OOM问题的排错方法,首先
然后点击Thread Dump
13. GC:垃圾回收
JVM进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是在新生代。
GC两种类:轻GC(普通的GC),重GC(全局GC)
常用算法:怎么用?
1.标记清除算法:当堆中的有效空间被耗尽时,JVM就会停止整个程序(也被称为stop the world),然后开始两项工作.一是:标记, 二是:清除
优点:不需要额外的空间
缺点:两次扫描,严重浪费时间,会产生内存碎片。(hash)
链接:https://blog.csdn.net/weixin_37335761/article/details/109437859
2.标记压缩:
标记清除压缩:优化,先清除5次再进行一次压缩
链接:https://blog.csdn.net/xinlingmen/article/details/107656286
3.复制算法:
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另外一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
好处:没有内存的碎片
坏处:浪费了内存空间,多了一半空间永远是空to。假设对象100%活(极端情况)
最佳使用场景:对象存货度较低的情况(新生区)
链接:https://www.cnblogs.com/zuoxiaolong/p/jvm5.html
4.引用计数法:用的少,主要是假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,那说明对象A没有引用了,可以被回收。
JVM的内存模型和分区?详细到每个区放什么?
链接:https://www.cnblogs.com/infinity-zhang/p/13378598.html
堆里面的分区有哪些?
Eden,from,to,老年区,它们的特点?
链接:https://blog.csdn.net/a362813870/article/details/77480524
轻GC和重GC分别在什么时候发生?
轻GC绝大部分在新生区的时候发生,重GC在老年区的时候发生。
14. JMM
什么是JMM?Java内存模型(Java Memory Model, JMM)
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型
Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。
作用:
1.保证线程间变量的可见性。
2.禁止CPU进行指令重排序。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
可见性
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。
除了volatile关键字之外,final和synchronized也能实现可见性。
JMM对8种内存交互操作制定的规则:
• 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
• 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
• 不允许线程将没有assign的数据从工作内存同步到主内存。
• 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
• 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
• 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
• 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
• 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
详细了解链接:https://www.sohu.com/a/420276955_612370
15. 总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
最优的算法?
没有最好的算法,只有最合适的算法。GC称为分代手机算法
年轻代:存活率低。复制算法
老年代:区域大:存活率高。标记清除(内存碎片不是太多的情况下可以一直使用标记清除算法) + 标记压缩混合实现