目录
1.JVM的基本介绍+结构
- Java程序设计语言,java虚拟机,javaApi类库三部分统称为JDK,JDK是用户支持java程序开发的最小环境。把java文件列入javaSe API子集和java虚拟机这两部分统称为JRE, JRE是支持的java程序运行的标准环境
2.类加载过程
- jvm把class文件加载到内存,并对数据进行验证,解析,初始化,最终形成jvm可以直接使用的java类型的过程
(1)加载
- 将class文件(文件可以来自硬盘,内存,网络等各个地方)字节码加载到内存中,并将这些静态数据转化成方法区的二进制的运行时数据结构,同时在堆中生成一个代表这个类的java.lang.class(反射)对象,作为方法区类数据的访问入口。
(2)链接(验证,准备,解析)
- 将加载进来的二进制数据合并到jvm运行状态的过程
1.验证
- 确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
2.准备
- 正式为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中
3.解析
- 把jvm常量池中的符号引用替换成直接引用
(3)初始化
初始化就是执行类构造器 ()方法的过程,类构造器()方法是由编译器自动收集类中所有的类变量赋予的动作和静态语句块中的语句合并产生的。如果该类具有父类(父类未初始化)就先对父类进行初始化。jvm会保证一个类在多线程环境下正确加锁和同步。
1.主动引用(一定会初始化)
- 1.调用类的静态成员(除fial常量)和静态方法
2.new一个类的对象
3.使用反射方法对类进行反射调用
4.先启动main方法所在的类
2.被动引用(不会初始化)
- 1.引用常量不会触发类的初始化
2.访问一个静态域时,只有当正真声明这个类的域时才会被初始化
3.通过数组定义类的引用不会触发类的初始化
3.(运行时数据区)内存结构分析
(1)栈
1.虚拟机栈结构
-
1Java虚拟机栈是线程隔离的也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
-
2如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈) -
3.Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分
2.栈帧
-
1.栈帧是一种先进后出的数据结构,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
-
2.在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行的操作
3.局部变量表
- 1.局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
- 2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
4.变量槽
- 局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
(2)堆
JDK 7后方法区(永生代)移出了堆改为元空间,所以这里不做讨论
1.堆结构
-
1堆内存分为年轻代和老年代,年轻代又分为Eden和Survivor区。Survivor区又分为0区和1区。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1
-
2 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收(GC算法后面会说)
2.分代概念
-
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
-
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC=Fullr GC,避免响应超时。
-
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代(所有GC都会停止应用所有线程)
3.堆内存常用参数
GC算法
-
引用计数法(Java没有采用)
-
标记-清除法 (jvm老年代回收)
-
标记-压缩法 (jvm老年代回收)
-
复制算法 (jvm新生代回收)
(3)本地方法栈
- 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。不同的是本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。如何去服务native(本地)方法?native(本地)方法使用什么语言实现?怎么组织像栈帧这种为了服务方法的数据结构?虚拟机规范并未给出强制规定,因此不同的虚拟机实可以进行自由实现,我们常用的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈
(4)程序计算器
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。-
- 摘自《深入理解Java虚拟机》
-
1.程序计数器会随着线程的启动而创建,先来直观的看下计数器中会存哪些内容
-
2.线程隔离性,每个线程工作时都有属于自己的独立计数器
-
3如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址
-
4.执行native本地方法时,程序计数器的值为空。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的
-
5此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域