深入理解Java虚拟机

1、内存区域分布:https://blog.csdn.net/xyh930929/article/details/80955959

     第6点【intern的特殊之处】,有个错误如下图,笔者说反了,应该是://JDK1.6以前是false,JDK1.7以后是true

2、垃圾收集算法:https://blog.csdn.net/xyh930929/article/details/81121478

3、HotSpot算法和垃圾收集器:https://blog.csdn.net/xyh930929/article/details/81281471

4、Eden、Survivor、老年代、GC日志:https://blog.csdn.net/xyh930929/article/details/84067767

5、类文件结构:https://blog.csdn.net/xyh930929/article/details/81290669

6、类加载机制:https://blog.csdn.net/xyh930929/article/details/81329260

7、字节码执行引擎(栈帧、动态连接、方法调用):https://blog.csdn.net/xyh930929/article/details/84067186

 

 

 

发展史

技术体系

java虚拟机

java内存管理:

1、程序计数器(PC、Program Counter Register)

当前线程所执行的字节码的行号指示器。

Java方法,记录正在执行的虚拟机字节码指令的地址;当执行的是Native方法,这个计数器值为空。

每条线程都会有一个独立的程序计数器,唯一不会出现OutOfMemoryError的。

2、Java栈/虚拟机栈(VM Stack)

方法执行会创建一个栈帧,用于存储局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1)局部变量表:方法中非静态变量以及函数形参。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

2)操作数栈:程序中的所有计算过程都是在借助于操作数栈来完成的。

3)指向运行时常量池的引用:类中的常量,指向运行时常量的引用。

4)异常可能性:对于栈有两种异常情况,如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常

3、本地方法栈(Native Method Stack)

本地方法栈与Java所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法。有的虚拟机直接把本地方法栈和虚拟机栈合二为一

异常可能性:和Java栈一样,可能抛出StackOverflowError和OutOfMemeryError异常

4、Java堆(Heap)

虚拟机启动时创建。存放对象实例以及数组,几乎所有的对象实例都在这里分配内存,垃圾回收器管理的主要区域。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。

异常可能性:如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常

5、方法区(Method Area)

存储类信息(类的名称、方法信息、字段信息)、常量、静态变量、以及编译器编译后的代码等数据。

1)运行时常量池

方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法(将字符串的值放到常量池)。

垃圾收集比较少出现,回收目标主要针对常量池的回收和对类型的卸载。 

异常可能性:当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常

6、直接内存

不是虚拟机运行时数据区的一部分,在NIO类中引入一种基于通道与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

不会受到Java堆大小的限制,会受本机内存大小的限制,所有可能会抛OutOfMemoryError异常。

对象的文件结构

对象的访问定位:

句柄

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改

直接指针

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。(例如HotSpot)

内存分配:

-XX:+PrintGCDetails

-XX:+UseSerialGC

堆上分配

1、优先分配到eden

2、大对象直接分配到老年代

3、长期存活的对象分配到老年代

-XX:MaxTenuringThreshold 默认15

4、空间分配担保

-XX:+HandlePromotionFailure

5、动态对象年龄判断

栈上分配(Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)线程私有的缓存区

逃逸分析

-XX:+DoEscapeAnalysis

如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

逃逸分析研究对于 java 编译器有什么好处呢?我们知道 java 对象总是在堆中被分配的,因此 java 对象的创建和回收对系统的开销是很大的。java 语言被批评的一个地方,也是认为 java 性能慢的一个原因就是 java不支持栈上分配对象。JDK6里的 Swing内存和性能消耗的瓶颈就是由于 GC 来遍历引用树并回收内存的,如果对象的数目比较多,将给 GC 带来较大的压力,也间接得影响了性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。java 中应用里普遍存在一种场景,一般是在方法体内,声明了一个局部变量,并且该变量在方法执行生命周期内未发生逃逸,按照 JVM内存分配机制,首先会在堆内存上创建类的实例(对象),然后将此对象的引用压入调用栈,继续执行,这是 JVM优化前的方式。当然,我们可以采用逃逸分析对 JVM 进行优化。即针对栈的重新分配方式,首先我们需要分析并且找到未逃逸的变量,将该变量类的实例化内存直接在栈里分配,无需进入堆,分配完成之后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量对象也被回收,通过这种方式的优化,与优化前的方案主要区别在于对象的存储介质,优化前是在堆中,而优化后的是在栈中,从而减少了堆中临时对象的分配(较耗时),从而优化性能。

