JVM虚拟机学习


关于该学习笔记参考网上的jvm课程以及一些文章的讲解

JVM结构:

JVM运行时数据区

局部变量表存放了编译期可知的基本数据类型、对象引用和returnAddress类型,其所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。

除了程序计数器之外,内存区域可能都会产生OutOfMemoryError异常,还可能抛出该异常的有直接内存,例如N I/O是一种基于通道和缓冲区的I/O方式,可以使用Native区域直接分配堆外内存。

对象的创建:

当遇到new对象时,虚拟机首先检查该指令的参数是否能够在常量池中定位到一个类的符号引用,并检查类的符号引用的类是否被加载、解析和初始化。

检查后为新生的对象分配大小固定的堆空间,分配的方式有“指针碰撞”和“空闲列表”,依据垃圾回收器是否带有压缩整理功能决定。例如:Serial和ParNew采用指针碰撞;CMS使用Mark-Sweep算法的收集器采用空闲列表。

频繁的对象创建导致内存空间分配出错,虚拟机采用两种解决方法,第一种是CAS失败重试的方式保证更新操作的原子性;另一种是把内存分存,又称本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)为每个线程分配内存,此时的对象是一个半初始化的值,随后调用方法,产生真正可用的对象

对象的布局:

对象的布局信息

Mark Word用于存储对象自身的运行时数据,有哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。

对象访问定位:

对象使用句柄访问

对象直接指针访问

两种访问方式的特点

使用句柄访问 使用直接指针访问
好处是reference中存储的是稳定的句柄地址,在对象移动时只改变句柄中实力数据指针,reference本身无需修改,垃圾收集器对象移动较多。 速度更快,节省了一次定位的时间开销,对象的访问在java中十分的频繁,Sun Hotspot使用该方式实现

JMM java的内存模型:

JMM内存模型

1、 Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。

2、 主要目的是定义程序中各个变量的访问规则。

3、 Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。

