《深入理解Java虚拟机》读书笔记

最近花了几天时间读完了《深入理解Java虚拟机》,摘录了一些重点备忘。


《深入理解Java虚拟机》读书笔记_第1张图片

内存管理机制

程序计数器 Program Counter Register : 当前线程执行字节码的行号指示器。为了方便线程切换后能恢复到正确的执行位置,每个线程都需要独立的程序计数器。 线程私有
Heap:存放所有对象实例 和 数组。 线程共享
方法区:存已被虚拟机加载的类信息、常量池(比如String.intern()这个Native方法放入的字符串)、静态变量,即时编译器编译后的代码等数据。


《深入理解Java虚拟机》读书笔记_第2张图片

StackOverflowError: 线程请求的栈深度大于虚拟机所允许的最大深度
OutOfMemoryError: 虚拟机扩展栈时无法申请到足够的内存空间

垃圾回收器与内存分配策略

如何判断对象可回收?

  1. 引用计数算法:很多教科书提到,但是Java,C#,Lisp都不是这个算法,因为它很难解决对象之间相互循环引用的问题。
    objA.instance = objB; objB.instance = objA; objA = null; objB = null; 这两个对象虽然不可能再被访问,但是他们引用计数都不为0,无法被回收。

  2. 根搜索算法:GC Roots Tracing . Java,C#,Lisp用这种算法实现GC。
    通过一系列名为“GC Roots"的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链(Reference Chain),
    当一个对象到GC Roots没有任何引用链相连时,证明此对象不可达

在Java语言里,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)的引用的对象。


《深入理解Java虚拟机》读书笔记_第3张图片

垃圾收集算法

  1. 标记-清除算法(Mark-Sweep) 效率低,产生内存碎片
  2. 复制算法 : 划分两块区域Eden,Survivor . 一般Eden占90%都是可回收的,将不可回收的对象复制到Survivor区,然后一次性删掉Eden区
    3.标记-整理算法:
    4.分代收集算法:新生代,每次垃圾收集都发现大批对象死去,那就用复制算法。老年代对象存活率高,没有额外空间对他进行分配担保,就用”标记-清除“或”标记-整理“算法。

JVM性能监控与故障处理工具

JDK的命令行工具:bin目录下,大都类似linux命令命名,加了前缀j
jps -l -v -m : JVM Process Status Tool, 虚拟机进程状况工具。 LVMID(Local Virtual Machine Identifier)
jstat -gc -class- gcutil 虚拟机统计信息监视工具
jinfo :Java配置信息工具
jhat :虚拟机堆转储快照分析工具
jmap : Memory Map for Java Java内存映像工具
jstack -l pid Java堆栈跟踪工具 类似 Java.lang.Thread.getAllStackTraces() 方法获取JVM所有线程的StackTraceElement[]
JConsole : GUI工具,将自动搜索本机运行的所有虚拟机进程(即jps查询出的)
JVisualVM : All-in-One Java Troubleshooting Tool多合一的故障处理工具。性能分析Profiling。依赖插件,下载 https://visualvm.github.io/plugins.html
安装和使用TDA插件: https://github.com/irockel/tda

我的一次分析线上内存溢出的实战记录:
(1)使用 jps -lvm找出要分析的Java进程

(2)使用 jmap -dump:format=b,file=heapDump //dump出一个Java进程的堆数据到heapDump文件。
(3)使用JVisualVM => Load这个dump文件。 如下图,在找到实例残存比较多的Classes,双击进入”Instances“查看Refre

《深入理解Java虚拟机》读书笔记_第4张图片

《深入理解Java虚拟机》读书笔记_第5张图片

虚拟机执行子系统

类文件结构

字节码与机器码。 字节码是与操作系统和CPU指令集无关的程序编译后的存储格式,实现跨平台.

The Java Language Specification + The Java Virtual Machine Specification是独立的。是为了让其他实现JVM的语言也能运行在Java虚拟机上,目前有Clojure /clouje/,Groovy,JRuby,Jython,Scala
字节码命令提供的语义描述能力肯定比Java本身更强大,因此有些Java语言本身无法有效支持的语言特性并不代表字节码无法有效支持。这也为其他语言实现一些有别于Java语言特性提供了基础

javap -verbose xxx.class 查看解析后的字节码内容

虚拟机类加载机制

虚拟机把描述类的数据从.class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载

《深入理解Java虚拟机》读书笔记_第6张图片