垃圾回收:

哪些内存需要回收?

java堆、方法区的内存

什么时候回收?

1、引用计数器、

2、是否可达(大部分GC使用这个算法)。

引用计数法不能解决对象之间的循环引用,见下例

怎么回收?

1、“标记-清除”(Mark-Sweep)算法

缺点:

(1)、效率问题,标记和清除过程的效率都不高;

(2)、空间问题,标记清除之后会产生大量不连续的内存碎片

2、复制”(Copying)算法

缺点:

(1)、只是这种算法是将内存缩小为原来的一半,有点过于浪费;

(2)、对象存活率较高时就要执行较多的复制操作,效率将会变低;

3、标记-整理”(Mark-Compact)算法

4、分代回收

Young Generation(新生代)、Old Generation(老年代)、Permanent Generation(持久代)

1) 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。

2) 在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。

3) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收Old段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾。

说明:

当新建对象无法放入eden区时,将出发minor collection。JVM采用copying算法,将eden区与from区的可到达对象复制到to区。经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象。随后from区成为新的to区, to区成为新的from区。如果进行minor collection的时候,发现to区放不下,则将部分对象放入成熟世代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟世代。如果成熟世代放满对象,无法移入新的对象,那么将触发major collection(Full回收)。

垃圾回收器:

1、serial 垃圾回收器(使用场景:客户端应用,应用内存不大)

历史最久   单线程

2、parNew垃圾回收器()

多线程

3、parallel Scavenge垃圾回收器

复制算法(只能作用于新生代内存)

多线程

达到可控制的吞吐量【吞吐量 = 代码执行时间 / (代码执行时间+垃圾回收时间)】

客户端:需要响应迅速,所以停顿时间需要短;

服务端:需要高可用,吞吐量要高,更适合parallel 作为垃圾回收器

4、cms垃圾回收器

标记清除算法  作用于老年代

初始标记-->并发标记-->重新标记-->并发清理

优点:并发收集、低停顿

缺点:占用大量CPU资源、无法处理浮动垃圾、出现concurrent mode failure、空间碎片

5、g1垃圾回收器

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。

 

虚拟机工具:

1、jps  (java进程状态)

-l   运行的主类全名,或jar包的名称

-m  运行时传入主类的参数

-v   虚拟机参数

-mlv  以上三个都显示

2、jstat

类装载,内存,垃圾收集,jit编译   的信息

-gcutil 进程ID

3、jinfo

4、jmap

5、jhat

分析堆文件

查看分析结果

6、jstack

查看线程状态(死锁,死循环)

7、jconsole

死锁:两个线程,线程1锁了A资源,需要调用B资源;线程2锁了B资源。需要调用A资源

避免:不要存在锁嵌套,即加了锁的代码中,不要再出现锁

8、visualVM工具

下载安装、插件安装

性能调优:

1、高性能机器,部署一个应用,堆内存配置比较大。大对象太多,老年代触发GC,fullgc停顿时间长

分析:停顿是随机发生,所以排除内存和代码问题,考虑GC问题

解决方案:

负载均衡+部署多个web容器+session共享

每个容器,堆内存改小,GC停顿时间减少

2、机器内存比较少,堆内存比较大,内存溢出出现,但是堆的运行正常。

分析:堆的运行正常,问题出现于NIO

解决方案:

NIO,缓冲区是分配在堆外内存,

如果系统中剩余的内存很小,这块直接内存就                             

分配不了多少。此问题可以关注下byteBuffer。

解决方案是加大内存

3、jvm奔溃(connect reset)

分析:大量请求数据没有处理,积压,导致jvm奔溃

解决方案:

增加消息队列,处理完再去消息队列取

class文件结构:

class文件查看器

javap命令分析class文件

1、魔数

魔数:CA FE BA BE

主版本号:00 00 00 34

次版本号:

(十六进制 34 = 十进制 52)

jdk 1.8=52

jdk 1.7=51

jdk 1.6=50

jdk 1.5=49

......

jdk 1.1=45

2、常量池

2、访问标志符

3、类索引

4、字段表集合

字段表(field_info)用于描述接口或者类中声明的变量.字段包括类级变量以及实例级变量,但是不包括在方法内部声明的局部变量.

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型.

描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值.根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符加L加对象名的全限定名来表示.

5、方法表集合

6、属性表集合

