《深入了解Java虚拟机》笔记

最近在阅读《深入理解JAVA虚拟机》,因为本人记性较差,而且这类书本身就比较难懂,更需要反复阅读,所以采用的是慢读+笔记的学习方式,一处看不懂会反复地再阅读。

本书是基于jdk7,目前的最新版本是jdk1.9(2017.9)

jdk1.x一般指的是开发版本,jdkX是真正改动后的名字。

笔记是按照阅读顺序(目录顺序?)



第一部分:走进JAVA

概念:

1、JVM(JAVA virtual machine)Java虚拟机,提供了字节码文件(.class)的运行环境支持。JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。

Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

2、JRE(JAVA Runtime Environment):是支持JAVA程序运行的标准环境,包含JAVA SE API子架,JAVA虚拟机。运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。

.jre判断程序是否执行结束的标准时:所有的前台线程都执行完毕。

3、JDK(Java Development Kit):是支持JAVA程序开发的最小环境,是Java 语言的软件开发工具包(SDK)。包含JAVA语言、JAVA虚拟机、JAVA API类库。

jdk(java开发工具)> jre(java运行时环境)> jvm(java虚拟机)

4、java一开始是sun公司,后来的oracle公司收购。

5、现在主流的虚拟机有:HotSpot VM


第二部分:自动内存管理机制

一、运行时java虚拟机管理的数据区域:

java虚拟机运行时数据区

1、程序计数器:该内存区域是唯一一个在java虚拟机规范中没规定任何OutOfMemoryError情况的区域。线程隔离。

每条线程有一个独立的PC,且互不影响,独立存储。是线程私有的内存。

用于选取下一条需要执行的字节码指令。

2、JAVA虚拟机栈:为虚拟机执行java方法(也就是字节码)服务

也是线程私有的内存,生命周期同线程。是线程隔离的。

描述JAVA方法执行的内存模型,每个方法调用时都创建一个栈帧,用于存储信息。方法从调用到执行完成,意味一个栈帧在虚拟机栈中入栈到出栈。

一般JAVA内存区的栈就是指虚拟机栈。

在java虚拟机规范中:对该区域规定2种异常状况:

(1)线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常;

(2)虚拟机栈扩展时无法申请足够内存,抛出OutOfMemoryError异常

3、本地方法栈(为虚拟机使用到的native方法服务)

可以自由实现,或者有的JVM将虚拟机栈和方法栈合二为一。

在java虚拟机规范中:同样抛出两种异常。

栈区:存方法局部变量

4、JAVA堆:所有线程共享的内存区域

一般时JAVA虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。

目的:存放对象实例(所有对象实例都在这分配内存)类对象!!

JAVA堆时垃圾收集器管理的主要区域(常叫为GC堆,Garbage Collected Heap)

在java虚拟机规范中:可处于物理上不连续的内存空间,只要逻辑上是连续的即可。若在堆中没有内存完成实例分配,且堆也无法再扩展,抛出OutOfMemoryError异常。

5、方法区:所有线程共享的内存区域,与堆一样

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器变异后的代码等数据。

在java虚拟机规范中:

不需要连续的内存和可选择固定大小或者可扩展

可选择不是先垃圾收集(也常被成为永久代),但一般收集是必要的,不然会内存泄漏...bug

当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常

(1)运行时常量池:是方法区一部分

存放编译期生成的各种字面量和符号引用,将在类加载后进入方法区的运行时常量池中存放。

没有什么规定。

但具备动态性,常量不一定只有编译期产生。同样,当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常

PS:直接内存:不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区

但常使用,也可能导致OutOfMemoryError异常


总结!!!

方法区:线程共享,类信息、常量、静态变量、即时编译器编译后的代码等数据

(运行时常量池:方法区一部分,存类版本、字段、方法、接口等信息)

堆:线程共享,类对象,new出来的对象。

栈:线程隔离,存放方法局部变量,对象引用。

程序计数器:线程隔离,指示当前所执行的字节码行号,唯一不抛异常。

```

String s="abc";  //两个对象,引用s:栈中对象;“abc”:常量池对象

String  s=new String("abc"); //三个对象:引用s:栈中对象;“abc":常量池对象;new String:堆中对象

```



