JVM
什么是JVM?
JVM是Java虚拟机,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
JVM的位置
JVM运行在操作系统之上,与其他应用软件相同层
JVM的体系结构
类加载器
作用:加载Class文件
1.虚拟机自带的加载器
2.启动类(根)加载器(C++实现)
3.扩展类加载器
4.应用程序加载器
双亲委派机制:安全
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
APP–>EXC–>BOOT
例子:java.lang 下的String
沙箱安全机制
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
java执行程序分为本地代码和远程代码,一开始,java将远程代码视为不可信的,授信的是本地代码,可以访问一切资源,后来增加了安全策略(java1.1)来允许用户指定代码访问本地资源。再后来改进了安全策略,增加了代码签名(java1.2)不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。
最新的安全机制实现则是引入了域的概念,虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限。
沙箱基本组件:
字节码校验器:确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。
**类装载器(双亲委派):**类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
Native
带了Native关键字的,说明java的作用范围达不到了,需要回去调用底层C语言的库
进入本地方法栈,调用本地方法本地接口 JNI (Java Native Interface)
JNI的作用:扩展java 的使用,融合不同的编程语言为java所用
它在内存区域中专门开辟了一块标记区域:Native Method Stack ,登记Native 方法,在最终执行的时候,通过JNI加载本地方法库中的方法
PC寄存器
程序计数器:Program Counter Register
线程是私有的
占用较小的内存空间
它的作用可以看作是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
方法区Method Area
所有的线程共享
静态变量static、常量final、类信息class模板(构造方法、接口定义)、即时编译器编译后的代码等数据存在方法区中;
实例变量存在堆内存中,和方法区无关;
运行时的常量池是方法区的一部分;
运行时的常量池存放编译期生成的各种字面量和符号引用。
栈
Java虚拟机栈
用于描述Java方法执行时的内存模型;
主管程序的运行;
与生命周期和线程同步;
线程结束,栈内存就释放,不存在垃圾回收问题。
每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表(形参也是局部变量)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
本地方法栈
作用与Java虚拟机栈类似,用于执行使用到的Native方法,不同的虚拟机有不同的实现方式。
栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
三种JVM
- Sun – HotSpot
- BEA – JRockit
- IBM – J9 VM
堆
Heap 一个JVM只有一个堆内存,堆内存的大小可以调节
所有线程共享
主要是存放对象实例和数组。
GC管理的主要区域
内存模型: 新生代+老生代
GC垃圾回收,主要是在新生区(轻GC)和养老区(重GC)
内存满了OOM
方法区、永久代(PermGen)、元空间的(Metaspace)三者的关系:
方法区 是 JVM 的规范,所有虚拟机 必须遵守的。
常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虚拟机有方法区 ,但是就没有 PermGen space。
PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。
Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机 对 方法区 的新的实现。
运行时常量池:
- 在 JDK6 ,抛出永久代(PermGen space)异常,说明
运行时常量池
存在于 方法区; - 在 JDK7、JDK8 抛出堆(Java heap space)异常,说明
运行时常量池
此时在 Java堆 中;
JDK8 的方法区是 元空间,其参数设置是 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N
。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。默认情况下,元空间的大小仅受 本地内存 限制,但可以通过参数来指定元空间的大小。
字符串常量池和运行时常量池是在堆还是在方法区?
在JDK1.7时,已经把原本放在永久代的字符串常量池、静态变量等移出。
在JDK1.8中,永久代的概念被完全废弃,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。原来移除从永久代移出的字符串常量池,静态常量,在更换了方法区实现后,并没有顺势进入到元空间。
在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区变动的只是方法区中内容的**物理存放位置。**类型信息(元数据信息)等其他信息被移动到了元空间中;但是运行时常量池和字符串常量池被移动到了堆中。不论它们物理上如何存放,逻辑上还是属于方法区的。
JDK1.8中字符串常量池和运行时常量池逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两则存在于方法区中,这就是造成误解的地方。
常见的垃圾收集器
串行垃圾收集器:-XX:+UseSerialGC
并行垃圾收集器:-XX:+UseParallelGC
CMS垃圾收集器:-XX:+UseConcMarkSweepGC
G1垃圾收集器:-XX:+UseG1GC
JDK8默认是并行垃圾收集器,JDK11默认是G1
设置初始化内存分配大小:-Xms1m 默认1/64
设置最大分配内存大小:-Xmx8m 默认1/4
打印GC 垃圾回收信息:-XX:+PrintGCDetails
OOM Dump: -XX:+HeapDumpOnOutOfMemoryError
GC:垃圾回收
引用计数法
对每个对象都有个引用计数器,引用一次就就加1,
弱点是:不能解决循环引用问题
复制算法
用于Eden区GC时,from区和to区 的复制
好处:没有内存碎片
坏处:浪费了一片幸存区to
标记清除算法
扫描活着的对象进行标记,对没有标记的对象进行清除
缺点:两次扫描浪费时间,会产生内存碎片
优点:不需要额外的空间
标记压缩算法(优化标记清除算法)
防止内存碎片产生,再次扫描,向一端移动存活的对象
分代收集算法
年轻代:存活率低 复制算法
老年代: 区域大:存活率 标记清除+标记压缩混合 实现