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
1、程序计数器(PC、Program Counter Register)
Java方法,记录正在执行的虚拟机字节码指令的地址;当执行的是Native方法,这个计数器值为空。
每条线程都会有一个独立的程序计数器,唯一不会出现OutOfMemoryError的。
方法执行会创建一个栈帧,用于存储局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
1)局部变量表:方法中非静态变量以及函数形参。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
2)操作数栈:程序中的所有计算过程都是在借助于操作数栈来完成的。
3)指向运行时常量池的引用:类中的常量,指向运行时常量的引用。
4)异常可能性:对于栈有两种异常情况,如果线程请求的栈深度大于栈所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态拓展,在拓展的时无法申请到足够的内存,将会抛出OutOfMemoryError异常
本地方法栈与Java栈所发挥的作用是非常相似的,它们之间的区别不过是Java栈执行Java方法,本地方法栈执行的是本地方法。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
异常可能性:和Java栈一样,可能抛出StackOverflowError和OutOfMemeryError异常
虚拟机启动时创建。存放对象实例以及数组,几乎所有的对象实例都在这里分配内存,垃圾回收器管理的主要区域。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。
异常可能性:如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemeryError异常
存储类信息(类的名称、方法信息、字段信息)、常量、静态变量、以及编译器编译后的代码等数据。
方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法(将字符串的值放到常量池)。
垃圾收集比较少出现,回收目标主要针对常量池的回收和对类型的卸载。
异常可能性:当方法区无法满足内存分配需求时,将抛出OutOfMemeryError异常
不是虚拟机运行时数据区的一部分,在NIO类中引入一种基于通道与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
不会受到Java堆大小的限制,会受本机内存大小的限制,所有可能会抛OutOfMemoryError异常。
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。(例如HotSpot)
栈上分配(Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)线程私有的缓存区)
如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
逃逸分析研究对于 java 编译器有什么好处呢?我们知道 java 对象总是在堆中被分配的,因此 java 对象的创建和回收对系统的开销是很大的。java 语言被批评的一个地方,也是认为 java 性能慢的一个原因就是 java不支持栈上分配对象。JDK6里的 Swing内存和性能消耗的瓶颈就是由于 GC 来遍历引用树并回收内存的,如果对象的数目比较多,将给 GC 带来较大的压力,也间接得影响了性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。java 中应用里普遍存在一种场景,一般是在方法体内,声明了一个局部变量,并且该变量在方法执行生命周期内未发生逃逸,按照 JVM内存分配机制,首先会在堆内存上创建类的实例(对象),然后将此对象的引用压入调用栈,继续执行,这是 JVM优化前的方式。当然,我们可以采用逃逸分析对 JVM 进行优化。即针对栈的重新分配方式,首先我们需要分析并且找到未逃逸的变量,将该变量类的实例化内存直接在栈里分配,无需进入堆,分配完成之后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量对象也被回收,通过这种方式的优化,与优化前的方案主要区别在于对象的存储介质,优化前是在堆中,而优化后的是在栈中,从而减少了堆中临时对象的分配(较耗时),从而优化性能。
(1)、只是这种算法是将内存缩小为原来的一半,有点过于浪费;
(2)、对象存活率较高时就要执行较多的复制操作,效率将会变低;
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 垃圾回收器(使用场景:客户端应用,应用内存不大)
达到可控制的吞吐量【吞吐量 = 代码执行时间 / (代码执行时间+垃圾回收时间)】
服务端:需要高可用,吞吐量要高,更适合parallel 作为垃圾回收器
缺点:占用大量CPU资源、无法处理浮动垃圾、出现concurrent mode failure、空间碎片
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。
死锁:两个线程,线程1锁了A资源,需要调用B资源;线程2锁了B资源。需要调用A资源
1、高性能机器,部署一个应用,堆内存配置比较大。大对象太多,老年代触发GC,fullgc停顿时间长
2、机器内存比较少,堆内存比较大,内存溢出出现,但是堆的运行正常。
字段表(field_info)用于描述接口或者类中声明的变量.字段包括类级变量以及实例级变量,但是不包括在方法内部声明的局部变量.
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型.
描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值.根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符加L加对象名的全限定名来表示.
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 |
- 将一个局部变量加载到操作栈的指令包括有: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>
编译器优化:return 1+2; 不会再将1和2出入栈,直接编译时就常量计算。
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。
- 创建数组的指令:newarray,anewarray,multianewarray
- 访问类字段(static 字段,或者称为类变量)和实例字段(非 static 字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
- 检查类实例类型的指令:instanceof、checkcas
Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap。
- 条件分支: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
- invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
- invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic 指令用于调用类方法(static 方法)。
在程序中显式抛出异常的操作会由 athrow 指令实现,除了这种情况,还有别的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。
同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持。
calss文件 ---> 加载到内存 ---> 验证 ---> 准备 ---> 解析 ---> 初始化
加载源:文件(class、jar)、数据库、网络(applet)、计算生成(动态代理)、由其他文件生成(jsp)
hotspot把class对象放在方法区,其他的可能放在堆中
为类变量分配内存并设置变量的初始值(int为0,boolean为false,引用为null, char为'0'等)。这些变量使用的内存都将在方法区中进行分配
i=0不报错,因为在准备阶段就已经初始化,代码会在打印那行报错,变量的访问,必须在定义代码之后。
byte,short,int,long,boolean,char,double,float,reference
当int a = 10;没有这一行时,GC不会回收,反之,a的变量会复用buff的空间,所以回收,slot复用导(2)操作数栈(栈)
最匹配的参数:传入char, char-->int-->long-->object-->char...
2、监视器锁(同步)规则:对于一个监视器的解锁,happens-before于随后对这个监视器的加锁
读volatile变量时,会把当前线程的本地变量置为无效,重新读取主内存
5、start规则:A线程调用B线程,A happens before B
多线程:提高了运行性能外,有如下弊端,reader中拿到a的值不一定是1,重排序后可能先执行 flag=true;
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 域已经在构造函数中被正确初始化过了。
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 域正确初始化之后的值。