二、 HotSpot虚拟机对象

1、对象的创建:

通常是(1)new创建

(2)虚拟机先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过,没有则先执行相应的类加载过程.

(3)为新生对象分配内存(即把一块确定大小的内存从java堆中划分出来)(对象创建时并发情况可能线程不安全,解决方案:①对分配内存空间的动作进行同步处理;  ②把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲 TLAB) 

(4)内存分配完成后,虚拟机将分配到的内存空间都初始化为零值。

(5)虚拟机对对象进行必要的设置

2、对象的内存布局:

HotSpot虚拟机中,对象在内存中存储的布局分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

(1)虚拟机对象头包括两部分信息:

第一部分用于存储对象自身的运行时数据(通常长度是32位或64位,对32位的,存储对象哈希码25bit,对象分代年龄4bit,锁标志位2bit,1bit固定为0);是8字节整数倍

第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。但并不是所有虚拟机都保存类型指针。

(2)实例数据:对象真正存储的有效信息,代码中所定义的各种类型的字段内容,分配策略是相同狂赌字段分配一起(ints、longs/doubles、shorts/chars、bytes/booleans、oops)

(3)对齐填充:不是必须,用于补全8字节的整数倍

3、对象的访问定位:

java程序通过栈上的reference数据操作堆上的具体对象(reference类型在java虚拟机规范中只规定了一个指向对象的引用)

访问方式:

1、使用句柄访问:划分一块内存做句柄池,reference存对象的句柄地址(稳定的句柄地址)

2、直接指针访问:reference存对象地址(较快)


三、OutOfMemoryError异常:

1、堆溢出:OutOfMemoryError + "java heap space"

处理方式:

首先确认是内存中的对象是否是必要的?

内存泄漏(Memory Leak):准确定位泄漏代码位置

内存溢出(Memory Overflow):内存中的对象确实还必须存活,应检查虚拟机的堆参数(-Xmx,-Xms),减少内存消耗、调大堆参数

2、虚拟机栈和本地方法栈溢出:

java虚拟机规范中两种异常:

(1)线程请求的栈深度  > 虚拟机所允许的最大深度:StackOverflowError异常

(2)虚拟机在扩展栈时无法申请到足够内存空间:OutOfMemoryError异常

两者可能重叠

3、方法区和运行时常量池溢出:

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

String.intern():Native方法,若字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。

4、本机直接内存溢出


第三部分:垃圾收集器与内存分配策略

GC:Garbage Collection

一、判定对象是否存活的算法:垃圾收集器对堆进行回收前,要先确定这些对象中哪些还存活,哪些已经死去(不可能再被任何途径使用的对象)

【JVM中共划分为三个代:年轻代、年老代和持久代,

年轻代:存放所有新生成的对象;

年老代:在年轻代中经历了N次垃圾回收仍然存活的对象,将被放到年老代中,故都是一些生命周期较长的对象;

持久代:用于存放静态文件,如Java类、方法等。

新生代的垃圾收集器命名为“minor gc”,老生代的GC命名为”Full Gc 或者Major GC”.其中用System.gc()强制执行的是Full Gc.】

判断对象是否需要回收的办法?

【1、引用计数算法:

给对象添加一个引用计数器,每当一个地方引用它,计数器值加一;当引用失效时,计数器值减一;任何时刻计数器为0的对象就是不可能再被使用的。(python用了,但java虚拟机没用引用计数算法来管理内存,因为很难解决对象间相互循环引用的问题)

2、可达性分析算法:

通过一系列的被称为“GC Roots”的对象作为七十点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(从GC Roots到该对象不可达),则对象不可用,可回收。】

GC Roots包括:

虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区常量引用的对象、本地方法栈中JNI(即Native方法)引用的对象



3、引用:

强引用:Object obj=new Object(),new的对象,强引用存在,其被引用的对象就不会被回收。

软引用:描述还有用但非必需的对象。系统将发生内存溢出异常之前回收,如果还没有足够内存,才抛出内存溢出异常。

