最近在研究JVM原理,整理心得以便于后续面试复习使用。
一。JVM内存模型(运行时数据区)
说明:这是JVM规定的内存模型,每个具体实现根据厂商有所不同。比如:HotPot 1.8之前有持久带(方法区的实际实现)的概念,其他如IBM J9没有这个概念,
1.程序计数器(PC)
线程执行的字节码的行号指示器,一块比较小的内存,这个指示器一行一行的读取命令进行处理。
不同线程执行的命令肯定是不一样的所以这个需要每个线程有一个。
执行本地方法(Native)时值为unifined空。
该区域是唯一不会OOM(内存溢出)的地方
2.栈
虚拟机栈是描述Java方法执行的内存模型,是动态概念。每个方法执行时会创建一个栈帧,里面存放局部变量,操作数栈,动态链表等信息。
基本数据类型都是直接存放到栈上的,对应到每个方法就是该方法的栈帧。但是,对象都放到堆上,栈里只存放对象的引用。
3.本地方法栈
上面的栈是执行Java字节码,本地方法栈执行的是本地方法,作用相同。本地方法可以使用不同语言实现,比如C++。
4.堆
内存最大的一块,所有对象都在这里创建,GC(垃圾回收的主要战场,注意不是全部)。
内存分配策略:
使用哪种一般有GC方式决定。使用分代复制方法的一般使用指针碰撞,使用标记清除方法的使用空闲列表。
指针碰撞:
内存被占用和空闲区域是规整的,指针指向临界位置,每次分配内存只需要指针向空闲区域偏移对象的大小即可。
空闲列表:
内存被占用和空闲区域是离散的,空闲区域由专门的表记录。
线程分配缓存(TLAB)
每个线程会在堆里申请一块属于自己的空间,对象创建的时候再自己的空间中创建,避免多线程引起的问题。后面创建对象的时候详细说。
设置参数
-Xmx(最大值) -Xms(最小值)
如果最大值和最小值相同表示堆不能自动扩展。
5.方法区
存储类信息,常量,静态变量,即时编译代码等。该区域是问题最多的区域,JDK1.8之前hotspot的GC可以回收该部分的垃圾(常量池的回收和类型的卸载),但是效果不好,而且JVM很多严重bug都是这个地方导致的。
1.8已经把这块内存移动到物理内存。
运行时常量池
方法区的一部分,跟类的常量池不一样,它是动态的,运行期会动态变化,比如调用string 的intern()方法。(string str = 'abc' 先去常量池中找是否有该对象引用,如果没有创建引用并把引用放到常量池,如果有就返回常量池中的引用 )
6.HotSpot对象创建过程。
(1)对象创建方式
克隆,反序列化,new等
(2)获取类信息
JVM遇到new之后先检查方法区中该类是否已经加载,如果没有就先进行加载,如果有就可以获取到类的相关信息,通过这些信息就可以获取到该类需要使用的内存空间大小。
(3)申请内存
根据GC方式使用指针碰撞或者空闲列表申请内存,如上面说的。
由于现在都是多线程,申请内存过程中会有多线程的问题。比如两个线程同时申请一块内存,就会导致问题。
解决方法:
a.事务:把申请内存的操作做成事务,保证原子性。一般采用CAS失败重试机制。
b.TLAB: 每个线程在堆中申请属于自己特有的缓存区,创建对象的时候在缓冲区中申请内地。只有在本地缓冲区的内存不足时再进行同步申请
-XX:+/-UseTLAB来设置是否使用。
(4)创建对象
内存分配好后就会创建出对象,由于没有进行初始化,每个字段的数据类型值都是零。但是对象可以使用,比如解决循环依赖问题。
(5)对象初始化
调用init方法初始化对象中的字段。
7.对象定位的方式
对象信息包括两部分:对象实例数据和对象类型数据。
(1)通过句柄方式
优势:GC过程中会把对象进行移动,这种方式只需要修改到对象实例数据的指针就可以,reference本身不需要变动。
(2)直接使用地址引用方式
优势:节省一次指针定位开销。
二,类加载原理
在加载之前是编译,编译的作用就是把.java文件编译成.class文件。
如上图所示类加载有七个阶段,其中验证,准备,解析统一为链接阶段。
需要初始化的5中情况 (有且仅有)
1.new,getstatic,putstatic,invokestatic 这4个字节指令码。场景,new一个对象,获取类的静态对象,调用类的静态方法。
2.反射调用
3.使用子类的时候,如果父类没有初始化,先初始化父类。
4.main函数的类在启动的时候就会初始化。
5.使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果的方法句柄对应的类没有初始化,需要进行初始化。
注意事项:
类:
1.初始化之前必须先进行加载和链接。所以虽然JVM规范没有定义加载时机,但是,上面五个会触发加载。
2.通过子类调用父类的静态字段,只会初始化父类,不会初始子类.
3.数组中的引用类不会进行初始化,JVM会自动生成一个object的子类,类为数组中的类型。 字节码:newarray。
4.调用类中的final类型字段不会导致初始化。比如一个类A只是调用了B中的final类型的字段,在编译完成后,A的常量池中就已经有B的这个字段值,所以在编译完成后这两个类就没有任何关系了。
上面都是针对类的,接口有所不同:
1.接口中不能放 static{}语句块。但是,编译器仍然会生成<clinit>() 类构造器。因为接口中可以声明成员变量,需要初始化。
2.接口在初始化的时候不要求它的父类都初始化。只有在调用的时候才会初始化。
个人总结:只要在初始化之前能获取到,就不进行初始化。在编译期能获取到就不会进行连接。
加载过程详解:
1.加载
总共三步:
(1)通过全限定名获取类的二进制字节流
(2)将上面的字节流转换成方法区的运行时数据结构。
(3)在内存中生成该类的对象作为程序访问该类的外部接口,以后创建对象就是基于这个对象创建,可以放在方法区也可以放到堆上,不同厂商有不同实现,HotSpot是放到方法区中。
注意:数组类型是JVM自动创建。
2.验证
(1)文件格式验证 (2)元数据验证 (3)字节码验证 (4)符号引用验证
3.准备
在方法区中给类静态对象(非静态的不分配,跟对象在堆中分配)分配内存,并设置初始值(这里的初始值不是程序设定的值而是默认值),真正的值需要在初始化的时候才设定。
boolean:false 数值类型:0 引用类型:null;
4.解析
把常量池中的符号引用替换为直接引用的过程,
符号引用:
一组描述被引用对象的符号,可以是任何形式,只要无歧义即可。
直接引用:
直接指向目标的指针,或者句柄。
需要解析的类别:
(1) 类或者接口 (2)字段(3)类方法(4)接口方法(5)方法类型(6)方法句柄(7)调用点限定符
注意:有个特殊,invokeddynamic指令,支持动态的指令,这个不会解析,需要等到程序运行时才会解析。
所以解析阶段发生时间不确定。
5.初始化
执行类构造<clinit>()方法的过程。该方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块,按照顺序进行赋值。
public class Test{
static{
i=0; //赋值可以
system.out.println(i); //应用报错,非法向前引用。
}
static int i=1;
}
注意事项:
(1)<clinit>()方法是JVM自动调用,而且是先执行父类的,所以第一个一定是object。
(2)父类的静态语句优先于子类的赋值操作。
(3)<clinit>()非必须,如果没有静态语句块,和变量赋值,不会生成。
(4)接口也会生成。
(5)JVM会保证多线程安全。
类加载器
JVM外部实现。
类相同必须满足两点
(1)同一个类
(2)被同一个加载器加载。
双亲委派模型
1.启动类加载器(Bootstrap ClassLoader)
加载<JAVA_HOME>\lib中的jar包,使用C++编写。
2.扩展类加载器(Bootstrap ClassLoader)
加载<JAVA_HOME>\lib\ext中的jar包,使用C++编写。
3.应用程序加载器
getSystemClassLoader()方法获取。负责加载用户路径(ClassPath)上的指定类库。默认加载器。
加载过程:
收到请求后先用父类加载器加载,父类也是找父类,所以一个对象最终都是由启动类加载器先加载,除非获取不到再自己加载。
这样可以保证同一个类都是用同一个加载器加载,从而保证唯一性。比如 object 在rt.jar中。越基础的类由越上层的加载器进行加载。
破坏双亲委派模型:
JDBC的加载器是启动类加载器,但是,jdbc需要依赖不同的厂商,所以需要调用子类的方法去解析特定的环境。
来源:https://www.cnblogs.com/fymc/p/11060595.html