(1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。

(2) 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。

(3)主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。

4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制。

(1)JMM决定一个线程对变量的写入何时对另一个线程可见。

(2)线程之间共享变量存储在主内存中

(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。

(4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。

5、可见性、有序性(volatile):

(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。

(2)保证线程的有序执行,这个为有序性。(保证线程安全)

6、内存间交互操作:

(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

(3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。

(4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中

(5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。(6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。

(7)store(存储):把工作内存的变量的值传递给主内存

(8)write(写入):把store操作的值入到主内存的变量中

6.1、注意:(1)不允许read、load、store、write操作之一单独出现

(2)不允许一个线程丢弃assgin操作

(3)不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中

(4)一个新的变量只能在主内存中生成

(5)一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁

(6)如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值。

(7)如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量

(8)对一个变量执行unlock操作之前,需要将该变量同步回主内存中

堆的内存划分:

java堆的内存结构

Java堆的内存划分分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。

1、新生代:(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1 (3)内存不足时发生Minor GC

2、老年代:(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。

3、Perm:用来存储类的元数据,也就是方法区。(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

垃圾回收之前首先判断该对象能否被回收

判断对象是否要回收的方法:可达性分析法

1、 可达性分析法:通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)

2、 以下对象会被认为是root对象

JVM stack;native method stack;run-time constant pool;

static references in methodarea;Clazz

(1) 虚拟机栈(栈帧中本地变量表)中引用的对象 (2) 方法区中静态属性引用的对象 (3) 方法区中常量引用的对象 (4) 本地方法栈中Native方法引用的对象

对象的存活都与引用相关,java中的引用分为强引用、弱引用、软引用、虚引用。

强引用是程序代码中普遍的存在,只要强引用还在该对象不会被回收。

软引用是描述一些还有用但并非必需的对象在系统内存溢出前将会将这些对象列入垃圾回收的范围,SoftReference类实现软引用。(常用做缓存)

弱引用也用来描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前(放置内存泄露)

虚引用最弱的引用不能够获取对象实例,该引用关联的对象被回收时收到一个系统的通知(管理堆外内存)

3、 对象被判定可被回收,需要经历两个阶段:(1) 第一个阶段是可达性分析,分析该对象是否可达 (2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)

4、 方法区中的垃圾回收:(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池 (2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:A、 该类的所有实例被回收 B、 加载该类的ClassLoader被回收 C、 该类的Class对象没有被引用 (无法通过反射访问该类反射)

5、 finalize(): (1) GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。(2) 可以在该方法里面,指定一些对象在释放前必须执行的操作。

常见的垃圾回收算法:

1、Mark-Sweep(标记-清除算法):(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。(2)优缺点:实现简单,容易产生内存碎片

2、Copying(复制清除算法):(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。

3、Mark-Compact(标记-整理算法):(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

4.分代垃圾回收算法:

因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。(1)新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量 GC使用时对程序的影响?垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间。

几种不同的垃圾回收类型:

(1)Minor GC:从年轻代(包括Eden、Survivor区)回收内存。

(2)Major GC:清理整个老年代,当eden区内存不足时触发。

(3)Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发

常用的垃圾收集器

GC垃圾回收:

垃圾收集器

内存分配和回收策略

内存管理在于给对象分配内存回收分配给对象的内存

对象优先分配在Eden区域,大的对象直接进入老年代,例如数组对象;

长期存活的对象进入老年代,虚拟机给每个对象定义了一个对象年龄计数器(默认大于15岁进入老年代)。动态对象年龄判定,survivor区中相同年龄的所有的对象大于survivor区域的一半,比该年龄大的对象进入老年代;

空间分配担保MinorGC发生之前,需要确保老年代的最大可用连续空间是否大于新生代对象总空间,大于时直接Minor GC;不大于的时候需要查看HandlePromotionFailure担保值,可担保时进行Minor GC否则进行Full GC。

简单的测试题(j++和++j):

//输出的结果为10
public static void main(String[] args) {
        int j=0;
       for(int i =0;i<10;i++){
           j = (++j);
       }
        System.out.println(j);
}
//输出的结果为0
 public static void main(String[] args) {
        int j=0;
       for(int i =0;i<10;i++){
           j = (j++);
       }
        System.out.println(j);
}

字节码指令的比较

 j++字节码(在if内部的指令是先进栈,再改变局部变量表中j的值,栈中的元素值不会受到影响,依旧是0)
 0 iconst_0  将int类型数据0入栈
 1 istore_1  将栈顶int类型数值存入第二个局部变量表(istore_1表示第二个,从0开始)
 2 iconst_0  将int类型数据0入栈
 3 istore_2  将栈顶int类型数值存入第三个局部变量表
 4 iload_2    第三个局部变量进栈
 5 bipush 10   将一个byte类型的常量值推送至栈顶
 7 if_icmpge 21 (+14)  比较栈顶两int型数值大小,当结果大于等于0时跳转到21位置

10 iload_1     第二个局部变量进栈
11 iinc 1 by 1   指定int型变量增加指定值,局部变量的增加不会改变栈中元素的值,最后会被覆盖

14 istore_1    将栈顶int类型数值存入第二个局部变量表(istore_1表示第二个,从0开始)
15 iinc 2 by 1  指定int型变量增加指定值
18 goto 4 (-14)  无条件跳转
21 getstatic #2 
24 iload_1
25 invokevirtual #3 
28 return

++j字节码(在if内部的指令是先改变局部变量表中j的值,再将局部变量表中的值入栈,栈中元素的值会随着局部变量表的值更改,栈中的元素值不会受到影响,依旧是0)
0 iconst_0 将int类型数据0入栈
 1 istore_1  将栈顶int类型数值存入第二个局部变量表(istore_1表示第二个,从0开始)
 2 iconst_0  将int类型数据0入栈
 3 istore_2  将栈顶int类型数值存入第三个局部变量表
 4 iload_2    第三个局部变量进栈
 5 bipush 10   将一个byte类型的常量值推送至栈顶
 7 if_icmpge 21 (+14)   比较栈顶两int型数值大小,当结果大于等于0时跳转到21位置

10 iinc 1 by 1    指定int型变量增加指定值,先增加变量表中的值
13 iload_1       第二个局部变量进栈

14 istore_1    将栈顶int类型数值存入第二个局部变量表
15 iinc 2 by 1   指定int型变量增加指定值
18 goto 4 (-14)   无条件跳转
21 getstatic #2 
24 iload_1
25 invokevirtual #3 
28 return

Class文件结构:

class文件是以8字节为基础单位的二进制流紧凑的排列在一起,中间无分割符,高于8位字节的数据项以Big-Endian(高位字节地址在最低位、最低位字节地址在最高位)存储。Class文件中只有两种伪数据结构:无符号数(u1、u2、u4、u8)和表(_info)。

类的文件结构

魔数:class文件中的头4个字节为魔数,0xCAFEBABE

主次版本号:第5,6字节存储的是class文件的版本号,第7,8字节存储的是主版本号。

常量池:首先放置u2类型的常量池容量计数值,从1开始计数。常量池最要存放的是字面量(文本字符串,final常量等)和符号引用(类和接口的权限定名、字段的名称和描述符、方法的名称和描述符)

访问标志:标识当前的类是Class还是接口;是否定义为Public;是否为abstract类型;如果是类的话,是否声明为final。

类索引、父类索引、接口索引集合:由于java是单继承多实现,需要由标识,类索引与父类索引为u2的类型,接口是一组u2类型的数据集合,根据这三个数据项确定类的继承关系。

字段表集合:用于描述接口或者类中声明的变量,字段作用域(public;private;protect)、实例变量或类变量(static)、可变性(final)、并发可见性(valitatle)、是否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称.

方法表集合:对方法的描述,访问标志(access_flags)、名称索引(nane_index)、描述符索引(descriptor_index)、属性表集合(attributes)。

属性表集合:class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些专有的信息。

字节码指令:

字节码的指令类型(后面学习时再完善字节码指令部分)

加载和存储指令、运算指令、类型转换指令、对象创建与访问指令、操作数栈管理指令、控制转移指令、方法调用和返回指令、异常处理指令、同步指令、公有设计和私有实现

类加载机制:

类的生命周期:加载、验证、准备、解析、初始化、使用和卸载,验证、准备、解析三个部分统称为连接(Linking)


文章作者: it星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 it星 !
 上一篇
java源码 java源码
it星
java源码笔记 注:源码学习是一个长久的过程,目前有的程序还模糊,相信自己以后会明白的1.ArrayList ArrayList是由长度可变的数组组成的,允许null值,与Vector相类似,该类的方法不同步 size、empty、ge
下一篇 
java操作excel java操作excel
java操作excel 使用apache提供的工具POI操作Excel1.Maven的依赖 <dependency> <groupId>org.apache.poi</groupId
2020-04-07
  目录