一、什么是JVM?
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
类加载器/双亲委派
1、JVM虚拟/类加载/类加载器
2、 双亲委派机制
定义
APP->EXT(ext包)->Boot(rt.jar)
- 1、类加载器收到类加载请求
- 2、将这个请求向上委托给父类加载器去完成,一直向上委派,指导启动类加载器(boot)
- 3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前加载器,否则跑出异常,通知子加载器进行加载
- 4、重复步骤3
代码
Car car1 = new Car();
System.out.println(car1.getClass().getClassLoader());
System.out.println(car1.getClass().getClassLoader().getParent());
System.out.println(car1.getClass().getClassLoader().getParent().getParent());
输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@5cad8086
null
好处
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
3、sandbox安全机制
定义
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
jdk1.6后
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6)
域 (Domain)
字节码校验器
字节码校验器:保证java类文件遵照java语言规范,实现内存宝库,但不是所有类都经过字节校验,比如核心类
类加载器
1、防止恶意代码干涉善意代码(双亲委派)
2、守护被信任的类库边界(不能直接调用c库)
3、将代码归入保护域,确定代码可以进行哪些操作
native关键词
本地方法接口(JNI)
native:凡是带了native关键字带方法,说明java 带作用域达不到,都会进入本地方法栈,调用JNI接口,扩张java使用(EXT).
JNI:扩张java的使用,融合不同的编程语言为java使用,最初C\C++
new Robot()类
PC寄存器
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码。由执行引擎读取下一条指令。
- 1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
- 2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
- 4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 5.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收
- 程序计数器
每一个线程都有一个程序计数器,是线程私有的,就是一个指证,指向方法区中的方法字节码(用来存储指向对象一条指令的地址,也就是执行的指令代码),在执行引擎读取下一个指令
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
- 方法区
被所有线程共享的,所有字段跟方法节码,以及一些特殊的方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法信息都保存在该区域,此区域属于共享区间
静态变量
常量
类信息(构造方法、接口定义)
运行时常量池
实例变量存储在堆内存中,和方法区无关
存储
static
final
Class
常量池
PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码。由执行引擎读取下一条指令。
- 1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
- 2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
- 3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
- 4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 5.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收
程序计数器
每一个线程都有一个程序计数器,是线程私有的,就是一个指证,指向方法区中的方法字节码(用来存储指向对象一条指令的地址,也就是执行的指令代码),在执行引擎读取下一个指令
程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
方法区
被所有线程共享的,所有字段跟方法节码,以及一些特殊的方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法信息都保存在该区域,此区域属于共享区间
静态变量
常量
类信息(构造方法、接口定义)
运行时常量池
实例变量存储在堆内存中,和方法区无关
存储
static
final
Class
常量池
栈(STACK)
栈
main方法执行
栈溢出(StackOverflowError)
无限压栈
递归
程序正在运行的方法,一定在栈的顶部
栈内存
线程级别。主管程序的运行,生命周期和线程同步,线程结束,栈内存释放,栈内存释放完成,程序结束。对于栈来说,不存在垃圾回收问题
JVM的栈内存
栈内容
8大基本数据类型
对象的引用
实例方法
栈堆引用
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配
本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
堆(Heap)
堆
堆内存
heap:一个jvm只有一个堆内存,堆内存的大小可以调节。类加载器读取类文件后,一般把“类、方法、常量、变量”保存引用的正式对象。
类
方法
常量
变量
区域
新生区(伊甸区)new
养老区old
永久区perm
经过研究,99%的对象都是临时对象,如果新生区、养老区满了,内存异常,oom异常。
jdk1.6:永久代,常量池在方法区
jdk1.7:永久代,但是慢慢退化,去永久代,常量池在堆中,
jdk1.8后:无永久代,常量池在元空间。
永久区:常驻内存,用来放jdk自身携带代class对象,Interface元数据,存储的是java 运行时一些环境或类信息,这个区域不存在在垃圾回收中。关闭虚拟机时候,释放.(逻辑上存在,物理上不存在)
OOM错误
1、尝试扩大堆内存看结果
2、分析内存,看下那个地方出现了问题(-Xms1024m -Xmx1024m -XX:+PrintGCDetails)
[GC (Allocation Failure) [PSYoungGen: 987K->224K(1024K)] 1075K->652K(1536K), 0.0012821 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 224K->0K(1024K)] [ParOldGen: 428K->407K(512K)] 652K->407K(1536K), [Metaspace: 3016K->3016K(1056768K)], 0.0043404 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
3、线上问题定位
Mat.Jprofiler
分析Dump内存文件-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError
GC
轻GC
伊甸区
幸存0区
幸存1区
重GC
老年区
回收算法
标记清除法
1、扫描空间中活着的对象,进行标记
2、对没有标记的对象进行清除
好处:不需要额外空间
缺点:两次扫描,浪费时间,会产生内存碎片
标记压缩
标记清除上,再次扫描,向一边移动存活的对象。标记清除压缩算法
好处:防止内存碎片
缺点:有加了一次扫描及移动
复制算法
FROM_TO(幸存0区幸存1区相互交互,复制,复制算法主要用新生代),默认15次轻GC删除不了的对象,进行一次重GC
好处:没有内存碎片
坏处:浪费一半空间(幸存区)为空
最佳使用场景:新生区
引用计数法
程序计数器
程序计数器(java不采用,对象管理消费较大,python采用该方式)
JVM调优
分代收集算法
年轻代
存活率低
复制算法
老年代
存活率高
标记清除算法(内存碎片不是太多)+标记压缩算法混合实现
JMM(java Memory Model)
定义:
Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM
方法
主内存
主内存是所有的线程所共享的,主要包括本地方法区和堆。
每个线程都有一个工作内存不是共享的,工作内存中主要包括两个部分:
1:一个是属于该线程私有的栈;
2:对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。
2.每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
3.线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成,即:线程、主内存、工作内存。
特征
原子性:
一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
可见性:
一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。
有序性:
对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
volilate
规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来防止指令重排序。
线程对内存间交互
Lock(锁定):作用于主内存中的变量,把一个变量标识为一条线程独占的状态。
Read(读取):作用于主内存中的变量,把一个变量的值从主内存传输到线程的工作内存中。
Load(加载):作用于工作内存中的变量,把read操作从主内存中得到的变量的值放入工作内存的变量副本中。
Use(使用):作用于工作内存中的变量,把工作内存中一个变量的值传递给执行引擎。
Assign(赋值):作用于工作内存中的变量,把一个从执行引擎接收到的值赋值给工作内存中的变量。
Store(存储):作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中。
Write(写入):作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
Unlock(解锁):作用于主内存中的变量,把一个处于锁定状态的变量释放出来,之后可被其它线程锁定。
1.1.1 程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
1.1.2 JAVA 虚拟机栈
线程私有,生命周期和线程一致。描述的是 JAVA 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(STACK FRAME)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
1.1.3 本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
1.1.4 JAVA 堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(THREAD LOCAL ALLOCATION BUFFER, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
1.1.5 方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.1.6 运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
1.1.7 直接内存
非虚拟机运行时数据区的部分。在 JDK 1.4 中新加入 NIO (NEW INPUT/OUTPUT) 类,引入了一种基于通道(CHANNEL)和缓存(BUFFER)的 I/O 方式,它可以使用 NATIVE 函数库直接分配堆外内存,然后通过一个存储在 JAVA 堆中的 DIRECTBYTEBUFFER 对象作为这块内存的引用进行操作。可以避免在 JAVA 堆和 NATIVE 堆中来回的数据耗时操作。
OUTOFMEMORYERROR:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。