在class文件,字段表,方法表都可以携带自己的属性表集合(像前面方法表的时候就用到"code"这个属性表)以用于描述某些场景专有的信息

 虚拟机中预定义的属性:

属性名称

使用位置

含义

Code

方法表

Java代码编译成的字节码指令

ConstantValue

字段表

final关键字定义的常量池

Deprecated

类,方法,字段表

被声明为deprecated的方法和字段

 Exceptions

方法表 

方法抛出的异常 

 EnclosingMethod

类文件 

仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 

 InnerClass

类文件 

内部类列表 

 LineNumberTable

Code属性 

Java源码的行号与字节码指令的对应关系 

 LocalVariableTable

Code属性 

方法的局部便狼描述 

 StackMapTable

Code属性 

JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 

 Signature

类,方法表,字段表 

 用于支持泛型情况下的方法签名

 SourceFile

类文件 

记录源文件名称 

 SourceDebugExtension

类文件 

用于存储额外的调试信息 

 Synthetic

类,方法表,字段表 

标志方法或字段为编译器自动生成的 

 LocalVariableTypeTable

类 

使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 

 RuntimeVisibleAnnotations

类,方法表,字段表 

为动态注解提供支持 

 RuntimeInvisibleAnnotations

表,方法表,字段表 

用于指明哪些注解是运行时不可见的 

 RuntimeVisibleParameterAnnotation

方法表 

作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法

 RuntimeInvisibleParameterAnnotation 

 方法表

 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数

 AnnotationDefault

 方法表

用于记录注解类元素的默认值 

 BootstrapMethods

类文件 

用于保存invokeddynamic指令引用的引导方式限定符  

属性表定义的结构:  

类型

名称

数量

u2

attribute_name_index

1

u2

attribute_length

1

u1

info

attribute_length

Code属性

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

max_stack

1

u2

max_locals

1

u4

code_length

1

u1

code

code_length

u2

exception_table_length

1

exception_info

exception_table

exception_length

u2

attributes_count

1

attribute_info

attributes

attributes_count

Exceptions属性

类型

名称

数量

u2

attribute_name_index

1

u2

attribute_lrngth

1

u2

attribute_of_exception

1

u2

exception_index_tsble

number_of_exceptions

LineNumberTable属性

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

line_number_table_length

1

line_number_info

line_number_table

line_number_table_length


字节码指令

1、加载与存储指令

    - 将一个局部变量加载到操作栈的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>

    - 将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>

    - 将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

    - 扩充局部变量表的访问索引的指令:wide

    编译器优化:return 1+2;    不会再将1和2出入栈,直接编译时就常量计算。

2、运算指令

    - 加法指令:iadd、ladd、fadd、dadd

    - 减法指令:isub、lsub、fsub、dsub

    - 乘法指令:imul、lmul、fmul、dmul

    - 除法指令:idiv、ldiv、fdiv、ddiv

    - 求余指令:irem、lrem、frem、drem

    - 取反指令:ineg、lneg、fneg、dneg

    - 位移指令:ishl、ishr、iushr、lshl、lshr、lushr

    - 按位或指令:ior、lor

    - 按位与指令:iand、land

    - 按位异或指令:ixor、lxor

    - 局部变量自增指令:iinc

    - 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

3、类型转换指令

    窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。

