天天看点

《深入理解Java虚拟机》学习笔记

第2章 垃圾收集器与内存分配策略 

1.java虚拟机在执行java程序时会把它所管理的内存会分为若干个不同的数据区域,

这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,

有些区域则是在以来用户线程的启动和结束而建立和销毁。

根据《java虚拟机规范》,包括以下几个运行时数据区域:

//此处应有类图,但是画起来太麻烦!

程序计数器(program counter register)

方法区(method area)

虚拟机栈(vm stack)

本地方法栈(native method stack)

堆(heap)

2.pcr 程序计数器

//在操作系统中学习过program counter,程序计数器是用于存放下一条指令所在单元的地址的地方。

在虚拟机中,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转等基础功能都需要依赖计数器完成。

为了多线程切换过程中能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,成为“线程私有”的内存。并且,程序计数器是唯一一个在java虚拟机规范中没有规定任何outofmemory情况的区域。

3.java虚拟机栈 java virtual machine stacks

与程序计数器一样,java虚拟机栈也是线程私有的,生命周期和线程相同。

虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息。

//每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出站的过程。

虚拟机栈会抛出 stackoverflowerror 和 outofmemoryerror异常。

4.本地方法栈 native method stacks

虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机使用到的native方法服务。

5.java堆

java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是 存放对象实例。

java堆是垃圾收集器管理的主要区域,很多时候被称为“gc堆(garbage collected heap)”。

如果在堆中没有内存完成实例分配,并且无法继续扩展时,会抛出outofmemoryerror异常。

6.方法区

方法区和java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,

//运行时常量池,是方法区的一部分。

7.直接内存 diret memory

直接内存并不是java虚拟机规范中定义的内存区域,但是频繁使用,也可能导致outofmemoryerror异常出现。

nio的部分操作,使用native函数库直接分配堆外内存,然后通过一个存储在java堆里面的directbytebuffer对象,作为这块内存的引用进行操作,避免了在java堆和native堆中来回复制数据,一些场景中可以显著提高性能。

8.主流的对象访问方式,使用句柄和直接指针。

使用句柄访问方式的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用指针访问速度更快,节省了一次指针定位的时间开销。

第3章 虚拟机性能监控与故障处理 

//了解gc和内存分配有哪些意义?

当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这些“自动化”的技术实施必要的监控和调节。

1.引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;引用失效时,计数器值减1‘任何时刻计数器都为零的对象就是不可能再被使用的。

但是,引用计数法有一个很大的不足,很难解决对象之间的相互循环引用问题。

下面的代码描述了引用计数法的一个情景:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<code>public</code> <code>class</code> <code>referencecountgc {</code>

<code>    </code><code>public</code> <code>object instance=</code><code>null</code><code>;</code>

<code>    </code><code>private</code> <code>static</code> <code>final</code> <code>int</code> <code>_1mb=</code><code>1024</code><code>*</code><code>1024</code><code>;</code>

<code>    </code><code>private</code> <code>byte</code><code>[] bigsize=</code><code>new</code> <code>byte</code><code>[</code><code>2</code><code>*_1mb];</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>testgc(){</code>

<code>        </code><code>referencecountgc obja=</code><code>new</code> <code>referencecountgc();</code>

<code>        </code><code>referencecountgc objb=</code><code>new</code> <code>referencecountgc();</code>

<code>        </code><code>obja.instance=objb;</code>

<code>        </code><code>objb.instance=obja;</code>

<code>        </code><code>obja=</code><code>null</code><code>;</code>

<code>        </code><code>objb=</code><code>null</code><code>;</code>

<code>    </code><code>}</code>

<code>}</code>

如果单纯的通过引用计数法来判断对象的状态,因为互相循环引用,这两个对象都不会被gc回收。

2.根搜索算法

通过一系列的名为“gc roots”的对象作为起始点,从这些节点开始向下搜索,当到达不在gc roots引用链上的对象时,即使它们之间互相关联,仍然判定为可回收对象。

用图论的理论,可以表示为gc roots到该对象不可达。

3.更好的理解“引用”

java中的引用,最开始的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

