- 本文以 hotspot 虚拟机
- jvm是一个内存中的虚拟机,也就代表着它的存储就是内存,我们所写的类、常量、方法都在内存中
1. 引言
1.1 java是如何实现平台无关性
- 程序员编写一个
hello.java
文件 - 使用
javac hello.java
命令开启java
编译器进行编译,生成一个hello.class
文件 - 使用
java hello
启动虚拟机运行程序 - 虚拟机首先会将字节码加载到内存中,这个过程称为类加载,是由类加载器
(class loader)
完成 然后虚拟机针对加载到内存中的字节码进行解析,比如实例化对象……
2. Jvm的构造
2.1 类加载器(class loader)
2.1.1 作用
主要是从系统外部获取特定格式的字节码文件,将其转换为二进制数据流,加载进入系统,交给虚拟机进行连接,初始化等操作,因为虚拟机就在内存中所以,类加载器也可以说将字节码文件加载到内存中
2.1.2 种类
对用户不可见,想看的话去看
jvm
的源码BootStrapClassLoader
:C++
编写,加载核心类库java.*
ExtensionClassLoader
:java
编写,加载扩展库javax.*
,自定义jar
包AppClassLoader
:java
编写,加载程序所在目录,加载classPath
,即环境变量CustomClassLoader
:java
编写,自定义class load
定制化加载2.1.3 双亲委派机制
双亲委派机制(上图↑)
- 自底向上的检查类是否已经加载,若发现没有加载
- 自顶向下的尝试加载类
- 该加载机制的优势
- 避免多份同样的字节码文件被夹在,浪费内存
源码解析
// 注意: 一些不重要的代码已经删除,想看直接来看源码
protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
// 需要用同步锁,防止在高并发下多个线程同时加载
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经加载,如果已经加载,因为resolve默认是false直接返回加载的类
Class<?> c = findLoadedClass(name);
// 如果类没有加载,就开始下面的骚操作
if (c == null) {
// 这里就是一层一层的向上询问,父类加载器是否加载过该类
// 最后因为BootstrapClassloader是c++写的需要调用native的方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 检查完4层的类加载器,发现类还是没有被加载过,则尝试从上到下不断尝试加载该类
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
2.2 运行时区域(runtime data area)
运行时区域这个就是
**Jvm**
的内存结构模型
2.2.1 线程问题
- 线程私有: 程序计数器、虚拟机栈、本地方法栈
-
2.2.2 程序计数器
记录当前线程所执行的字节码文件的行号指示器,改变计数器的值选取下一行所需要执行的指令
- 如果是执行java方法则是计数能力,如果是
native
的方法此时的值是Undefined
- 和线程是一对一的关系,所以“线程私有”
-
2.2.3 虚拟机栈
java方法执行的内存模型
- 包含多个栈帧,每个方法被执行的时候都会创建一个栈帧,即方法运行期间的基础数据结构,当方法调用结束的时候帧才会被销毁
- 栈帧存储的信息:局部变量表、操作栈、动态连接、返回地址
- 局部变量表:包含方法执行过程中的所有变量
- 操作栈:入栈、出栈、复制、交换、产生消费变量
问题一:递归为什么会引起
**java.lang.stackOverFlowError**
,栈溢出?- 每创建一个方法就会产生一个栈帧,系统会在该方法执行完成之后销毁栈帧
- 每个线程的虚拟机栈深度是有限的
- 递归调用会不断的产生栈帧,并且不会销毁,所以造成栈溢出
问题二:虚拟机栈过多,会引起
**java.lang.OutOfMemoryError**
,内存溢出?- 虚拟机栈会进行动态扩展内存,如果未能申请到扩展内存,便会报出上面一错误
2.2.4 本地方法栈
2.2.5 方法区
- 方法区只是
JVM
的规范,它的实现是元空间(mate space)
,jdk7
以前叫做永久代(PermGen)
- 两者都是用来存储
class
的相关信息,包含class
对象的method
等 jdk7
之前字符串常量池位
于方法区
,jdk8
之后移动到了堆内存
中问题三:
**Mate Space**
相较于**PermGen**
的优势?(核心是**mate space**
使用本地内存产生的好处)**Mate Space**
使用本地内存;**PermGen**
使用的**jvm**
的内存- 字符串常量池存在
**PermGen**
中容易出现性能问题和内存溢出 - 类和方法的信息大小不确定,给永久代大小指定带来困难,太小容易永久代溢出,太大容易导致老年代溢出
**PermGen**
会为**GC**
带来不必要的复杂性,并且回收效率偏低,是指永久代不足的时候会触发**Full GC**
2.2.6 堆
- 对象实例的分配区域,并且堆内存是可扩展的,物理地址可以是不连续的,逻辑是连续的即可,如果堆内存申请扩展没有收到相应的内存,会抛出
java.lang.OutOfMemoryError
-
2.3 执行引擎(execution engine)
负责对
class load
加载到内存中的命令进行解析,解析完成提交到操作系统执行2.4 本地接口(native interface)
融合不同语言的原生开发库为
java
所用,比如“Class.forName()
”,锁底层实现CAS
一些问题
类装载的过程
加载: 通过classLoader加载class字节码文件,生成class对象
- 链接
a. 校验: 检查加载的class的正确性和安全性
b. 准备: 为类变量分配存储空间,并设置类变量的初始值
c. 解析: jvm将常量池内的符号引用转换为直接引用 -
JVM三大内存调优参数的意义
-Xms -Xmx -Xss
静态存储: 编译时确定每个数据目标在运行的的存储空间需求
- 栈式存储: 数据需求在编译时未知,运行时模块入口前确定
-
内存模型中堆和栈的区别
联系:引用对象、数组时,栈里面定义变量保存堆中目标的首地址;当他们没有引用对象指向的时候,在一个不确定的时间被GC回收
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈能支持静态分配和动态分配,堆只支持动态分配
- 效率:栈的效率比堆高很多
元空间、堆、线程独占部分之间的联系-内存角度
- 翔仔面试视频
6-11
的9:36
- 执行
main
方法之内存中的变化,看图即可