Java程序进阶课程学习(三)

写在前面

前面两节,我们学习了关于,并发编程 & 网络编程的 一些基础知识,有了基本的了解。这一部分我们学习一下关于 Java 虚拟机的基本概念。

其实,对于 JVM ,后续我们还要再看一本书,这里的对 JVM 只是简单的介绍。
通过这里的学习,对 JVM 有一个大概的理解和把控,就行了。具体的细节,后续还要学习 《深入理解java虚拟机》。


6.2 Java虚拟机概念

这里我们要理解JVM是什么、为什么有JVM、JVM 中是怎么划分的等一系列问题。

什么是 JVM

JVM(Java Vertual Machine),顾名思义,是一个想象出来的虚拟机器,在实际计算机上通过软件模拟来实现。

为什么要有 JVM

为了 java 的跨平台实现。
JVM 实际上就是在操作系统的基础上,生成出来的一层。看下面这个图就明白了。


java虚拟机所处的逻辑位置

而对于java 代码,编译完成之后,生成 字节码。这字节码就可以在各个操作系统中的 JVM 中运行了。即实现了跨平台特性。

JVM 的生命周期
  • JVM实例对应一个 Java 程序(进程级别)
    所以,当我们启动任何一个 Java 程序的时候,一个 JVM实例就产生了。虚拟机的产生的起点就是, public static void main(String args[]) 方法。
    而 JVM 实例的消亡,则对应着 进程中所有非守护线程的停止(就是我们前面说的前端线程和后端线程,后端线程都结束,则进程结束),当然,我们可以通过一些 exit() 方法来结束进程。
    对于JVM生命周期很好的解释
JVM 的体系结构

JVM的体系结构分为4(5)个部分:类加载器子系统、执行引擎、垃圾回收子系统、运行时数据区、本地接口(也有说法将本地接口也算进来。)

JVM体系结构

(1)运行时数据区
用于运行时,数据、代码的存放。
包含方法区、java堆、栈、本地方法栈、程序计数器等。
(2)类加载器子系统
类加载器,顾名思义,用来加载 .class 文件
(3)执行引擎
执行。 这里是执行 编译之后的字节码 / 本地方法
(4)GC子系统
GC(Carbage Collection)子系统,用作垃圾回收。

JVM 数据类型

JVM中的数据类型简单分类为:Primitive types原始数据类型、Reference type引用数据类型。
原始数据类型:4类8个--int/byte/short/long; float/double; boolean; char;
引用数据类型:就是自己定义的那些类的引用。

先这么理解,数据类型这里和之前的理解都是一样的。
不过实际上,JVM中还多了一个 java 语言中不能使用的数据类型,return type返回值类型,具体遇到了再看,这里先不管。


准备知识

JDK、JRE和JVM的关系
  • JDK(Java Development Kit):将 JVM + Java 语言 + Java API 类库
  • JRE(Java Runtime Environment):JVM + Java API类库中 Java SE API 子集

JRE 是支持 Java程序运行的标准环境。
JDK 包含 JRE,还包含开发工具(编译工具javac、反编译 javap、打包工具 jar 等)
而 JVM 是 运行 Java 程序的核心,用来处理了字节码、管理内存等

Java EE/ Java SE

其实这是一种 Java 技术栈划分的方式。除了 java EE、SE, 还有其他的两种 Java Card & Java ME
这是一种按照技术所服务的领域来划分的方式:

  • Java Card:支持Java小程序运行在小内存设备上的平台
  • Java ME(Micro Edition):支持Java程序运行在移动终端上的平台
  • Java SE(Standard Edition):支持面向桌面级应用(比如说Windows)的平台
  • Java EE(Enterprise Edition):支持多层架构的企业级应用的平台。

6.3 Java 虚拟机内存划分

注意,这里不区分JVM内存和运行时数据区域,二者是一个概念。

上一小节中,我们大概介绍了Java的体系结构,下面我们依次看一下JVM中每一个部分的具体内容。
这一小节先看,Java 虚拟机内存的具体划分是怎样。


JVM 内存区域