加载:从一个类的全限定名来获取此类的二进制字节流。来源:
可以从jar,ear,war
从网络获取,这种场景是Applet
运行时计算生成,比如动态代理java.lang.reflect.Proxy ProxyGenerator,generateProxyClass来为特定接口生成*$Proxy代理类的二进制字节流。
由其他文件生成,比如JSP应用
从数据库中读取,比如有些中间件SAP Netweaver,把程序安装到数据库来完成程序代码在集群中的分发
开发人员可以通过定义自己的类加载器去控制字节流的获取方式
验证:文件格式验证,元数据验证,字节码验证,符号引用验证
准备:为类变量(static变量,不包括实例变量哦)分配内存并设置初始值(类型的默认值比如int=0,char='\u0000',另外final变量会直接赋值),分配在方法区中。
解析:将虚拟机常量池内的符号(Symbolic References)引用替换为直接引用(Direct References)的过程。即在Class文件中CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info。
一般包括:静态方法,私有方法,实例构造器,父类方法四类(编译器可知,运行期不可变)
符号引用与虚拟机实现的内存布局无关,引用的目标不一定加载到内存中。直接引用是直接指向目标的指针,相对偏移量或能瞬间定位到目标的句柄,必定已经在内存中存在。

类加载器

JVM设计团队把类加载阶段的”通过一个类的完全限定名来获取描述此类的二进制流“这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块被称为”类加载器“
用途:类层次划分,OSGi(Open Service Gateway Initiative,OSGi联盟指定的基于Java的动态模块化规范),热部署,代码加密等领域。(原本类加载器是设计满足Java Applet的需求而开发的,虽然已经死掉,但失之桑榆,收之东隅)
不同类加载器加载相同的类,永远不会相等(instanceof,isAssignableFrom(), equals()方法)
启动类加载器(Bootstrap ClassLoader):加载\lib目录或者被-Xbootclasspath指定路径中的类库。启动类加载器无法被Java程序直接引用
扩展类加载器(Extension ClassLoader):sun.misc.Launcher$ExtClassLoader,加载\lib\ext或者java.ext.dirs系统变量指定路径中的类库。开发者可使用扩展类加载器
应用程序类加载器(Application ClassLoader):sun.misc.Launcher$ApClassLoader,由ClassLoader.getSystemClassLoader()返回,加载用户类路径classpath指定的类库,这是应用程序默认的类加载器
双亲委派模型Parents Delegation Model:要求除顶层启动类加载器之外,其余类加载器都应当有自己的父类加载器,非继承Inheritance,而是Composition关系来服用父加载器

《深入理解Java虚拟机》读书笔记_第7张图片

破坏双亲结构的特例:启动类加载器的缺陷是它并不认识SPI(Service Provider Interface)的代码,无法加载,比如JNDI,JDBC,JCE,JAXB,JBI等。所以引入Thread Context ClassLoader, 通过Thread.setContextClassLoader()方法来设置。这就是父类加载器请求子类加载器去完成类加载动作,实际上是双亲委派模型的层次结构逆向使用。
对程序的动态性追求,比如代码热替换HotSwap,OSGi模块化热部署Hot Deployment 关键是自定义类加载器机制实现。

虚拟机字节码执行引擎

运行时栈帧结构 Stack Frame : 支持虚拟机进行方法调用和执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈Virtual Machine Stack的栈元素。每一个栈帧包含:


《深入理解Java虚拟机》读书笔记_第8张图片
  1. 局部变量表 : 存放方法参数和方法内部定义的局部变量。实例方法的第一个参数总是this引用。在class文件Code属性的max_locals数据项中
  2. 操作数栈 : class文件Code属性max_stacks数据项中
  3. 动态连接 :指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持动态连接。字节码中方法的调用指令就以常量池中的符号引用为参数,这写符号一部分在类加载阶段转换为直接引用,称为静态解析,另外一部分在每次运行期间转化,称为动态连接。
  4. 方法返回值地址。 方法退出过程,当前栈帧出栈,恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用这栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令。
    每一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

分派Dispatch

静态分派:编译阶段编译器的选择过程,实现方法重载Overlaod的机制,多态1
动态分派:运行阶段虚拟机的选择过程,实现重写Override,多态2


《深入理解Java虚拟机》读书笔记_第9张图片

子类没有重写的方法与父类相同方法的入口地址是一致的,都指向父类的实现入口

解释执行

《深入理解Java虚拟机》读书笔记_第10张图片

基于栈的指令集和基于寄存器的指令集

Java编译器输出的指令流基本上是基于栈的指令集架构(Instruction Set Architecture,ISA)。
优点:可移植性,缺点:内存操作,性能比不上CPU寄存器
1+1代码示例:
iconst_1
iconst_1
iadd //add指令想加,把结果放回栈顶
istore_0 //最后把栈顶值放到局部变量表的第0个Slot中

