《深入理解Java虚拟机》知识点总结

一、走进Java

Java技术体系包括虚拟机、Java API、Java编程语言、第三方Java框架。

在虚拟机层面隐藏了底层技术的复杂性以及机器与操作系统的差异性。

Java程序设计语言、Java虚拟机、Java API类库统称为JDK。

二、Java内存区域与内存溢出异常

Java运行时的数据区域

线程共享:方法区、堆

线程隔离:虚拟机栈、本地方法栈、程序计数器

程序计数器是当前线程所执行的字节码的行号计数器,jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。

Java虚拟机栈的生命周期与线程相同,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存储了编译器可知的各种基本数据类型、对象引用和returnAddress类型,所需的内存空间在编译期完成分配,在方法运行期间不会改变。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemeryError异常。

本地方法栈与虚拟机栈类似,为jvm使用到的native方法服务,有的虚拟机例如Sun HotSpot把它们合二为一了。

Java堆是被所有线程共享的一块内存区域,是jvm管理的内存中最大的一块,在虚拟机启动时创建。几乎所有的对象实例以及数组都要在堆分配内存。Java堆是垃圾收集器管理的主要区域,可分为新生代和老年代,再具体的分法是Eden空间、From Survivor空间、To Survivor空间。如果在堆中没有足够内存完成分配实例,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,描述为堆的一个逻辑部分,称为非堆,本质上与永久代不等价。(JDK8有所改变

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,运行期间也可能将新的常量放入池中,比如String类的intern方法。

直接内存,分配堆外内存、例如NIO。

对象的创建:参数定位、分配内存(指针碰撞、空闲列表)、初始化为零值、init。

对象的内存布局:对象头、实例数据、对齐填充。

将对内存的最小值-Xms参数与最大值-Xmx参数设置为一样可以避免堆自动扩展。

通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照以便事后分析。

内存泄漏:应该是需要回收的对象没有被回收

三、垃圾收集器与内存分配策略

更详细内容请查看:https://blog.csdn.net/qq_31142553/article/details/81295331

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地进行出栈和入栈操作;每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。

引用计数算法:很难解决对象之间相互循环引用的问题。

可达性分析算法:当一个对象到GC Roots没有任何引用链相连,证明此对象不可用。

可作为GC Roots的对象有:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的Native方法)引用的对象。

引用可分为强引用、软引用、弱引用、虚引用。

回收方法区:可以不要求虚拟机在方法区实现垃圾回收。

判断类无用的条件:1、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2、加载该类的ClassLoader已经被回收。3、该类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法,此外,还需要jvm进行参数控制是否开启。

标记-清除算法:标记和清除的效率都不高;标记清楚之后产生大量不连续的内存碎片。

复制算法:一块较大的Eden和两块小的Suivivor。

标记-整理算法:将存活的移到一边,清理另一边。

分代收集算法:根据对象存活周期的不同将内存划分为几块,比如新生代和老年代。

在HotSpot的实现中,使用一组OopMap的数据结构来保存哪些地方存放着对象引用。

Serial收集器:单线程、执行时停止一切。

ParNew收集器:是Serial版的多线程版本,目前只有它能与CMS收集器配合工作。

Parallel Old收集器:吞吐量优先。

Serial Old收集器:

Parallel Old收集器:

CMS收集器:以获取最短回收停顿时间为目标,步骤包括初始标记、并发标记、重新标记、并发回收,并发收集、低停顿,缺点1、对CPU资源敏感;2、无法处理浮动垃圾;3、产生大量空间碎片。

GI收集器:JDK7,取代CMS,优点有并行与并发、分代收集、空间整合、可预测的停顿,步骤包括初始标记、并发标记、最终标记、筛选回收,运行日志、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)、异常堆栈等定位问题的数据。

四、虚拟机性能监控与故障处理工具

jdk的命令行工具

jps:虚拟机进程状况工具

jstat:虚拟机统计信息监视工具

jinfo:Java配置信息工具

jmap:Java内存映射工具

jhat:虚拟机堆转储快照分析工具

jstack:Java堆栈跟踪工具,Thread.getAllStackTraces

HSDIS:jit生成代码反编译

jdk的可视化工具

JConsole:Java监视与管理控制台

VisualVM:多合一故障处理工具

五、类文件结构

商业机构与开源机构已经在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Clojure、Groovy、JRuby、Jython、Scala等。

任何一个Class文件都对立着唯一一个类或接口的定义信息。

表是由多个无符号数或者其它表作为数据项构成的复合数据类型。

高版本的jdk能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使格式不变。

