JavaJVM
JVM探究
JVM的学习方法
面试题:
- 请你谈谈你对JVM的理解?Java8虚拟机和之前相比有什么变化?
- 什么是OOM?什么是栈溢出StackoverflowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
- 谈谈JVM中,你对类加载器的认识?
要素:
-
JVM的位置
-
JVM的体系结构



-
类加载器
作用:加载 Class字节码文件

加载器分类:
-
虚拟机自带的加载器
-
启动类(根)加载器
-
扩展类加载器
-
应用程序加载器
-
-
双亲委派机制
作用:保证安全。APP--》EXT--》BOOT
自己定义的包和系统包冲突的时候,会用系统包。
也就是说,java会向上检查是否存在这个包,如果上面有这个包就用上面的,不会用你自己写的包。
类加载步骤:
- 类加载器收到类加载的请求!
- 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
- 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
- 重复步骤 3
ClassNotFound
null:java调用不到,底层是用C/C++写的
-
沙箱安全机制
一:什么是沙箱?
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——
CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,可以定制安全策略。
首先进行一个字节码校验器:确保java类文件遵循java语言规范,这样可以帮助java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类。
接下来类装载器:其中类装载器在3个方面对java沙箱起作用
- 它防止恶意代码去干涉善意的代码。//双亲委派机制
2.它守护了被信任的类库边界。//双亲委派机制
3.它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由java虚拟机为每个类装载器维护的,他们互相之间甚至不可以见。
类装载器采用的机制是双亲委派模式。
二:java类运行
第一步:编译
利用编译器将源程序编译成字节码文件;源文件名.class
第二步:运行
利用虚拟机(解释器,java)解析执行class字节码文件

-
Native,被Native修饰的方法,底层是由C/C++实现。
-
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
-
方法区
方法区被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单来说,所有定义的方法信息都保存在该区域,此区域属于共享区域。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存储在方法区中,但是实例变量存储在堆内存中,与方法区无关。
-
栈:数据结构
程序 = 数据结构 + 算法
栈:先进后出,后进先出。比如弹夹。
队列:先进先出,比如水管。
栈:占内存,管理程序的运行,生命周期和线程同步。线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就结束。
栈一般存放8大基本类型、对象引用、实例方法
堆、栈、方法区交互图:

-
三种JVM
SUN公司:HotSpot,最流行最常用
BEA公司:JRockit JVM
IBM公司:J9VM
-
Heap,一个JVM只有一个堆内存,并且堆内存的大小是可以调整的!
类加载器读取了类文件后,一般会把什么大小放到堆中?
堆内存分为三个区域:
-
新生区
-
老年区
-
永久区

GC垃圾回收,主要发生在新生区和老年区
假如内存满了,就会出现OOM,说明堆内存不够!OutOfMemoryError
在JDK8以后,永久存储区改了一个名字,叫做元空间。
-
-
新生区、老年区、永久区
新生区:
- 类:诞生和成长的地方,可能死亡
- 伊甸园:所有对象都是在伊甸园区new出来的!
- 幸存区:分为幸存者0区和幸存者1区

伊甸园满了就用轻GC,活下来的放入幸存区。幸存区满了就用重GC,活下来的放入老年区。
老年区:
经过研究,98%的对象都是临时对象!
永久区:
这个区域是常驻内存的。用来存放JDK自带的Class对象。如Interface元数据,存储的是Java运行时的一些环境。这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存。
永久区崩溃的情景:
- 一个启动类加载了大量的第三方jar包。
- Tomcat部署了太多的应用。
- 大量动态生成的反射类,不断被加载,直到内存满,就会出现OOM。
版本历史
- JDK1.6之前:有永久代,常量池在方法区。
- JDK1.7:有永久代,但是正在退化,提出了去永久代,常量池在堆中。
- JDK1.8之后:没有永久代,常量池在元空间。

元空间:逻辑上存在,物理上不存在。
在一个项目中,突然出现了OOM故障,该如何排错?
- 看代码第几行出错:使用内存快照分析工具,Jprofiler
- Debug:一行一行分析代码!
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象
-
堆内存调优
-
GC
- 常用算法
标记清除法、标记整理法、复制算法、分代收集算法
引用计数法:

复制算法:


-
好处:没有内存的碎片
-
坏处:浪费了内存空间,一半空间永远是空to
复制算法最佳使用场景:对象存活度较低的时候,新生区
标记清除算法:

- 优点:不需要额外的空间!
- 缺点:两次扫描,严重浪费时间,还会产生内存碎片。
标记压缩算法:

-
JMM
Java内存模型
-
总结
学习途径:
- 搜索引擎
- 通过前人总结的方法,思维导图等
总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法 --> GC:分代收集算法
年轻代:
- 存活率低
- 采用复制算法
老年代:
- 区域大:存活率高
- 采用标记清除(内存碎片不是太多) + 标记压缩混合 实现