JVM入门
JVM入门
- 对JVM的理解,java8虚拟机和之前的变化更新
- 什么是OOM,什么是StackOverFlowError,怎么分析
- JVM的常用调优
- 内存快照如何抓取,怎么分析Dump文件
- 对类加载器的理解
- 推荐:《深入理解java虚拟机》
文章目录
1.JVM的位置
在代码和操作系统之间,将代码解释给操作系统(这也是java跨平台的原因)
2.JVM体系结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5z4oGVS0-1587998498524)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422143504767.png)]
3.类加载器
- 虚拟机自带的加载器
- 启动器加载器
- 扩展类加载器
- 用户自定义
public class Student {
public static void main(String[] args) {
String s = new String();
Student st = new Student();
System.out.println(st.getClass().getClassLoader());
System.out.println(st.getClass().getClassLoader().getParent());
System.out.println(s.getClass().getClassLoader());
//null : java调用不到:底层使用C/C++
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwwmTGio-1587998498526)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422153709906.png)]
4.双亲委派机制
双亲委派机制:
当某个类加载器需要加载某个.class文件时,它首先把这
个任务委托给他的上级类加载器,递归这个操作,如果上级
的类加载器没有加载,自己才会去加载这个类。
1.BOOT(C/C++)-->EXC(JAVA)-->APP(JAVA)-->CustomClassLoader(JAVA)
当类加载器器需要加载一个类时,最先在BOOT加载器中找,
如果找到直接加载,否则再往下找。代码中自定义了一个
String类,但由于BOOT中已有该类,最终加载的是BOOT
中的类,即原本JDK中有的String类,而不会加载自定义
的String ,所以找不到main方法无法运行
双亲委派机制的作用:
- 1、防止重复加载同一个.class。通过委托去向上面问一问,
- 加载过了,就不用再加载一遍。保证数据安全。
- 2、保证核心.class不能被篡改。通过委托方式,不会去篡
- 改核心.clas,即使篡改也不会去加载,即使加载也不会是
- 同一个.class对象了。不同的加载器加载同一个.class也
- 不是同一个Class对象。这样保证了Class
- 执行安全。
5.沙箱安全机制
- 字节码校验器:确保java文件遵循java语言规范,核心类不会经过校验器
- 类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
6.Nativa
凡是带了native关键字的方法,说明超出java的作用范围
需要调用更加底层的方法,程序会进入本地方法栈,调用本地
方法接口(JNI:融合不同的语言,扩展java的使用)
一般只用在需要驱动硬件时才需要调用native方法
7.PC寄存器
每个线程都又一个程序计数器,线程私有,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址),在执行引擎中读取下一跳指令,是一个很小的内存空间,可以忽略不计
8.方法区
方法区被所有线程共享,所有字段个方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,属于共享区间。
静态变量,常量,类信息(构造方法,接口定义),运行的常量池都存在方法区中,但实例变量存在堆内存中,和方法区无关
static, final, Class, 常量池
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tyTYHbF1-1587998498528)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422162504567.png)]
9.栈
栈内存:主管程序的运行,生命周期和线程同步,线程结束,栈内存释放,不存在垃圾回收问题。
放什么:
- 8大基本类型
- 对象引用
- 实例的方法
栈帧:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDdookV5-1587998498528)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422164202406.png)]
当前运行的方法一定在栈顶
10.三种JVM
- SUN公司 :Hotspot
- IBM : J9VM
- BEA : JRockit
11.堆
heap : 一个JVM只有一个堆内存,
堆中一般放些什么:类,方法,常量,变量,所有引用类型的真实对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPnIqGUF-1587998498529)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422170755819.png)]
GC垃圾回收,主要在伊甸区和养老区,有轻GC和重GC两种
幸存区:主要作为新生区到养老区的过度:当一个对象在幸存区之间没有被垃圾回收时,在两个幸存区间徘徊,每徘徊一次年龄+1,当年龄达到预设值时还没有被回收就会进入养老区。
新生区 :
- 伊甸区:所有对象创建的地方
- 幸存区:当伊甸区满,会触发一次轻GC,没有被回收的对象会被移到幸存区。当两者都满,会触发重GC。
老年区:当一个对象在幸存区之间没有被垃圾回收时,在两个幸存区间徘徊,每徘徊一次年龄+1,当年龄达到预设值时还没有被回收就会进入养老区。当老年区满,但还有对象需要进入时,会OOM。
永久区:
这个区域常驻内存中,用于存放JDK自带的Class对象,interface元数据,不存在垃圾回收,关机虚拟机时自动释放
- jdk1.6前:
永久代
,常量池在方法区中 - jdk1.7:
去永久代
,常量池在堆中 - jdk1.8后:
无永久代
,常量池在元空间
12.堆内存调优
OOM:
- 尝试扩大堆内存空间
- 如果1没用,可能是代码问题,如无用的调用和循环
内存快照分析工具:MAT , Jprofiler。
package com.java123.JVM;
import java.util.ArrayList;
import java.util.List;
/**
* -Xms 设置初始化内存分配大小 默认1/64
* -Xmx 设置最大分配内存 默认1/4
* -XX:+HeapDumpOnOutOfMemoryError OOM Dump
* -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
*/
public class Heap {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<byte[]>();
int i = 0;
boolean flag = true;
while(flag){
try{
i++;
list.add(new byte[1024*1024]);
Thread.sleep(40);;
} catch (Throwable e) {
e.printStackTrace();
flag = false;
System.out.printf("Count : " + i);
}
//会出现内存溢出错误(OOM)
}
}
}
13.GC
垃圾回收:作用区一般在方法区和堆区
JVM在进行GC时,并不是对三个区域(新生代,幸存区,老年区)统一回收,大多数时候在新生区。只有重GC才会对全局进行回收
GC问题:
- GC的算法有哪些,标记清除法,标记压缩,复制算法,引用计数器怎么用
- 轻GC和重GC分别在什么时候发生
GC算法:
-
引用计数法:对每一个对象被引用的次数标记,当GC发生时将次数为0的对象回收
-
复制算法:每次GC都会将伊甸区活的对象移到幸存to区,并将from区存活的对象复制到to区,当一个对象经历15此GC任然存活就会进入老年区
-
-XX:MaxTenurinfThreshold = 15; 通过这个参数可以调整进入老年区需要经历的GC次数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQCXfq3K-1587998498531)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422185748938.png)]
-
好处:没有内存碎片
-
坏处:幸存区浪费了一般的内存空间,to区需要保持空,如果出现对象存活较多时,复制的成本也会大大增加
-
-
标记清除算法:
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8Zv9G0p-1587998498532)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422190501704.png)]
- 好处:不需要额外的空间
- 坏处:两次扫描浪费时间,会产生内存碎片
-
标记压缩:在标记清除的基础在进行了几次标记清除后压缩
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lcxVUwie-1587998498533)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422190818786.png)]
- 有效处理了内存碎片的问题,但加大了开销
总结
时间效率:复制>标记清楚>标记压缩
内存整齐度:复制=标记压缩>标记清除
内存利用率:标记压缩=标记清除>复制
没有最优的算法,只有最适合的算法
GC分代算法:
新生代:
- 区域较小,存活率低
- 采用复制算法
老年代:
- 区域大,存活率高
- 采用标记清除+标记压缩混合实现
14.JMM
Java Memory Model
作用:为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HwEb1TPb-1587998498534)(C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200422194413465.png)]
物理机高速缓存和主内存之间的交互有协议,同样的,java内存中线程的工作内存和主内存的交互是由java虚拟机定义了如下的8种操作来完成的,每种操作必须是原子性的;
- lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
- unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
- read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
- load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
- use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
- assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
- store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
执行这8中操作的时候必须遵循如下的规则:
- 不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
- 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
- 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
- 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
- 一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。
- 对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
- 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
- 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
- 在Java中,为了保证原子性,提供了两个高级的字节码指令
monitorenter
和monitorexit
。这两个字节码,在Java中对应的关键字就是synchronized
。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- Java中的
volatile
关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile
来保证多线程操作时变量的可见性。 - Java中的
synchronized
和final
两个关键字也可以实现可见性。
有序性即程序执行的顺序按照代码的先后顺序执行。
- 在Java中,可以使用
synchronized
和volatile
来保证多线程之间操作的有序性。实现方式有所区别:volatile
关键字会禁止指令重排。synchronized
关键字保证同一时刻只允许一条线程操作。