x86的二进制指令集是基于寄存器的指令集。 依赖硬件
mov eax, 1
add eax, 1 //add结果直接保存在EAX寄存器

基于栈的解释器执行过程 (概念模型,实际会有编译器优化)

《深入理解Java虚拟机》读书笔记_第11张图片

《深入理解Java虚拟机》读书笔记_第12张图片

Tomcat类加载器架构

主流Java Web服务器,Tomcat,Jetty,WebLogic,WebSphere都实现了自己的类加载器(一般都不止一个),因为都需要解决如下问题:

  1. 部署在同一个服务器上的多个Web应用使用的Java类库可以互相隔离
  2. 可以互相共享,比如都使用Spring的话只需要一份
  3. 支持JSP的应用需要支持HotSwap功能
  4. 服务器尽可能保证自身的安全不受部署的Web应用程序影响

Tomcat四组目录

  1. /common/* 类库可被Tomcat和所有Web App公用
  2. /server/* 类库仅Tomcat自己用
  3. /shared/* 类库可以所有Web App公用,但Tomcat自己不可见
  4. /WEB-INF/* 仅Web App自己可见


    《深入理解Java虚拟机》读书笔记_第13张图片

    Tomcat6.x默认配置把/common,/server,/shared合并成一个/lib目录

OSGi的网状类加载器架构

灵活但是很复杂,容易出循环依赖,死锁的问题。 其他Java9自己的模块化实现


《深入理解Java虚拟机》读书笔记_第14张图片

字节码生成技术与动态代理的实现

1.常用框架:Javassist、CGLib(Code Generation Library)、ASM等字节码类库
JDK的javac命令就是字节码生成技术的“老祖宗”
2.应用场景:JSP编译器,编译时织入的AOP框架
3.动态代理:java.lang.reflect.Proxy实现InvocationHandler接口。Spring就是通过动态代理来对Bean进行增强的。
main()方法中加入System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); 会将动态代理生成的$Proxy0.class文件输出到磁盘

静态代理AspectJ

AspectJ是编译时增强代码, 需要使用自己的编译器来编译 Java 文件,还需要织入器

程序编译与代码优化

编译器

1.前端编译器:.java转变成.class的过程。比如Javac
JIT编译器:虚拟机运行期JIT(Just In Time Compiler)编译器把字节码转变成机器码的过程。比如HotSpot VM的C1,C2编译器。
静态提前编译器:AOT(Ahead of Time)直接把.java编译成本地机器码。GNU Compiler for the Java(GCJ), GCC,the GNU Compiler Collection

Javac : com.sun.tools.javac.main.JavaCompiler

《深入理解Java虚拟机》读书笔记_第15张图片

《深入理解Java虚拟机》读书笔记_第16张图片

1.词法、语法分析
源代码字符流 (int a=b+2) 转变为 Token(int、a、=、b、+、2)
由Token构造抽象语法树(AST,Abstract Syntax Tree)


《深入理解Java虚拟机》读书笔记_第17张图片

2.注解处理器

3.语义分析与字节码生成

Java语法糖

1.泛型 :实现方式是类型擦除,伪泛型
JDK1.5引入,本质是Parameterized Type的应用,就是操作的数据类型被指定为一个参数。
Java泛型只存在与源码中,编译后的字节码文件就替换为Raw Type源类型了。(比如运行期List,List是同一个类)
2.自动装箱、拆箱和遍历循环


《深入理解Java虚拟机》读书笔记_第18张图片

3.其他:内部类、枚举类、断言语句、对枚举和字符串的switch支持(JDK1.7中支持),try中定义和关闭资源(1.7)。可以跟踪javac源码,反编译class了解他们的本质。

运行期优化

Java程序最初是通过解释器Interpreter进行解释执行的,当JVM发现某个方法或代码块运行很频繁,被认定为Hot Spot Code,为了提高执行效率,运行时,JVM会将这些代码编译成与本地平台相关的机器码,并进行各种优化,这个过程叫Just In Time Compiler.


《深入理解Java虚拟机》读书笔记_第19张图片

线程与并发

Java内存模型

1.多核CPU各自有高速缓存,然而又都共享同一主内存,引发问题:缓存一致性,Cache Coherence


《深入理解Java虚拟机》读书笔记_第20张图片

2.Java内存模型:屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的并发效果。
(1)主内存和工作内存。注意如上图中的主内存不是同一个东西。这里是逻辑分类,而上图是物理分类。不过,两者关系类似上图中的物理主内存和CPU高速缓存
主内存:虚拟机内存的一部分
工作内存:每个线程独有,保存该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,不能直接读取主内存变量。


《深入理解Java虚拟机》读书笔记_第21张图片

(2)内存间交互操作
lock:作用于主内存变量,把一个变量标识为一条线程独占

unlock:作用于主内存变量,把处于锁定的变量释放
read:主内存变量,把变量从主内存传输到线程工作内存,以便随后的load动作使用
load:作用于工作内存变量,把read操作得到的变量值放入工作内存的变量副本中
use:作用于工作内存变量,把工作内存的变量的值传递给执行引擎
assign:赋值,作用于工作内存变量,把从执行引擎收到的值赋值给工作内存的变量
store:作用于工作内存变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用
write:作用于主内存的变量,把store操作得到的变量的值放入主内存变量中
(3)volatile型变量的特殊规则
volatile是最轻量级的同步机制,(相对synchronized来说)。
语义一:能保证变量对所有线程的可见性,即新值对所有线程立即可得知。(普通变量需要等到从工作内存向主内存回写的时候才知道)
语义二:禁止指令重排序
但是变量有非原子操作的话,volatile并不安全,比如a++就是非原子的
(4)原子性、可见性、有序性
Java内存模型就是围绕在并发操作中如何处理以上三个特征来建立的。
Atomicity :read,load,assign,use,store,write都是原子性的,基本数据类型的读写是原子性的(long和double虽然没规定必须原子性,但实际上各种虚拟机实现他们都是原子性)
其他应用场景,提供lock,unlock,synchronized块(字节码指令monitorenter,moniterexit)
Visibility:一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile,synchronized,final都能实现可见性。
fina是因为初始化完并没有将字节码的this指针传递出去,无须同步其他线程就能正确访问。
Ordering:

实现线程的三种方式

  1. Kernel Thread,KLT就是直接由操作系统内核支持的线程,由内核来完成线程切换,内核通过操纵调度器Schedular对线程进行调度,并负责将线程的任务分配到各个处理器上。
    支持多线程的内核交Multi-Threads Kernel。内核线程的一种高级接口----轻量级进程Light Weight Process,LWP,就是通常意义上的线程。
    局限性:内核线程是系统级调用,代价高,需要在用户态User Mode和内核态Kernel Mode中来回切换,一个系统支持LWP数量有限。


    《深入理解Java虚拟机》读书笔记_第22张图片
  2. User Thread,UT,这种进程与用户线程是一对多模型。劣势很大,因为没有系统内核的支援,阻塞如何处理,多CPU如何线程映射到其他CPU,基本都很难处理。很少有语言用了
  3. 混合实现

Java线程的实现

Sun JDK Windows版和Linux版是使用一对一的Kernel Thread实现。在Solaris平台中,支持一对一的Bound Threads或Alternate Libthread,以及多对多LWP/Thread Based Synchronization的线程模型。
1.Java线程调度
抢占式调度多线程系统,每个线程由系统来分配执行时间,线程切换不由线程本身决定(Thread.yield()可以让出执行时间,但要获取执行时间没办法)
这种方式下,线程执行时间是可控的,不会出现一个线程导致整个进程阻塞的问题。
优先级Priority
2.线程状态

《深入理解Java虚拟机》读书笔记_第23张图片

3.线程安全的实现方式
3.1 互斥同步:Mutual Exclusion&Synchronization,互斥的实现方式临界区Critical Section,互斥量Mutex,信号量Semaphore。 他是阻塞同步Blocking Synchronization
3.2 非阻塞同步:Non-Blocking. 因为互斥同步是一种悲观并发策略,总是要进行加锁。
而非阻塞同步是基于冲突检测的乐观并发策略,就是说先进行操作,没有其他线程争共享数据,那操作成功;如果有数据争用产生了冲突,那就再进行补偿措施(通常就是不断重试)。
这种乐观并发策略的许多实现都需要把线程挂起,因此被称为非阻塞同步。
不过乐观并发策略需要“硬件指令集的支持”,需要操作和冲突检测这两个步骤具备原子性,只能靠硬件,这类处理器指令有:
(1)测试并设置Test-and-Set
(2)获取并增加Fetch-and-Increment
(3)交换Swap
(4)比较并交换 Compare-and-Swap, CAS: IA64,x86指令集通过cmpxchg指令实现。 Java中sun.misc.Unsafe类compareAndSwapInt()提供封装,虚拟机即时编译出来结果就是一条平台相关的CAS指令
(5)加载链接/条件存储Load-Linked/Store-Conditional, LL/SC
前三条早已在大多数CPU指令集中,后两条是现代处理器新增的

你可能感兴趣的:(《深入理解Java虚拟机》读书笔记)