可以看到分为5个部分:方法区、堆、虚拟机栈、本地方法栈 、 程序计数器。
其中,方法区和堆,是进程共享的,而后面三个是进程私有的。下面进行逐一介绍。

程序计数器

对当前线程执行字节码的行数进行计数。JVM根据行数来选取要执行的操作语句。当然是线程私有的。
该区域不会发生任何 内存出界 OutOfMemoryError 的情况。(是唯一一个没有这个异常的区域。)

虚拟机栈

虚拟机栈,即通常提到的 “栈内存”。
每一个方法的调用,都对应着一个栈帧从入栈到出栈的过程。
虚拟机栈可能会抛出两种错误:

  • StackOverflowError,线程请求的栈深度大于JVM所允许的深度
  • OutofMemoryError,虚拟机栈扩展时候无法申请到足够内存。
本地方法栈

跟上面的虚拟机栈非常类似,不过本地方法栈是用来运行本地方法的。(之前说到过,Java是跨平台的语言,所以可能很多本地方法不是用 java 编写的。)

JVM中内存最大,线程共享的区域。
目的是存储对象实例。也是GC主要收集的区域。因为现代GC采用分代收集算法,所以堆也分为,新生代和老年代。
可以通过参数来配置堆内存,超过无法扩展时,出现 OutofMemoryError

方法区

线程共享的区域。存储 类信息、常量、静态变量、class 文件。GC也会运行在这一个区域,比如常量池的清理和类型的卸载。
也会抛出 OutofMemory.

这么理解,因为程序计数器、虚拟机栈、本地方法栈都是线程独立的。所以其生命周期都是随着线程相关的。
而对于线程共用的 heap/ non-heap来说,需要GC来及时释放资源。

常量池、运行时常量池、String常量池

首先要明白,这3个是不同的概念。

  • 常量池:全称应该是 Class常量池。是 .Class 编译文件的一部分,占其中绝大部分内存。其保存 字面量 & 符号引用。符号引用就不用说了,字面量就是 文本字符串、final定义的常量值、基本数据类型值等(肯定有常量啊,不然怎么叫常量池)。
  • 运行时常量池:存在于方法区中。对于String常量池的深入理解
    可以在运行过程中,向运行时常量池中添加常量,一般 String 的 intern() 方法就是做这个的。

在类加载之后,被类加载器,将类的常量池复制到运行时常量池中(那自然对应里面保存的内容就是上面提到的常量池保存的内容)。

  • String常量池:跟上面二者都是独立的。
  1. 为什么要有String常量池:为了缓解频繁的赋值删除操作带来的开销。
堆和栈的区别

基本数据类型和局部变量是存放在栈中的,用完就消失。
new的实例化对象及数组,是放在堆中的。用完了靠GC来处理。
堆和栈的区别


一个对象的创建

在上面了解了内存结构的基础上,我们具体来看一下对象的创建过程是怎样的(这里是以 HotSpot 虚拟机为例的)。
检查--内存分配(指针碰撞&空闲列表)--初始化

检查

当一条 new 指令进来的时候,首先就要检查是否在常量池中可以定位到类的符号引用,并检查这个类是否已经完成了相关加载。否则,执行加载。

内存分配

两种情况

  1. heap 中的内存是规整的,有一个指针指示器来界定用过的内存和没有用过的内存。那么分配内存就是简单的挪动这个指针的过程,这种分配方式叫做“指针碰撞”。
  2. heap 不规整。JVM需要一个列表记录哪些内存块是可以用的。这种分配方式叫“空闲列表”。

除此之外,为了解决内存分配时的并发问题,有两种解决策略:CAS + 失败重试,保证操作的原子性 && 做一个缓冲区内存。

初始化

在上面两步的基础上,执行 <init> 方法,完成具体由程序员规定的初始化。


对象的内存布局

对象在内存中储存的布局可以分为3块:对象头、实例数据、对齐填充

对象头包括两部分:

a) 存储对象自身运行时数据(Mark Word)。哈希码、GC分代年龄等。
b) 类型指针。指向其 类的元数据的指针。用来确定该对象所属哪个类。

实例数据

对象真实有效的信息,就是代码中定义的字段。

对齐填充