常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其它项目关联最多的数据类型。

分析Class文件字节码的工具:javap。

字节码指令:字节码与数据类型、加载和存储指令、运算指令、类型转换指令、对象访问与创建指令、操作数栈管理指令、控制转移指令、方法调用与返回指令、异常处理指令、同步指令。

六、虚拟机类加载机制

Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现。

类的生命周期:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备、解析3个部分统称为连接。加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的。

类初始化的触发条件:

1、遇到new、getstatic、putstatic、invokestatic这4条指令且类没有进行过初始化。

2、使用java.lang.reflect包的方法对类进行反射调用且类没有进行过初始化的时候。

3、初始化一个类前,先触发其父类的初始化。

4、虚拟机启动时指定要执行的主类先初始化。

5、当调用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF-getStatic、REF-putStatic、REF-invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于静态字段,只有直接定义这个字段的类才会被初始化。

通过数组定义来引用类,不会触发此类的初始化。

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载的过程

在加载阶段要完成的3件事情:1、通过一个类的全限定名来获取定义此类的二进制字节流;2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass方法)。

一个类必须与类加载器一起确定唯一性。

内存中实例化的java.lang.Class对象没有明确规定存放在堆中。

加载阶段与连接阶段的部分内容是交叉进行的,但开始时间保持固定的先后顺序。

验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致完成4个阶段的校验动作:1、文件格式校验,例如魔数、版本号;2、元数据验证,例如子类是否覆盖了父类的final字段;3、字节码验证,例如保证方法体中的类型转换是有效的;4、符号引用验证,确保解析动作能正常执行。

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。

通常情况下初始值是零值,在初始化阶段才会设为指定的值。但是,常量(static final)的初始化值被设为属性指定的值。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。

解析功能主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

类方法和接口方法符号引用的常量类型是分开的,如果在类方法表中发现class-index中索引的c是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

java.lang.IllegalAccessError异常一般是访问权限。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化变量和其它资源。或者说初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。

虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。

父类中定义的静态语句块要优先于子类的变量赋值操作。

<clinit>()方法对于类或接口来说并不是必须的。

接口中不能使用静态语句块,只有当父接口中定义的变量使用时,父接口才会初始化。

虚拟机会保证一个类的<clinit>()方法在多线程环境下正确地加锁、同步。

同一个类加载器下,一个类型只会初始化一次。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

启动类加载器Bootstrap ClassLoader:如果不指定引导类加载器,则使用此null代替即可。

扩展类加载器Extension ClassLoader。

启动程序类加载器Application ClassLoader,也称系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

七、虚拟机字节码执行引擎

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序便以为Class文件时,就在方法的code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

不使用的对象应手动赋值为null:占用大量内存且不再使用的变量,促使及时GC。从编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。

在方法执行的任何时候,操作数栈的深度都不会超过在max_locals数据项中设定的最大值。

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

方法执行后有两种方式可以退出这个方法,第一种是执行引擎遇到任意一个方法返回的字节码指令,称为正常完成出口;另一种是遇到了没有得到处理的异常,称为异常完成出口。

方法调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析:编译器可知、运行期不可变,主要包括静态方法和私有方法、实例构造器、父类方法、final方法。

在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

变长参数的重载优先级是最低的。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分配,典型应用是方法重载。

动态分派和重写有很密切的关系。

只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还是编译执行才会比较确切。

Java编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。

Java编译器输出的指令流,基本上是一种基于栈的指令集框架,指令流中的指令大部分都是零地址指令。

八、其它

JDK1.6之后提供了Compiler API,可以动态地编译Java程序。

编译过程大致可以分为3个过程:

1、解析与填充符号表过程。

2、插入式注解处理器的注解处理过程。

3、分析与字节码生成过程。

词法分析是将源代码的字符流转变为标记集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素。

关键字、变量名、字面量、运算符都可以成为标记。

在运行时,虚拟机把热点代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器JIT。

逃逸分析的基本行为就是分析对象动态作用域:方法逃逸和线程逃逸。

变量优化方法:栈上分配、同步消除、标量替换。

每秒事务处理数TPS,代表一秒内服务器平均能响应的请求总数。

volatile:1、保证此变量对所有线程的可见性;2、禁止指令重排序。

另外两个关键字实现可见性:synchronized和final。

synchronized可以实现原子性、可见性、有序性。

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

线程安全的实现方法:

1、互斥同步

synchronized关键字。

ReentrantLock高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件。

2、非阻塞同步:CAS(比较并交换)

3、无同步方案:可重入代码、线程本地存储

  • 5
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值