JVM
文章目录
三种JVM
- Sun公司 Hotspot
-
- BEA
JRockit
- IBM
J9VM
我们学习都是HostSpot
1- JVM的位置
2-JVM的体系结构
模型参考大佬画的, 地址:ProcessOn模板社区-JVM内存模型完整版 https://www.processon.com/view/5ea7a1b9e401fd21c196eb17?fromnew=1#pc
3-类加载器
-
作用: 加载class文件
-
-
虚拟机自带的加载器
-
启动类(根)加载器 (BootstrapClassLoader)
-
扩展类加载器(ExtClassLoader )
-
应用程序加载器(AppClassLoader)
-
4-双亲委派机制
- 步骤
- 类加载器收到类加载的请求
- 将这个请求向上委托给父类加载器去完成, 一直向上委托, 直到启动类 ( 根 ) 加载器
- 启动加载器检查是否能够加载当前这个类, 能加载就结束, 使用当前的加载器, 否则, 抛出异常, 通知子类加载器加载
- 重复步骤3 直到找到
- Class Not Found ~ 都找不到
- Null : java调用不到 , 由于底层是C语言写的
- 作用
- 防止重复加载同一个
.class
。通过委托去向上面去查找,加载过了,就不用再加载一遍。保证数据安全。 - 保证核心
.class
不能被篡改。通过委托方式,不会去改核心.clas
,即使改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
5-沙箱安全机制
Java安全模型的核心就是ava沙箱(sandbox) ,什么是沙箱?沙箱是一个限制程序运行的环境。 沙箱机制就是将Java代码限定在虚拟机JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保
证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么? CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的ava程序运行都可以指定沙箱,可以定制安全策略。
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信
的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙
箱(Sandbox)机制。如下图所示JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就
无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资
源的访问权限。如下图所示JDK1.1安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全
策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
JDK1.2安全模型
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,
系统域部分专门门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访
问。虚拟机中不同的受保护域(Protected Domain),对应不一-样的权限 (Permission)。存在于不同域中的类文件
就具有了当前域的全部权限,如下图所示最新的安全模型(jdk 1.6)
组成沙箱的基本组件:
字节码校验器
(bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存
保护。但并不是所有的类文件都会经过字节码校验,比如核心类。类装载器
(class loader) : 其中类装载器在3个方面对Java沙箱起作用- 它防止恶意代码去干涉善意的代码; //双亲委派机制
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由-系列唯一的名称组成, 每一个被装载的
类将有一一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的, 它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
1.从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然
无法生效。
存取控制器
(access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略
设定,可以由用户指定。安全管理器
(security manager) :是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优
先级高。安全软件包
(security package) : java.security 下的类和扩展包下的类,允许用户为自己的应用增加新的安
全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名 keytools
- 加密
- 鉴别
6-Native关键字
第一次见是在Thread的start方法
- native :
- 有native关键字的方法, 说明java的作用范围达不到了, 回去调用底层C语言的库
- 会进入本地方法栈 --> 调用本地方法接口 JNI : java Native Interface
- JNI 的作用: 扩展java的使用, 融合不同的编程语言为java使用
- 历史:
- java诞生的时候, C , C++横行, 想立足必须要有调用C, C++ 的程序
- 它在内存区域中专门开辟了一块标记区域,: 本地方法栈(Native Method Stack), 登记Native方法
- 最终执行的时候, 通过JNI加载本地方法库中的方法
public static void main(String[] args) {
new Thread( ()->{
},"Thread name").start();
}
private native void start0();
7-PC寄存器
程序计数器: Program Counter Register
每个线程都有一个程序计数器, 是线程私有的, 就是一个指针, 指向方法区中的方法字节码( 用来存储指向想一条指令的地址, 也即将要执行的指令代码 ) , 在执行引擎读取下一条指令, 是一个非常小的内存空间, 几乎可以忽略不计
8-方法区
Method Area 方法区
方法区是被所有线程共享, 所有字段和方法字节码, 以及一些特殊方法, 如构造函数 , 接口代码也在此定义, 简单说, 所有定义的方法信息都保存在该区域, 此区域属于共享区间
静态变量 , 常量, 类信息(构造方法, 接口定义), 运行时的常量池存在方法区中, 但是实例变量存在堆内存中, 和方法区无关
static , final , Class模板 , 常量池
9-栈 Stack
线程级
程序 = 数据结构 + 算法
- 栈: 先进后出, 后进先出 :桶
- 队列: 先进先出 (FIFO: First Input , First Output )
- 喝多了吐就是栈, 吃多了拉就是队列
栈帧
栈帧随方法的调用而创建, 随方法结束而销毁, 存放了方法的局部变量信息
栈运行原理:
启动main方法往桶里放一个main方法
main方法调用test会在桶里在压一个test ,test调用a 在压一个a 方法, a执行完成之后弹出, 依次直到没有
| | | | | a() |
| | |test()| |test()|
|main()| |main()| |main()|
———————— ———————— ————————
递归 test 调用 a , a 调用test , 就会一直往栈里压方法 , 放满时就报异常
栈: 栈内存 , 主管程序的运行, 生命周期和线程同步
线程结束, 栈内存也就释放了 , 对于栈来说, 不存在垃圾回收问题; 一旦线程结束, 栈就结束了
程序计数器是用在多线程的情况下,他会记录每个线程的执行位置,当cpu切换回来的时候,就可以找到之前的位置,继续执行
栈中用哪些内容
- 8大基本类型
- 对象引用
- 实例方法
10-堆 Heap
一个JVM只有一个堆内存,堆内存的大小是可以调节的
JDK8中运行时常量池和静态常量池存放在方法区(元空间)中,而字符串常量池依然存放在堆中。
- 新生代( Eden , 伊甸园)
- 老年代:
- 当新生代经历15次轻GC后还存在引用的,则被转移到老年代
- 元空间:这个区域常驻内存的,用来存放jdk自身携带的class对象,interface元数据
GC垃圾回收, 主要是在新生区和养老区
假设内存满了, OOM , 堆内存不够~! java.lang.OutOfMemoryError: Java heap space
在JDK8以后, 永久存储区改了个名字 ( 元空间 metaSpace )
新生区
- 类: 诞生和成长的地方, 甚至死亡
- Eden , 所有的对象都是在Eden区new出来的
- 幸存者区( survivors0,survivors1)
对象在伊甸园放不下将会执行一次轻GC,清理幸存区直到能放下对象,如果幸存区清理不出来,任然放不下对象,则会发生重GC,将清理幸存者区并且将幸存区存活下来的对象移到养老区,
如果清理过后幸存区域任然放不下对象,则重GC会将对象直接放在养老区中,如果养老区也不足以放下该对象,则会产生OOM。
老年代:
- 当新生代经历15次轻GC后还存在引用的,则被转移到老年代
永久区
这个区域常驻内存的. 用来存放JDK自身携带的Class对象 Interface元数据, 存储的是Java运行时的一些环境或类信息, 这个区域不存在垃圾回收! 关闭JVM虚拟机会释放这个区域的内存
一个启动类, 加载了大量的第三方jar包, Tomcat户数了太多的应用, 大量动态生成的反射类, 不断地被加载. 直到内存满了, 就会出现OOM
- jdk1.6之前:永久代,运行时常量池和字符串常量池在方法区
- jdk1.7 :永久代,但是慢慢退化,去永久代,字符串常量池在堆中
- jdk1.8之后:无永久代,运行时常量池在元空间,字符串常量池在堆中
- 元空间:这个区域常驻内存的,用来存放jdk自身携带的class对象,interface元数据
元空间:直接就是一句话逻辑上存在物理上不存在
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory(); //字节
//返回具jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = "+max+" "+(max/(double)1024/1024)+"MB");
System.out.println("total = "+total+" "+(total/(double)1024/1024)+"MB");
// max = 3806855168 3630.5MB
// total = 257425408 245.5MB
//默认情况下: 分配的总内存是电脑内存的 1/4 . 初始化内存 1/64
//-Xms1024m -Xmx1024m -XX:+printGCDetails
/*
max = 1029177344 981.5MB
total = 1029177344 981.5MB
Heap
PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3011K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 323K, capacity 388K, committed 512K, reserved 1048576K
*/
// young 305664K+ old 699392K = 1,005,056K = 981.5MB 所以元空间不在JVM内存中,在本机内存中
}
oom报错分析
- 能够看到代码第几行出错: 内存快照分析工具, Eclipse( MAT) , Jprofiler
- Debug, 一行行分析代码
MAT, Jprofiler作用
- 分析Dump内存文件, 快速定位内存泄露
- 获得堆中的数据
- 获得大的对象
11-GC: 垃圾回收
JVM在进行GC时, 并不是对两个区域统一回收, 大部分时候, 回收都是在新生代
- 新生代
- Eden 伊甸园
- Survivors 幸存者
- from
- to 谁空谁是to
- 老年代
两种GC : 轻GC MinorGC , 重GC FullGC
GC题目
- JVM的内存模型和分区 详细到每个区放什么
- 堆里有哪些分区?
- GC的算法有哪些? 标记清除法, 标记压缩, 复制算法, 引用计数器, 怎么用
- 轻GC和重GC分别在什么时候发生
复制算法
- 好处 : 没有内存碎片
- 坏处 : 浪费了内存空间, 多了一般的空间永远是空的to.
- 极端情况( 对象100%存活) ,复制的时候复制很多对象,
复制算法最佳使用场景: 对象存活度低 新生代
标记清除法
标记: 可达性分析(引用链法):查询对象从GCRoot-虚拟机栈-方法区静态应用-方区常量池-本地栈JNI,若无引用则GC
- 优点: 节省空间
- 缺点: 两次扫描, 严重浪费时间, 会产生内存碎片
标记压缩
总结
内存效率: 复制算法> 标记清除 > 标记压缩 ( 时间复杂度 )
内存整齐度: 复制算法 = 标记压缩 > 标记清除
内存利用率: 标记压缩 = 标记清除 > 复制算法
没有最好的方法, 只有最合适的算法
GC: 分代收集算法
- 年轻代
- 存货量低 — 复制算法
- 老年代
- 区域大: 存活率高
- 标记清除+标记压缩结合使用
- 先标记, 内存碎片过多在压缩
Jprofiler
官网:ej-technologies - Java APM, Java Profiler, Java Installer Builder
下载安装包后安装好之后打开IDEA --> Plugins安装插件
IDEA配置jf路径
添加参数
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
运行获得jf文件打开
在项目路径下面
发现报错行数
找到错误行
JVM参数
-Xms: 堆最小空间
-Xmx: 堆最大空间
-XX:NewRatio: Old/New 的比例
-Xmn: 年轻代大小, 调整会影响老年代大小, 官方建议为堆大小的3/8
-XX:SurvivorRatio: 调整Survivor和Eden区的大小比例
-XX:MetaspaceSize: 元空间初始化大小, 64为JVM默认为20.75M
-XX:MaxMetaspaceSize: 元空间最大大小, 逻辑限制为屋里内存上限
-XX:PretenureSizeThreshold: 大对象直接进入老年代的阈值
-XX:MaxTenuringThreshold: 进入老年代的分代年龄阈值
-XX:-XX:TargetSurvivorRatio: 动态年龄判断比例设置
-Verbose:gc: 输出JVM的gc情况
-XX:+PrintGCDetails: 输出GC详细信息
-XX:+PrintTenuringDistribution: 输出对象GC年龄信息
-XX:+PrintGCTimeStamps: 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps: 输出GC的时间
-XX:+PrintHeapAtGC: 在进行GC的前后打印出堆的信息
-Xloggc…/logs/gc.log: 日志文件的输出路径
-XX:+UseG1GC: 用G1-GC收集器
-XX:-UseConcMarkSweepGC: 用CMS-GC收集器
-XX:+PrintCommandLineFlags-version: 输出默认的垃圾回收器
Jprofiler
-XX:+HeapDumpOnOutOfMemoryError dumpOOM错误信息
-XX:+HeapDumpOn+报错类型
常见面试题
-
谈谈对JVM的理解, java8虚拟机和之前有什么变化
- 2
- 撤销了永久带,引入了元空间:
- 类执行pot虚拟机中,jkd1.6时,设计团队把方法区设计为永久带,这样GC工作区域就可以扩展至方法区。这种策略可以可以避免为方法区单独设计垃圾回收机制,但是坏处就是,方法区的回收条件十分苛刻,而且回收效果也不好。
- 在jdk1.7版本,设计团队也意识到这个问题,但是只将方法区中的字符串常量池移除永久带。到了最新的jdk1.8版本,就不再有永久带这个概念,并且用元空间来代替原来的永久代
- 元空间内的规则:元空间中类及其相关的元数据和类加载器生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。
-
什么是OOM, 什么是栈溢出stackoverflowerror? 怎么分析
-
堆内存不足是最常见的 OOM 原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。
而对于 Java 虚拟机栈和本地方法栈,这里要稍微复杂一点。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
直接内存不足,也会导致 OOM。
-
-
JVM常用调优参数有哪些
-
内存快照如何抓取, 怎么分析Dump文件?
-
谈谈JVM中, 类加载器你的认识?