不是必存在的。占位符。因为 JVM 中会要求对象起始地址必须是8的整数倍。


对象的访问定位

两种访问定位方式:使用句柄访问、使用直接指针访问

句柄访问

Java堆中划分出来一个 句柄池,reference 存的是句柄地址,句柄又指向对象示例数据和其他数据。

  • 优点:对象被移动的时候,只需要改句柄,在大量移动发生的时候,效率高


    句柄访问对象
直接访问
直接访问对象

6.4 JVM 类加载机制

类加载,即将编译后的 .class 文件(字节码)加载到内存,并且进行相关的校验、转换解析、初始化,最终形成可以被虚拟机直接使用的 Java 类型。
需要注意的是,Java 中 类的加载和连接过程是在程序运行期间完成的。

类的生命周期

一个类从加载进JVM内存到从内存中卸载,一共要经历 loading加载、verification验证、preparation准备、resolution解析、initialization初始化、using使用、unloading卸载这7个阶段。
下面我们逐一看一下每一个阶段做了些什么。

加载 Loading

加载就是为了找到相关的字节码并且将其放入JVM内存中的指定位置。

  • 首先,通过类名获取类的二进制字节码
  • 然后,将字节码代表的静态存储结构转换成为方法区的运行时数据结构
  • 在 Java 堆中生成代表这个 Class 的对象,作为方法区这些数据的访问入口。

首先,加载字节码;然后,结构转换;最后,生成Class对象。

验证 verification

验证主要是为了验证加载的字节流是否符合JVM的要求,并且不会危害JVM自身安全(某些恶意代码)。

  • 文件格式验证
    验证文件格式和版本号等。
  • 元数据验证
    主要进行语义语法验证。
  • 字节码验证
    主要进行安全验证,确保类的方法不会危害虚拟机。
  • 符号引用验证
    对类自身以外的信息进行验证。主要发生在解析阶段。
准备

为 类变量(static修饰的变量)分配内存(方法区中)和赋初值 的阶段。

public static int value = 123;

经过准备阶段被赋值成为 0;

public static final int value = 123;   // 被 final 修饰,存在 ConstantValue 属性。

经过准备阶段被赋值成为 123,存在 方法区 中。

这也就解释了为什么 static 变量的赋值情况和方法无关。甚至在调用方法前都已经存在了。

解析

解析,是把常量池内的符号引用替换为直接引用的过程。
符号引用:在一个类中,引入了其他的类。但是JVM并不知其他的类在哪里,就用一个唯一的符号来代替。
直接引用:一个地址用来找到被引入的类(所以此时被引入的类应该在内存中存在了。),可以是直接指针、偏移量、句柄(智能指针)等。

初始化

初始化就是执行类构造器<clinit>()方法的过程。
<clinit>() 方法:类变量赋值语句和 静态语句块合并产生的方法。

注意这里的类构造器方法,不同于类的构造函数(实例构造方法<init>())的。这里的类构造方法不用显示调用父类的类构造方法(而实例构造方法是需要的),执行顺序是,父类构造方法执行完毕,再执行子类构造方法。

虚拟机会保证,一个类的<clinit>() 方法是同步的。

类主动引用

这里我们看完了上面的5个过程。
但是并不是每一种情况,都会有初始化的 阶段。 有初始化阶段的情况,叫做类的主动引用。
这里作者给出了5种类的主动引用的情景。

类被动引用

除了上面说的类主动引用的情景。其他的都是不会有初始化阶段的类的被动引用情景。
这里同样,作者也给出了3种情况。


6.5 类加载器

这一部分对应 JVM 书中的 7.4 小节。
实际上类加载中 “通过类的全限定名来获得描述此类的二进制字节流” 这个动作,是在 JVM 外部实现的,而在外部实现加载这个过程的代码就叫做,类加载器。

双亲委派模型