这样的定义在垃圾收集时太过绝对,后来对引用的概念进行了扩充,将引用分为强引用,软引用,若引用,虚引用,四种引用强度逐渐减弱。

只要强引用还存在,gc永远不会回收这些被引用的对象,其他的引用类型,垃圾收集器会根据情况进行对象回收。

4.不可达对象的“自我拯救”

在根搜索方法过程中,真正宣告一个对象死亡,至少经历两次标记过程:如果对象在进行根检索后发现没有与gc roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

5.回收方法区

6.垃圾收集算法

//标记-清除算法

//复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,把还存活着的对象复制到另一块内存上,然后把使用过的内存空间一次清理掉。实现简单,运行高效。只是空间上将内存缩小为原来的一半,代价较高。

目前商业虚拟机中较多的使用复制算法来回收新生代,并且不需要按照1:1的比例划分内存空间。而是将内存分为一块较大的eden空间和两块较小的survior空间,每次使用eden和其中的一块survivor。

比如hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%。

《深入理解Java虚拟机》学习笔记

//标记-整理算法

复制收集算法在对象存活率较高时需要执行较多的复制操作,效率变低。

//分代收集算法

根据对象的存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的手机算法。新生代时,每次垃圾收集都发现有大批对象死去,只有少量存活,那就选用复制算法。老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

7.垃圾收集器

//垃圾收集器可以搭配使用。

8.内存分配与回收策略

8.1 对象优先在eden分配

大多数情况下,对象在新生代eden区中分配。当eden区没有足够的空间进行分配时,虚拟机将发起一次minir gc。

虚拟机提供了 

-xx:+printgcdetails 这个收集器日志参数。

-xx:survivorratio=8 设置新生代中eden区与survivor区的比例是8:1。

8.2 大对象直接进入老年代

大对象指,需要大量连续内存空间的java对象,典型的比如很长的字符串及数组。

8.3 长期存活的对象将进入老年代

采用分代收集的思想来管理内存,必须有个规则来确定哪些对象放在新生代,哪些对象放在老年代中,虚拟机给每个对象指定了一个对象年龄计数器。

8.4 动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机在分代放置对象时,并不总是固定的,并不总是要求对象的年龄达到maxtenuringthreshold才能进入老年代。

8.5 空间分配担保

//学习了虚拟机内存分配与回收技术的理论,还要掌握如何在实际工作中应用。

//介绍了随jdk发布的6个命令行工具和2个可视化的故障处理工具。

1.jdk的命令行工具

jdk的bin目录下有许多工具,可以用于监视虚拟机和故障处理。

很多小工具的命名都类似unix命令,比如jps,功能和linux的ps相似。

常用的jdk命令行工具:

jps/jinfo/jmap/jhat/jstack

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

2.jdk的可视化工具

jconsole:java监视与管理控制台

visualivm:多合一故障处理工具

//分享几个比较有代表性的实际案例。

1.高性能硬件上的程序部署策略

在高性能硬件上部署程序,主要有两种方式:

通过64位jdk来使用大内存;

使用若干个32位虚拟机建立;逻辑集群来利用硬件资源。

对于用户交互性强,对停顿时间敏感的系统,给虚拟机分配超大堆的前提是有把握把应用程序的full gc频率控制得足够低,不能影响用户使用。

控制full gc频率的关键是看应用中绝大多数对象能否符合"朝生夕灭"的原则,即大多数对象的生存时间不应当太长,尤其是不能产生成批量的、长生存时间的大对象,这样才能保障老年代空间的稳定。

在使用64位jdk管理内存时,还需要考虑下面可能面临的问题:

内存回收导致的长时间停顿;

性能可能普遍较低;

因为指针膨胀及数据类型对齐补白等因素,相同的程序在64位jdk中消耗的内存一般比32位jdk大。

2.集群间同步导致的内存溢出

3.堆外内存导致的溢出错误

//大量使用nio操作会占用很多堆外内存。

下面是一些实践经验,除了java堆和永久代之外的内存:

direct memory:可通过-xx:maxdirectmemorysize 调整大小,内存不足时抛出outofmemoryerror