弱引用:描述非必需对象。其关联对象只能生存到下一次垃圾收集发生之前。

虚引用(幽灵引用/幻影引用):一定会被回收,回收前会通知


二、判定不存活的对象,至少进行两次标记,才确定真正死亡。

1、可达性分析发现不可达,则进行第一次标记+一次筛选

2、筛选:对象是否有必要执行finalize()方法(只能执行一次

“没有必要执行”——对象没有覆盖finalize()方法  或  finalize()方法已被虚拟机调用过

“有必要执行”——F-Queue队列,若对象在finalize()方法中重新与引用链上的任何一个对象建立关联,则第二次标记时会被移除出"即将回收"集合,成功拯救。否则——被回收。

但finalize()方法不鼓励用于拯救对象。

3、回收方法区:

java不要求虚拟机在方法区实现垃圾收集,且收集效率也较低

永久代垃圾收集回收:废弃常量、无用的类。

(1)废弃常量:

一个进入常量池的常量,不再被引用,则废弃,被清理出常量池

(2)无用的类:

同时满足:该类所有的实例都已被回收(java堆中不存在该类任何实例)、加载该类的ClassLoader已被回收、该类对应的java.lang,Class对象没在任何地方被引用(无法在任何地方通过反射访问该类的方法)

则该类可回收,但不是必然回收。


三、垃圾收集算法:

1、标记-清除算法:标记所有需要回收的对象,标记完成后统一回收。(效率不高;清除后产生大量不连续的内存碎片)

2、复制算法(很多用):把可用内存分两块,每次只使用其中一块;当这块内存用完了,将还存活的对象复制到另一块上,将已使用的一次清除(实现简单,运行高效;但每次将内存缩小到一半)

3、标记-整理算法(常用于老年代):标记所有需要回收的对象,将还存活的对象向一端移动,直接清理掉端边界以外的内存。

4、分代收集算法:新生代:复制算法;老年代:标记-清除/标记-整理


五、各类垃圾收集器(基于HotSpot虚拟机)

垃圾收集器是内存回收的具体体现。

1、Serial收集器:Client模式下的新生代收集器,单线程

2、Serial Old收集器:Client模式下的老年代收集器

(1、2是串行收集器)

3、ParNew收集器:server模式下的首选新生代收集器(Serial收集器的多线程版本)

4、Parall Scavenge收集器:更加关注吞吐量

5、Parall  Old收集器:parall scavenge收集器的老年代版本

(3、4、5是并行收集器)

6、G1收集器:整体上看,标记-整理,部分看是复制;新生代老年代都有,更加关注停顿时间。面向服务端应用。初始标记;并发标记;最终标记;筛选回收。

7、CMS收集器:并发标记清除,获取最短回收停顿时间为目的,老年代收集器(大量空间碎片),分为4个步骤:初始标记、并发标记、重新标记、并发清除。

【总结:

串行收集器:单线程,暂停所有应用线程来工作

并行收集器:默认的垃圾收集器。多线程。

G1收集器:新生代,老年代都有;整体(主要)是标记-整理,部分是复制算法。面向服务端应用。初始标记、并发标记、最终标记、筛选回收。

CMS收集器:以获取最短回收停顿时间为目标,老年代收集器;并发标记-清除。分为4个步骤:初始标记、并发标记、重新标记、并发清除。


第七章  虚拟机类加载机制

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

类型的加载、连接和初始化都是在程序运行期间完成的。(此处的Class文件更多表示一个类或接口,是一串二进制字节流。)


二、类的生命周期:加载、验证、准备、解析、初始化、使用、卸载  7阶段(连接:验证-->准备-->初始化)

类加载的五个过程:加载、验证、准备、解析、初始化。


(a)加载:类的全限定名--->定义此类的二进制字节流--->由字节流的静态存储结构--->转化为方法区的运行时数据结构--->内存中生成代表该类的java.lang.Class对象(方法区这个类的各种数据的访问入口)

(1)通过类的全限定名来获取定义此类的二进制字节流(从zip包获取——jar,war,ear;从网络中获取——Applet;运行时计算生成——动态代理;由其他文件生成——JSP应用;从数据库中获取)

(2)由字节流的静态存储结构转化为方法区的运行时数据结构

(3)在内存中生成代表该类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口

加载有两种情况,①当遇到new关键字或static关键字的时候就会发生

②动态加载,当用反射方法(如class.forName(“类名”)),如果发现没有初始化,则要进行初始化。(注:加载的时候发现父类没有被加载,则要先加载父类)

加载阶段可使用系统的引导类加载器完成,或用户自定义的类加载器完成(自定义控制字节流的获取方式),加载和连接阶段交错进行。

(b)验证:连接阶段第一步

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

验证阶段4个阶段检验动作:【文件格式检验、元数据验证(语义校验)、字节码验证、符号引用验证(解析时发生)】

(c)准备:

正式为类变量分配内存并设置类变量初始值,内存分配在方法区。

注意:!!此时只对类变量(static修饰的)进行内存分配,不包括实例变量(在对象实例化时随对象一起分配在java堆)。!!此时进行的是默认初始化,赋零值,即=0,=false;

(d)解析:虚拟机将常量池中的符号引用替换成直接引用的过程。

符号引用:以一组符号来描述所引用的目标(不一定已加载到内存中)

直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。(目标必定加载到了内存)


(e) 初始化:真正执行类中的代码

是执行类构造器()方法的过程。

用于:创建类的实例、访问类或接口的静态变量、调用类的静态方法、反射(Class.forName())、初始化类的子类

方法:编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生;JVM会保证父类的()先执行,所以第一个执行的一定是Object类,所以父类中定义的静态语句块优先于子类的变量赋值操作;该方法对类/接口不是必须的,如果类无静态语句块/对变量的赋值动作,可无该方法。】


(三)类加载器:完成加载阶段的【通过一个类的全限定名来获取描述此类的二进制字节流】的操作。在JVM外部实现。

1、分类:

(1)从JVM来看,

a.启动类加载器(C++,是虚拟机自身的一部分)

b.所有其他类加载器(Java,独立于虚拟机外部,全都继承自抽象类java.lang.ClassLoader)

(2)从开发来看,

a.启动类加载器:Bootstrap  ClassLoader:

负责加载 存放在\lib目录中,或被-Xbootclasspath参数所指定的路径中,被虚拟机识别的类库  到 虚拟机内存中。

b.扩展类加载器:Extension ClassLoader,可直接使用,负责加载   \lib\ext目录中,或被java.ext.dirs系统变量所指定的路径中的所有类库   到   虚拟机内存中。

c.应用程序类加载器:可直接使用,负责加载  用户类路径(ClassPath)上所指定的类库。是程序默认的类加载器。

d.自定义类加载器:通过继承ClassLoader实现,一般是加载我们的自定义类

2、类加载器间层次关系:

a.双亲委派模型:组合关系。

请求委派给父类,最终都传到启动类加载器,只有父类加载器反馈自己无法加载,子加载器才尝试加载。

其代码都集中在java.lang.ClassLoader的loadClass()方法中。【先检查是否被加载过,无则调用父类的loadClass(),父加载器为空则使用启动类加载器作为父加载器,若父类加载失败,抛出ClassNotFoundException,调用自己的findClass()加载】

【阿里的面试官问我,可以不可以自己写个String类

答案:不可以,因为 根据类加载的双亲委派机制,会去加载父类,父类发现冲突了String就不再加载了;

可以类比到其他已经有的类。我试过,然后其他同个包中的所有类有运用到原先的String类的全都报错,因为引用到了这个类。】





说一说你对环境变量classpath的理解?如果一个类不在classpath下,为什么会抛出ClassNotFoundException异常,如果在不改变这个类路径的前期下,怎样才能正确加载这个类?

classpath是javac编译器的一个环境变量。它的作用与import、package关键字有关。package的所在位置,就是设置CLASSPATH当编译器面对import packag这个语句时,它先会查找CLASSPATH所指定的目录,并检视子目录java/util是否存在,然后找出名称吻合的已编译文件(.class文件)。如果没有找到就会报错!

动态加载包

-

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