要理解双亲委派模型,首先就要了解一下 java 中的类加载器。
只存在两种不同的类加载器:启动类加载器(Bootstrap CLassLoader) & 所有其他类加载器。前者用C++ 实现,是JVM一部分;后者用 Java 实现,独立于 JVM,继承自抽象类 Java.lang.ClassLoader。

  1. 启动类加载器
    启动类加载器(Bootstrap ClassLoader)用来加载 Java的核心库,负责将 <JAVA_HOME>/lib 下面的核心类库(比如 dt.jar)加载到内存中。
    Java 程序无法获取 Bootstrap ClassLoader 的引用,如果在类加载时需要将加载请求委派给 Bootstrap ClassLoader,直接写 null 即可。
  2. 其他类加载器
  • 扩展类加载器(Extension ClassLoader)
    用来加载 Java 的扩展类库(jre/ext/*.jar),JVM 会提供一个扩展库目录,扩展类加载器在这个目录中查找并加载 Java 类。
  • 应用程序类加载器(Application ClassLoader)
    根据 (用户类的路径) CLASSPATH 来加载 Java 类。一般来说, Java 英勇的类都是它完成加载的,可以通过 ClassLoader.getSyetemCLassloader() 来获取它。
  • 自定义类加载器
    程序员可以继承 java.lang.CLassLoader 类,来自定义加载器。

将这些加载器都画在双亲委派模型中,即为下图


双亲委派模型图
双亲委派模型定义

除了顶层的启动类加载器外,其他的类加载器都应该有自己的父类加载器。
注意这里的父类并不是继承关系,而是采用组合关系来服用父类加载器的相关代码

这里翻译是 parents,指很多 parent,并不是指父母,所以其实没有“双亲”,只有爸爸和爷爷们。

工作过程

一个类加载器收到了类加载请求,首先他不会自己尝试加载这个类,而是委派请求给父类加载器完成。因此,所有的加载请求都是到了顶层的启动类加载器。
只有当父类加载器无法完成加载请求(在自己的搜索范围内找不到要加载的类),子加载器才会尝试去加载。

优点
  1. 避免类的重复加载
    双亲委派机制使得 Java的类 随着 类加载器一起 具备了一种优先级测层次关系,通过这种层次关系可以避免类的重复加载,当父类已经加载了,子类就不用再加载了
  2. 防止 核心API库被随意篡改
    如果从网络中传来一个 Java.Lang.Integer 的类,通过双亲委派模型传递到启动类加载器,启动类加载器自己的搜索空间(核心 java API)中发现了这个类名字,已经加载了,就不会再重新加载传来的恶意类。避免了核心 API 被篡改。

书上说了一个特例,就算自己定义了一个类加载器,去加载一个 java.lang开头的类,被一层层向下返回到自己定义的加载器中之后,也是不会加载成功的。因为java.lang 是 核心 API包,需要访问权限,报错。

实现方式

双亲委派模型的实现代码,都是集中在 java.lang.ClassLoader 的 loadClass() 方法中。
逻辑如下:

  • 检查该类是否被加载过
  • 没有加载,调用父类加载器的 loadClass() 方法
  • 如果父类加载器为空,则使用 启动类加载器作为父类加载器
  • 如果父类加载失败,抛出 ClassNotFoundException异常,调用自己的 findClass() 方法。

每一层都是这样,一层中只关心自己的父类和自己。

打破双亲委派

重写 loadClass() 方法。

java中 没有使用 双亲委派模型加载的地方

JDBC中双亲委派模型的破坏
解释2

JDBC 中就是破坏了双亲委派模型的。
为什么要破坏双亲委派模型
原生 JDBC 中的 Driver 驱动只是一个接口,并没有具体的实现,具体的实现是由不同的数据库类型去实现的(比如,MySQL 的 mysql-connector-.jar包中的实现)。
而原生的 JDBC中的 类是在 %JAVA_HOME%/bin/lib/rt.jar 包中的,是由 bootstrap 启动类加载器去加载的。
但是,JDBC 中的 Driver (驱动)类需要去加载不同数据库类型的 Driver类,而 mysql-connector-.jar 中的Driver类是用户自己写的代码,那肯定不是 bootstrap 加载器可以加载的,应该用 application 类加载器去加载。
这个时候就用到了一个 线程上下文类加载器(Thread Context ClassLoader),用这个东西,就可以把 原本需要启动类加载器加载的类,用应用程序类加载器加载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容