第一部分,走进Java
第二部分,自动内存管理机制:
第二章:Java内存区域与内存溢出异常
一,Java中,虚拟机自动管理内存机制,
不在需要为每一个new操作去写配对的delete和free操作,不容易出现内存泄漏和内存溢出的问题。但是不好排除。
二,运行时数据区域:
Java虚拟机在执行java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。以及创建和销毁的时间。
1)程序计数器:
一块较小的内存空间,看做线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复就是依赖程序计数器完成。
- Java虚拟机的多线程就是通过线程轮流分配处理器执行时间的方式来实现的。为了确定线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各个线程的计数器之间互不影响。独立存储。称线程私有的内存。
- 如果在执行Java方法,计数器记录字节码指令的地址,如果执行Native方法,计数器为空。
2)Java虚拟机栈:
描述Java方法执行的内存模型,与计数器一般,java虚拟机栈也是线程私有的,它的生命周期与线程相同,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链表,方法出口等信息。每个方法从调用到执行完成过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
- 局部变量表:存储了编译期可知的各种基本数据类型,对象的引用(reference类型,即指向对象起始地址的引用指针)和returnAddress类型(指向一条字节码指令的地址)。long与double类型数据会占用2个局部变量空间,其余的数据类型会占用一个,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,方法在帧中分配的局部变量空间是完全确定的。在方法运行期不会改变局部变量表的大小。
- 在Java虚拟机规范中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3)本地方法栈:
与虚拟机类似,区别是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。也会抛出StackOverflowError异常,OutOfMemoryError异常
4)Java堆:
java 堆是虚拟机所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一作用是用来存放对象实例,几乎所有的对象实例以及数组都要在堆上分配地址。
- java堆是垃圾收集器管理的主要区域。也被称为GC堆。
- Java堆可以处于物理上不连续的内存空间,只用逻辑上连续即可,可以通过-Xmx和Xms控制。
- 如果在堆中没有内存完成实例分配,并且堆中也无法扩展,会抛出OutOfMenory异常。
5)方法区:
方法区与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态常量,即编译器编译后的代码数据等。也称为永久代,
6)运行时常量池:
是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 运行时常量池对于Class文件常量池的一个特征是具备动态性,Java语言不要求常量一定是在编译期才产生,即并非预置入Class文件中的常量池的内容可以才能进入方法区运行时常量池,运行期也可以将新的常量放入池中,比如String的intern()方法。当常量池无法 在申请内存时会抛出OutOfMethodError异常。
直接内存:
三,HotSpot虚拟机对象探秘:
1)对象的创建:
- 虚拟机遇到一条new指令后,首先检查指令的参数是否在常量池中定位到一个类的符号引用,并检查符号引用代表的类是否已被加载,解析和初始化。类加载检查之后虚拟机会分配内存,即把一块确定大小的内存从Java堆中划分出来,当堆为绝对规整,使用指针碰撞的方式,当堆不是规整的,需要维护一个列表记录内存,称为空闲列表。Java堆是否规整由垃圾收集器是否有压缩整理决定。
- 在创建对象的并发考虑上,一种是分配内存空间动作的同时进行同步处理,采用CAS配上失败重试的方法保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)
- 内存分配完之后,会初始化内存空间为零值,如果使用TLAB,会在TLAB中完成,这一操作保证了对象实例字段在Java中不赋初值就可以使用,
- 之后要对对象进行必要的设置,即是哪个类的实例,元信息,对象的哈希码等信息。这些信息存放在对象头中。
- 执行初始化方法。
2)对象的内存布局:
在HotSpot虚拟机中,对象在内存中的存储分为三部分:对象头,实例数据,对齐填充。
- 对象头包括两部分,其一为存储对象自身的运行时数据,哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。其二为类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,如果对象是一个数组,在对象头中会有一块记录数组长度的数据。
- 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,存储顺序会受到虚拟机分配策略参数和字段在Java源码中的定义顺序决定。即相同宽度的字段总是会被分配到一起,父类字段在子类前。
- 对齐填充并不是必然的,也没有特别的含义,仅仅起着占位符的作用。
3)对象的访问定位:
Java程序通过栈上的reference数据来操作堆上的具体对象,访问方式有使用句柄和直接指针两种。
- 句柄方式,会划分一块内存作为句柄池,引用中存储对象的句柄地址,
- 使用直接指针方式访问,那么Java堆对象的布局考虑如何放置类型数据。使用指针速度更快,
四,实战,OutOfMemoryError异常:
虚拟机参数设置
1)Java堆溢出:
只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制。在对象到达最大堆的容量后就会产生内存溢出异常。
设置虚拟机参数;
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
//限制Java堆的大小为20MB,不可扩展,将堆的最小值与-Xms参数与最大堆-Xmx参数设置为一样即可以避免自动扩展。
//参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆储快照
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/13 10:03
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
运行结果:
/*
java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid13048.hprof ...
Heap dump file created [29202866 bytes in 0.128 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3204)
at java.util.Arrays.copyOf(Arrays.java:3175)
at java.util.ArrayList.grow(ArrayList.java:246)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:220)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:212)
at java.util.ArrayList.add(ArrayList.java:443)
at com.company.HeapOOM.main(HeapOOM.java:19)
*/
如果为内存泄漏:通过工具查看泄漏对象的到GC Roots的引用链。如果不存在泄漏就调整虚拟机参数。
2)虚拟机栈和本地方法栈溢出:
HotSpot虚拟机中并不区分本地方法栈和虚拟机栈, -Xoss可以设置本地方法栈的大小,栈容量由-Xss设置。关于虚拟机栈的异常,,描述为:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
- 如果虚拟机在扩展栈时无法申请到足够的内存,则抛出OutOfMemoryError异常
使用-Xss参数设置减少栈内存容量,抛出StackOverfiowError异常,堆栈深度相应缩小。
定义了大量的本地变量,增加方法帧中局部变量表中的长度,抛出StackOverfiowError异常,堆栈深度相应缩小。
设置虚拟机参数:
-Xss128k
//设置堆栈内存为128k
代码:
package com.company;
/**
* @Author: Liruilong
* @Date: 2019/7/13 18:00
* @VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeng(){
stackLength++;
stackLeng();
}
public static void main(String[] args)throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeng();
}catch (Throwable throwable){
System.out.println("sstack length:"+oom.stackLength);
throw throwable;
}
}
}
运行结果:
/*
sstack length:1001
Exception in thread "main" java.lang.StackOverflowError
at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:13)
at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:14)
………………
*/
单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配内存时,都会抛出StackOverflowError异常。
虚拟机提供参数来控制Java堆和方法区这两部分内存的最大值。减去最大堆Xmx,在减去最大方法区MaxPermSize,程序计数器可以忽略,如果虚拟机本身的内存忽略不计,剩下的内存就由虚拟机和本地方法栈瓜分了。
创建多线程导致内存溢出。
package com.company;
/**
* @Author: Liruilong
* @Date: 2019/7/13 18:00
* @VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private void dontStop(){
while (true){
}
}
public void stackLeaakByThread(){
while (true){
Thread thread = new Thread(() -> {dontStop();});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
oom.stackLeaakByThread();
}
}
//嗯,没有运行出来!!!
3)方法区和运行时常量池溢出
String.intern()是一个Native方法,当字符串对象已经包含一个String的字符串引用时,则返回字符串的引用,反之,将字符串添加到常量池中,并发挥Sting对象的引用。
在JDK1.6之前的版本,由于常量池分配永久代中,所以可以通过限制方法区的大小来限制常量池容量。
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/14 7:05
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持常量池的引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
int i = 0;
while (true){
list.add(String.valueOf(i++).intern());
}
}
}
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/14 7:05
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimConstantPoolOOM {
public static void main(String[] args) {
String str = "Java";
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("Ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
这段代码在JDK1.6中执行,会返回两个 false,在JDK1.7及更高版本中,返回 true,false。
1.6中intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。
而由StringBuilder创建的字符串实例在Java堆中,所以必然是不是同一个引用,返回 false 。
而jdk1.7中inter方法不再复制实例,只是在常量池中记录首次出现的实例的引用,
因此inter()返回的引用和StringBuilder创建的那个字符串是同一个引用,所以返回 true
返回 false 的原因是Java在执行str2.intern()时已经出现了不符合“首次出现的原则”,而计算机软件是首次出现的。
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。
4)本机直接内存溢出:
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定。
第三章:垃圾收集器与内存分配策略
一,概述:
1)程序计数器,虚拟机栈,本地方法栈这些随着线程而生,随线程而灭,栈中的栈帧随方法的进栈退栈而存在,这些私有内存不考虑垃圾回收。
2)Java堆和方法区的内存的回收和分配都是动态的 ,垃圾收集关注的也是这部分内容。
二,对象以死了吗:
1)引用计数算法:
给对象添加一个引用计数器,每当有一个地方引用他时,计数器就加1,当引用失效时,计数器就减1,任何时对象引用计数器为0则回收。Java没有使用引用计数器算法来实现管理内存。他很难解决对象之间的循环调用问题。
2)可达性分析算法:
在主流的商用语言中都采用可达性分析算法来判断对象是否存活,基本思想是通过称为GC Roots的对象作为起点,从该节点开始向下搜索,搜索走过的路劲称为引用链,当一个对象到GC Roots没有任何引用链相连接的时候(即从GC Roots到这个对象不可达),即对象是不可用的。才被判定为可回收对象。
3)在Java语言中,可做为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象,
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(native方法)引用的方法。
4)在谈引用:
在JDK1.2前,引用的定义为:如果reference类型的数据中存储的数值代表了另一块内存的其实地址,就称这块内存代表一个引用。
在JDK1.2后,分为强引用,软引用,弱引用,虚引用四种。
5)生存还是死亡:
如果对象在进行可达性分析之后发现没有GC Roots相连接的引用链,那将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,都视为没有必要执行。
如果被判定为有必要执行finalize(),那么对象将会放置一个叫F-Query的队列中,并在稍后由虚拟机自动建立Finalize线程去执行。
6)回收方法区:
在堆中,尤其是在新生代中,常规的应用进行一次垃圾收集一般可以回收70%-80%的空间,而永久代垃圾回收很低。
永久代的垃圾回收主要是两部分内容,废弃常量和无用的类,
回收废弃常量与回收Java堆中的对象非常类似。
以常量池中字面量的回收为例,当系统中没有任何一个String对象的引用常量池中的常量时,也没有其他地方引用这个字面量时,发生内存回收的话会被回收掉。
回收无用的类的条件:
- 该类所有的实例对象都已经被回收,即Java堆中不存在该类的任何实例,
- 加载该类的ClassLoader以经被回收。
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的的方法。
三,垃圾收集算法:
1)标记-清除算法:
最基础的收集算法,算法分为两个阶段,首先标记出所需要回收的对象,在标记完成后统一回收所标记的对象,
缺点:效率不高,空间问题,标记清除会产生大量不连续的内存碎片,可能当需要分配较大对象的内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2)复制算法:
为了解决效率问题,将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。即不用考虑内存碎片问题,缺点为实际使用内存变为原来的一半。一般用来回收新生代。使用额外空间对它进行分配担保。
复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低,一般老年代不使用这种算法。
3)标记-整理算法:
根据老年代的特点,在标记清除算法的基础上不是直接对可回收对象进行清理。而是让所有存活的对象都向一端移动然后直接清理掉边界以外的内存。
4)分代收集算法:
根据对象的存活周期的不同将内存分为几块,即把Java堆内存分为新生代和老年代,然后根据各个年代的特点选择合适的收集算法,在新生代中,每次都有大量的对象死去,少量存活,采用复制算法,在老年代中,对象的存活率高,没有额外空间进行分配担保必须使用,就必须使用“标记清除法”和“标记整理法”进行回收。
四,HotSpot的算法实现:
这部分内容以后看,98~183
1)枚举根节点
2)安全点
3)安全区域
五,垃圾收集器:
六,内存分配与回收策略:
第四章:虚拟机性能监控与故障处理工具
一,概述:
二,jdk命令行工具
三,jdk可视化工具
第五章:调优案例分析与实战
一,概述
二,案例分析
三,实战
第三部分,虚拟机执行子系统
第六章,类文件结构
Class文件中包含了java虚拟机 指令集和符号表以及若干其他的辅助信息。任何一个Class文件都对应唯一一个类或接口的定义信息。但类或接口不一定要定义到文件里。
根据Java虚拟机规范,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,有两种数据类型,无符号数和表。
无符号数:基本的数据类型,以一,u1,u2,u4,u8来表示1,2,4,8个字节的无符号数,无符号数可以用来描述数字,索引引用,数值量,或者按照utf-8的编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构的复合数据类型,所有表习惯以info结尾。描述有层次关系的复合数据结构。
第七章,虚拟机类加载机制
一,概述
虚拟机类加载机制:虚拟机把描述类的数据从Class文件加载到文件,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
在Java语言中,类型的加载,连接和初始化过程都是在程序运行期间完成的。Java天生可以动态扩展的特性就是依赖运行期动态加载和动态连接特点实现的。
二,类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:
加载阶段(加载class文件)与连接阶段(将二进制数据合并到JRE中)的部分内容是交叉进行的。
什么情况下开始类加载过程的
第一阶段:Java虚拟机闭并没有强制约束。
对初始化阶段,虚拟机严格规范了五种情况必须立即对类进行“初始化”。
有些只有这5中场景中的行为被称为主动引用。所有引用方式都不会触发初始化,称为被动引用。
- 遇到new (使用new关键字实例化对象),getstatic(读取或设置一个类的静态字段,被final修饰的,已经在编译期把结果放入常量池的静态字段除外),putstatic,或invokestatic(调用一个类的静态方法时)这四条字节码指令时,如果类没有进行类初始化,则需要进行类初始化。
- 使用Java.lang.reflect包的方法对类进行反射调用的时候,如果没有类被初始化需要进初始化。
- 当初始化一个类的时候,如果发现父类没有被初始化,需要初始化父类
- 当虚拟机启动时,用户指定一个要执行的类的主类,虚拟机会先初始化主类。
- 使用jkd1.7中动态语言的支持时,如果一个Java.lang.invoke.MethodHandle实例最后的解析结果是REF——getStatic^_^ .... 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发初始化。
package com.liruilong.jvmdemo;
/**
* @Description : 虚拟机代码
* 被动使用类字段演示一
* 通过子类引用父类的静态字段,不会导致子类初始化
* @Author: Liruilong
* @Date: 2019/8/21 14:14
*/
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Author: Liruilong
* @Date: 2019/8/21 14:24
*/
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Author: Liruilong
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
System.out.println(SubClass.value);
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会处触发子类的 初始化。
package com.liruilong.jvmdemo;
/**
* @Description : 被动使用类字段实例二
* 通过数组定义引用类,不会触发此类的初始化。
* @Author: Liruilong
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
// System.out.println(SubClass.value);
SuperClass[] superClasses = new SuperClass[10];
}
}
通过数组定义引用类,不会触发此类的初始化。
package com.liruilong.jvmdemo;
/**
* @Description :被动使用类字段三,
* 常量在编译阶段会存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
* @Author: Liruilong
* @Date: 2019/8/21 15:00
*/
public class CinstClass {
static {
System.out.println("CinstClass init ");
}
public static final String LIRUILPNG = "liruilong";
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
System.out.println(CinstClass.LIRUILPNG);
}
}
当一个类被初始化时,要求其父类全部都已经初始化过了,但是一个接口中初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口才会初始化。
三,类加载的过程
1)加载(是类加载的第一阶段)
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 1,从本地文件系统或jar包中加载.class文件,大部分文件常用。
2,通过网络加载.class文件。
3,从java源代码文件动态编译成.class 文件,执行时加载。动态代理技术,在Java.lang.reflect.Proxy中,用ProxyGenerator.generateProxyClass来为特定的接口生成形式为“*$Prosy”的代理的二进制文件。
- 1,从本地文件系统或jar包中加载.class文件,大部分文件常用。
- 将这个字节流所代表的静态存储结构(静态常量)转换为方法区的运行时数据结构
- 内存中生成java.lang.Class对象。
在加载阶段可以使用系统提供的引导类加载器完成,也可以使用用户自定义的类加载器完成。
数组类本身不通过类加载器去创建,而是由Java虚拟机直接创建,但数组类的元素类型最终要靠类加载器完成。
一个数组类创建过程遵循原则:
- 如果数组的最终类型是引用类型,那就递归采用加载过程去加载。
- 如果数组的组件不是引用类型,Java虚拟机会将数组标记为与引导类加载器。
- 数组类的可见性与组件类型相同,如果不是引用类型,可见性为public。
2)验证:
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机的自身安全。验证阶段:
- 文件格式验证
- 是否以魔数开头
- 主次版本号是否在当前虚拟机处理范围内。
- 常量池的常量类型
- 指向常量的索引值
- Class文件是否有被删除和附加消息。
- CONSTANT_Utf8_info型常量是否符合utf8编码。
- 元数据验证
- 类是否有父类
- 类是否继承了不允许被继承的类
- 如果这个类不是抽象类,是否实现了所有方法。
- 类中的字段…………
- 字节码验证
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
- 保证跳转指令不会跳转到方法体外的字节码指令。
- 保证方法体中的类型转换是有效的。
- 符号引用验证
- 符号引用中通过字符串描述的权限定名能否找到相应的类,
- 在指定的类中是否存在符合方法的字段描述已及简单名称所描述的方法和字段
- 符号引用中的类,字段,方法的访问性是否可以被当前类访问。
3)准备:
- 准备阶段为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中分配。
- 这个时候内存分配仅包括类变量(static),而不包括实例变量,这里的初始值是数据类型的默认值,
- 即 public static int values = 123 ;那么变量在准备阶段的值为0,不是123;具体的赋值在初始化阶段完成。
- 如果为 public static final int value = 123 ,那么在准备阶段就会被赋初值。即values的值为123.
4)解析:
- 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
- 符号引用:以一组符号来描述所引用的目标,引用的目标并不一定加载到内存中
- 直接引用:直接引用可以直接指向目标的指针,相对偏移量或一个能间接定位到目标的句柄。如果有个直接引用,那么引用的目标一定加载到了内存中。
- 解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符。
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
5)初始化:
- 类初始化是类加载过程的最后一步,初始化是执行类构造器<clinit>()方法的过程,
- <clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作的和静态语句块的中的语句合并产生的。编译器收集的顺序是由语句中源文件中出现的,
- 静态语句块只能访问到定义与静态语句块之前的变量,定义在他之后的变量,在前面的语句可以赋值,不能访问。
pubilce class Test{ static{ int i = 0; //给变量赋值可以正常编译通过 System.out.println(i ); // 这个话会提示非法先前引用 } static int i =0; }
- <clinit>()方法与类的构造函数不同,或者说与类的构造器<init>不同,它不需要显示的调用父类的构造器,虚拟机会保证在父类执行的时候子类会执行。即虚拟机中第一个被执行的<clinit>()方法的类为java.lang.Object.
- 由于父类的<cinlt>方法先执行,也意味着父类中定义的静态语句块要优于子类的变量赋值操作。
// 父类中定义的静态预计块要优于子类的变量复制操作。 输出2 而不是1 static class Parent { public static int A = 1; static{ A = 2; } } static class Sub extends Parent{ public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }
- 如果一个类中没有静态语句块,也没有对变量的赋值,那么编译器可以为这个类生成<cinlt>()方法。
- 虚拟机会保证一个类的<clinit>()方法中多线程环境下被正确的加锁,同步。
四,类加载器
1)类与类的加载器
2)双亲引用类型
3)破坏双亲引用类型
第八章,虚拟机字节码执行引擎
一,概述
二,运行时栈帧结构
1)局部变量表
2)操作数栈
3)动态连接
4)方法返回地址
5)附加信息
三,方法调用
1)解析
2)分派
3)动态类型语言支持
四,基与栈的字节码解释执行引擎
1)解释执行
2)基于栈的指令集与基于寄存器的指令集
3)基于栈的解释器执行过程。
第九章,类加载及执行子系统的案例与实战
第四部分,程序编译与代码优化
第五部分,高效并发
嗯,感觉剩下的部分有点深,下学期要找工作,不打算看了,嗯,以后有时间看!!
2019.7.22