JVM入门学习

走进Java

JDK(java development kit):
包括Java程序设计语言、Java虚拟机、Java API类库
JRE(java runtime Environment):
包括Java虚拟机、Java API类库中java se api子集
TCK(Technology Compatibility Kit):
运行平台兼容Java语言的测试
Fork/Join框架
是Java7提供一个用于并行执行任务的框架。
把一个大任务分割成若干个小任务,最终汇总每个小任务结果
后得到大任务结果的框架。
OpenJDK:
是Sun公司在2006年末把Java开源而形成的项目
新生代GC(Minor GC):回收频繁,回收速度快
老年代GC(Major GC/Full GC
GC堆(Garbage Collected Heap
停顿所有java执行线程(Stop the world

内存动态分配和垃圾收集

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

对于java程序员来说,不需要为每一个new操作去配写对应的delete/free代码,而是由虚拟机去管理内存

Java虚拟机运行时数据区

在这里插入图片描述

1. 程序计数器
  • 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
  • 由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
2. Java虚拟机栈(栈内存)
  • 也是线程私有的,它的生命周期与线程相同。
  • 虚拟机描述的是java方法(字节码)执行的内存模型: 每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出入口等信息。
  • 每一个方法从调用到完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 局部变量表 存放了编译期可知的各种基本数据类型(boolean、int、double、long、short、byte、char、float)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其它与此对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)。
  • 其中64位长度的long和double类型的数据会占用2个局部变量空间,其余数据类型只占用一个。局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 栈内存的两种异常:如果请求线程的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常,若虚拟机动态扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。
3. Java堆(堆内存)
  • 是java虚拟机所管理内存中最大的一块,用来**存放对象实例,数组
  • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • Java堆是垃圾收集器管理的主要区域,因此也被叫做GC堆(Garbage Collected Heap)
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
  • 如果在堆中没有内存来完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。
4. 方法区
  • 各个线程共享的内存区域,用于储存已被虚拟机加载的类信息(类名、访问修饰符、字符描述、方法描述等)、常量、静态变量、即时编译器编译后的代码等数据。
  • java虚拟机规范把方法区描述为堆的一个逻辑部分,但它的别名叫非堆(Non-Heap),目的是与java堆区分开。
  • 内存空间不需要连续内存,可以选择固定大小或可扩展的内存,还可以选择不实现垃圾收集。
    这个区域内存回收目标主要是针对常量池的回收和对类型的卸载。
  • 当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
运行时常量池

方法区的一部分

  • 用来存储编译器生成的各种字面变量和符号引用,还有翻译出来的直接引用。
  • 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,即并不要求常量一定只有编译器才能产生,运行期间也可能将新的常量放入池中。
5. 本地方法栈

Native方法执行,也会抛出StackOverflowError异常和OutOfMemoryError异常。
与虚拟机栈发挥作用相似。

HotSpot虚拟机对象奥秘

对象的创建

虚拟机遇到一条new指令时,

  1. 先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用的类是否被加载、解析和初始化过。如果没有,则先执行相应的类加载过程。
  2. 接下来虚拟机为新生对象分配内存。对象所需的内存大小在类加载完成后即可确定,为对象分配内存空间的任务等同于把一块确定大小的内存从java堆中划分出来。
  3. 虚拟机需要将分配到的内存空间都初始化为零值。
  4. 虚拟机对对象进行必要的设置。
  5. 执行方法,完成对象的初始化。
对象的内存布局

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

HotSpot虚拟机对象头包括两部分:

  1. 用于存储对象自身的运行时数据。
    如哈希码、GC分代年龄、锁状态标志、偏向线程ID等
  2. 类型指针
    虚拟机通过该指针来确定这个对象是哪个类的实例。
    如果对象是一个数组,那对象头中必须有一块记录数组长度的数据

实例数据:对象真正存储的有效信息
对齐填充:仅仅起占位符的作用。

对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。
reference类型:一个指向对象的引用。
两种访问对象的方式:

  1. 句柄访问
    首先java堆划分一块内存作为句柄池,reference中存储对象的句柄地址,句柄中存储对象实例数据和对象类型数据地址信息。
    优点:在对象被移动时只改变句柄中的实例数据指针,而reference的指针不需要改变。
  2. 直接指针
    reference中存储对象地址。
    优点:访问速度快

总结

程序计数器-------线程所执行的字节码的行号指示器
java虚拟机栈-------执行java方法服务,储存局部变量表、方法出入口等
本地方法栈--------执行native方法
java堆-------存储对象实例
方法区-------存储类的相关信息

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而进行入栈和出栈操作,每一个栈帧中分配多少内存在类结构确定时就已知。
因此这几个区域的内存分配和回收都具备确定性。

垃圾收集与动态内存分配

GC(Garbage Collection):垃圾收集,GC历史比java久远

引用类型

强引用
GC器永远不会回收被引用的对象,强引用在程序中普遍存在
软引用
还有用但非必须回收的对象、这些对象被列进第二次回收范围
弱引用
描述非必须对象,这些对象被列进第一次回收范围
虚引用
无法通过虚引用来获取一个对象实例。
虚引用的目的是该对象在被GC器回收时能够得到一个系统通知。

什么对象应该被回收?

不可能再被任何途径使用的对象

怎样判断对象不可用(对象存活判定算法)?
  1. 引用计数算法(不采用)
    给对象添加一个引用计数器,被引用时,计数器加一,引用失效时减一,
    任何时刻为零的对象就是不可用
    缺点:难以解决对象间互相引用的情况。
  2. 可达性分析算法
    通过一系列“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜素
    走过的路径称为引用链,当GC Roots与一个对象之间没有引用链时,则能够证明
    这个对象不可用。
    可作为GC Roots的对象:
		1.	虚拟机栈(栈帧中的本地变量表)中的引用对象。
		2.	方法区中类静态属性引用的对象
		3.	方法区中常量引用的对象
		4.	本地方法栈中JNI(即native方法)引用的对象
对象不可用时,就会被GC吗?

不是。
标记-清除算法过程:

  • 可达性分析得到的不可用对象,将会被进行第一次标记并且进行一次筛选。
    筛选的条件是此对象是否有必要让虚拟机调用自救方法。
  • 当对象没有覆盖自救方法或者自救方法被虚拟机调用过,则没必要执行;若对象被判定为有必要执行自救方法,那这个对象将会被放在一个叫做F-Queue的队列中,并在稍后由虚拟机建立一个低优先级的Finalizer线程去执行它,但并不承诺会等待它运行结束。
  • 稍后GC回对F-Queue进行第二次标记,若对象在自救方法中与引用链上的任何一个对象建立关联,则不会被回收;否则,该对象基本上被回收了。
方法区(永久代)的垃圾收集主要回收:

废弃常量和无用的类。

垃圾收集算法(收集方法)

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收
缺点:1.效率低2.空间碎片太多

复制算法

将内存按容量分为等大两块,每次使用其中一块。当这块使用完了,就将存活的对象复制到另一块上,再把使用完的那块内存空间清理掉。

标记-整理算法

首先标记出所有需要回收的对象,让所有存活的对象向内存一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

把java堆分为新生代和老年代,
在新生代中,每次GC都有大量对象死去,少量存活,采用复制算法。
在老年代,对象存活率高,使用“标记-清理”或“标记-整理”算法进行回收。

如何进行垃圾回收

程序执行到安全点或者处在安全区域内。

GC进行时,必须停顿所有java执行线程(Stop the world)原因:
  1. 枚举根节点时,不可以出现分析过程中对象引用关系还在不断变化的情况,
    虚拟机不需要检查完所有的引用位置,而是有办法知晓哪些地方存放着对象引用(即安全点,只有到达安全点时程序才能停顿执行,开始GC)
安全区域(Safe Region):
  • 安全点(Safepoint)机制保证程序执行时,在不太长时间内就会遇到可进入GC的安全点。
  • 当程序不执行时,比如程序处在Sleep状态或者Block状态,就需要使用安全区域进行GC。
  • 安全区域是指在一片代码片段之中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的。

垃圾收集器(内存回收具体实现)

垃圾收集器选择原则:对具体应用来选择最合适的收集器。不存在万能的完美收集器。

Serial收集器

单线程收集器,在进行GC时,必须暂停其它所有的工作线程,直到它GC结束。
优点:简单高效
新生代采用复制算法暂停所有用户线程
虚拟机对运行在Client模式下默认新生代的收集器

ParNew收集器

Serial收集器的多线程版本。
采用复制算法暂停所有用户线程
对运行在Server模式下的虚拟机中首选的新生代收集器

并发与并行:
并发: Concurrent,用户线程与垃圾收集线程同时执行(并不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集程序可能在另一个cpu上。
并行: Parallel,多条垃圾收集线程并行工作,此时用户线程处于等待状态。

Parallel Scavenge收集器

新生代收集器,使用复制算法,并行多线程收集器
目标:达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+GC时间)。
停顿时间越短则越适合与用户交互的程序。高吞吐量则可以高效利用cpu时间,尽快完成程序运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old收集器

Serial收集器的老年代版本,单线程,使用标记-整理算法。
给Client模式下的虚拟机使用
若在Server模式下使用用途:作为CMS收集器后备方案。

Parallel Old收集器

是Parallel Scavenge 的老年代版,多线程,标记-整理算法

CMS收集器(Concurrent Mark Sweep)

是一种以获取最短回收停顿时间为目标的收集器。
基于“标记-清除”算法。
运作过程:初始标记、并发标记、重新标记、并发清除
优点:并发收集、低停顿
缺点:对cpu资源敏感、无法处理浮动垃圾(Floating Garbage)、空间碎片多

G1收集器(Garbage-First)

将整个java堆划分为多个大小相等的独立区域(Region),G1跟踪各个region内垃圾堆积价值的大小,G1优先回收价值最大的Region。内存的化整为零
面向服务端
运作过程:初始标记、并发标记、最终标记、筛选回收
特点:1.并发与并行2.分代收集3.空间整理4.可预测的停顿

内存分配

堆上分配(也可能经过JIT编译后被拆散为标量类型并间接的栈上分配)

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。

大对象直接进入老年代

大对象:需要大量连续存储空间的java对象。

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

虚拟机给每个对象定义了一个对象年龄计数器,若在Eden出生并经过第一次Minor GC后仍能存活的对象,并且可以被Survivor容纳则该对象将会进入到Survivor空间中,对象在Survivor中当年龄增加到一定岁数时(15岁)将进入老年代。每经过一次Minor GC年龄就增加一岁。

动态对象年龄判定

并不是对象年龄增加到一定岁数时才能够进入老年代,若在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保

把Survivor无法容纳的对象直接进入老年代。

类文件结构

Java虚拟机提供语言无关性:支持其它语言运行在jvm上
实现语言无关性的基础任然是虚拟机和字节码存储格式。

Class类文件结构

Class文件是一组以8位字节为基础单位的二进制流,中间没有添加任何分隔符,Class文件格式有两种数据类型:无符号数和表。
无符号数属于基本数据类型,以u1、u2、u4、u8来分别表示一个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成的字符串值。
表是由多个无符号数或其它表作为数据项构成的复合数据类型,所有表习惯性以“_info”结尾。整个Class文件本本质就是一张表。

魔数与class版本

每个class文件头四个字节称为魔数,作用是确定这个文件是否为虚拟机接受的class文件。
第五和第六个字节是次版本号,第七和第八个字节是主版本号。
主版本后是常量池入口,常量池后的两个字节代表访问标志

常量池

主版本后是常量池入口,常量池可以理解为class文件之中的资源仓库。
Class文件中只有常量池的容量是从一开始。
常量池主要存放两大类常量:字面量和符号引用。
Java代码在javac编译时,在class文件中并不会保存各个方法、字段的最终布局信息。
而是在虚拟机运行时,需要从常量池获得对应符号的引用,再在创建或运行时解析,翻译到具体的内存地址之中。

访问标志

常量池后的两个字节代表访问标志,用于标识一些类或者接口层次的访问信息

类索引、父索引、接口索引集合

Class文件由这三项来确定这个类的继承关系
类索引确定这个类的全限定名
父类索引确定这个类父类的全限定名。
除了java.lang.Object类外,所有java类的父类索引都不为0.

字段表集合

用于描述接口或类中声明的变量。
字段包括类级变量和实例级变量,但不包括方法内部声明的局部变量。

方法表集合

方法表结构和字段表结构一样,依次包括访问标志、名称索引、描述符索引、属性表集合

属性表集合

在class文件、字段表、方法表都可以携带自己的属性表集合。

  1. code属性
    在整个class文件中,code属性用于描述代码,所有其它数据项用来描述元数据(包括类、字段、方法定义及其它信息)
  2. Exceptions属性、LineNumberTable属性、LocalVariableTAble属性
  3. SourceFile属性、ConstantValu属性、InnerClasses属性
  4. StackMapTable属性等

类的生命周期

加载-》验证-》准备-》解析-》初始化-》使用-》卸载
验证:确保class文件的字节流中包含信息符号当前虚拟机的要求。
文件格式验证、元数据验证、字节码验证、符号引用验证
准备:为类变量在方法区分配内存并设置类变量的初始值。
解析:将虚拟机内的符号引用替换为直接引用的过程。
初始化:是执行类构造器()方法的过程。

虚拟机字节码执行引擎

执行引擎在执行java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)
所有java虚拟机执行引擎都是一致的:输入的是字节码文件、处理过程是字节码解析的等效过程、输出的是执行结果。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值