4、对象创建与操作指令

    - 创建类实例的指令:new

    - 创建数组的指令:newarray,anewarray,multianewarray

    - 访问类字段(static 字段,或者称为类变量)和实例字段(非 static 字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic

    - 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

    - 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

    - 取数组长度的指令:arraylength

    - 检查类实例类型的指令:instanceof、checkcas

5、操作数栈管理指令

    Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap。

6、控制转移指令

    - 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。

    - 复合条件分支:tableswitch、lookupswitch

    - 无条件分支:goto、goto_w、jsr、jsr_w、ret

7、方法调用和返回指令

    - invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。

    - invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

    - invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

    - invokestatic 指令用于调用类方法(static 方法)。

8、抛出异常

    在程序中显式抛出异常的操作会由 athrow 指令实现,除了这种情况,还有别的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。

  try{}catch{} 在运行正常的情况下,不会影响性能

9、同步指令

    同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持。

管程:

虚拟机类加载机制

calss文件 ---> 加载到内存 ---> 验证 ---> 准备 ---> 解析 ---> 初始化

连接:验证、准备、 解析

可以使用懒加载机制

加载和连接是并行的

1、加载

加载源:文件(class、jar)、数据库、网络(applet)、计算生成(动态代理)、由其他文件生成(jsp)

hotspot把class对象放在方法区,其他的可能放在堆中

2、验证

3、准备

类变量分配内存并设置变量的初始值(int为0,boolean为false,引用为null, char为'0'等)。这些变量使用的内存都将在方法区中进行分配

类常量,直接从常量池取出并赋值

局部变量不会给初始值,所以局部变量中必须要给默认值

function(){

int a;

a++;//此处会报错,因为没有初始值

}

4、解析

符号引用:全限定名,存在方法区中

直接引用:直接内存地址

5、初始化

执行构造函数

public class Test {

static {

i=0;

System.out.println(i)

}

static int i = 1;

}

i=0不报错,因为在准备阶段就已经初始化,代码会在打印那行报错,变量的访问,必须在定义代码之后。

static静态代码块会加锁,多线程时只有一个线程能进来

虚拟机字节码运行引擎

1、运行时栈帧结构

(1)局部变量表(定长单位数组):

byte,short,int,long,boolean,char,double,float,reference

不同的类型,占用内存大小不同(slot 32  64)

当int a = 10;没有这一行时,GC不会回收,反之,a的变量会复用buff的空间,所以回收,slot复用导(2)操作数栈(栈)

(3)动态连接

静态连接:符号引用转换直接引用

动态连接:栈帧中有块区域,指向所属的方法

(4)方法返回地址

(5)附加信息

虚拟机规范中没有的信息

方法调用

1、解析调用(编译期间就可确定,不能被继承的方法)

静态方法、构造方法,私有方法,final修饰的方法

2、静态分派调用(编译期间不确定)重载

多态情况下 Parent p = new Child();

运行期间确定是child类型

最匹配的参数:传入char,  char-->int-->long-->object-->char...

3、动态分派调用(编译期间不确定)重写

动态类型语言支持

happens before

1、

A、B可以重排序,A、C不能

2、监视器锁(同步)规则:对于一个监视器的解锁,happens-before于随后对这个监视器的加锁

3、volatile

写volatile变量时,会刷新主内存的值

读volatile变量时,会把当前线程的本地变量置为无效,重新读取主内存

4、传递性

5、start规则:A线程调用B线程,A  happens before B

6、join规则:

重排序(提高运行效率)

1、原则(数据依赖性):as-if-serail

(写后读、读后写、写后写)

int a=1;

int b=2;

int c=a;

2、指令重排序分类:编译器重排序、处理器重排序

3、影响

单线程:提高了运行性能

多线程:提高了运行性能外,有如下弊端,reader中拿到a的值不一定是1,重排序后可能先执行 flag=true;

4、解决带来的影响:竞争与同步

5、锁的内存语义

6、final 的内存语义

对于final 域,编译器和处理器要遵守两个重排序规则:

1、在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2、初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

public class FinalExample {     int i;                            //普通变量     final int j;                      //final变量     static FinalExample obj;      public void FinalExample () {     //构造函数         i = 1;                        //写普通域         j = 2;                        //写final域     }      public static void writer () {    //写线程A执行         obj = new FinalExample ();     }      public static void reader () {       //读线程B执行         FinalExample object = obj;       //读对象引用         int a = object.i;                //读普通域         int b = object.j;                //读final域     } } 

非标准理解就是:

1.对象构造函数内有final域,必须先用构造函数构造对象,再把对象赋给其他引用

2.如果对象有final域,必须先读对象的引用,再读final域

写 final 域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。

 

final引用从构造函数中“溢出”

public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj;  public FinalReferenceEscapeExample () {     i = 1;                              //1写final域     obj = this;                          //2 this引用在此“逸出”}public static void writer() {     new FinalReferenceEscapeExample (); }  public static void reader {     if (obj != null) {                     //3         int temp = obj.i;                 //4     } } } 

假设一个线程 A 执行 writer() 方法,另一个线程 B 执行 reader() 方法。这里的操作2使得对象还未完成构造前就为线程 B 可见。即使这里的操作 2 是构造函数的最后一步,且即使在程序中操作 2 排在操作 1 后面,执行 read() 方法的线程仍然可能无法看到 final 域被初始化后的值,因为这里的操作 1 和操作 2 之间可能被重排序。实际的执行时序可能如下图所示:

从上图我们可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到 final 域正确初始化之后的值。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值