或者outofmemoryerror:direct buffer memory。

线程堆栈:可通过-xss调整大小,内存不足时抛出stackoverflowerror(纵向无法分配,即无法分配新的栈帧)

socket缓存区:每个socket连接都receive和send两个缓存区,分别占大约37kb和25kb的内存。

jni代码:

虚拟机和gc:

4.外部命令导致系统缓慢

5.服务器jvm进程崩溃

6.实战:eclipse运行速度调优

第6章 类文件结构

//实现语言无关性的基础仍然是虚拟机和字节码存储格式,使用java编译器可以把java代码编译为存储字节码的class文件,使用jruby等其他语言的编译器一样可以把程序代码编译成class文件,虚拟机并不关心class的来源是什么语言,只要它符合class文件应有的结构就可以在java虚拟机中运行。

1.class类文件的结构

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件之中,中间没有分隔符。

class文件格式采用一种类似于c语言结构体的伪结构来存储,只有两种数据类型:无符号数和表。

每个class文件的头4个字节成为魔数,唯一作用是用于确定这个文件是否为一个能被虚拟机接受的class文件。

主次版本号之后的是常量池入口,常量池是class文件结构中与其他项目关联最多的数据类型。主要存放两大类常量,即字面量(literal)和符号引用(symbolic references)。

第7章 虚拟机类加载机制

//代码编译的结果是从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

在class文件中描述的各种信息,被加载到虚拟机中之后才能被运行和使用。而虚拟机是如何加载class文件,class文件中的信息进入到虚拟机后会发生什么变化?

虚拟机读取class文件,把描述类的数据从class文件加载到内存中,并对数据进行校验,转换解析和初始化,然后形成可以被虚拟机直接使用操作的java类型,即虚拟机的类加载机制。

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

1.类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止。整个生命周期包括七个阶段。

加载、验证、准备、解析、初始化、使用和卸载七个阶段。

验证、准备和解析统称为连接(linking)。

2.类加载的过程

2.1 加载阶段

加载阶段,特别是加载时获取类的二进制字节流的动作,是开发期可控性最强的阶段,因为既可以使用系统的类加载器,也可以自定义类加载器实现,并且通过自定义类加载器控制字节流的获取方式。

2.2 验证阶段

确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

2.3 准备阶段

正式为类变量分配内存并设置类变量初始值,这些内存都将在方法区中进行分配。

2.4 解析阶段

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

2.5 初始化阶段

初始化阶段,才真正开始执行类中定义的java程序代码,即字节码。

3.类加载器

//类加载阶段的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作被放到虚拟机外部实现,

//即应用程序自己控制获取,实现这个动作的代码模块被称为"类加载器"。

类加载器在类层次划分,osgi,热部署,代码加密等领域都应用到。

3.1 类与类加载器

3.2 双亲委派模型

从虚拟机的角度,存在两种不同的类加载器:一种是启动类加载器(bootstrap classloader),这个类使用c++语言实现,是虚拟机自身的一部分,另一种是所有其他的类加载器,全部继承自抽象类java.lang.classloader。

双亲委派模型的工作过程,即一个类加载器收到类加载的请求,首先会委派给父类加载器去完成,所有的加载请求都传送到顶层的启动类加载器,如果父类加载器反馈无法完成加载请求,会继续交由子类加载。

这样加载的好处是java类随着它的类加载器拥有了带优先级的层次关系,避免了出现一个类加载多次。

第8章 虚拟机字节码执行引擎

1.运行时栈帧结构

2.方法调用

3.基于栈的字节码解释执行引擎

第9章 类加载及执行子系统的案例与实战

//在class文件格式与执行引擎这部分里,主要是由虚拟机直接控制的行为,能通过程序进行操作的,主要是字节码生成与类加载器这两部分。

1.案例分析

tomcat:正统的类加载器结构

主流的java web服务器,如tomcat、weblogic等都实现了自己定义的类加载器。

一个功能完备的web服务器,要解决下面的问题:

部署在同一个服务器上的多个web应用程序使用的java类库既可以实现相互隔离,又可以互相共享。