Java虚拟机相关知识整理

时间:2021-12-05 10:26:01
1.java的内存区域
线程共享
(1)堆:是java虚拟机所管理的内存中最大一块,用于存放对象实例,所有对象实例和数组都在堆上分配
基于垃圾收集器分为新生代和老年代,并可以进一步划分为Eden,survivor,默认比例为8:1 Eden、From survivor、To survivor三个部分
依据是否开启线程私有的分配缓冲区(TLAB)
也存在OutOfMemoryError异常
(2)方法区:类信息,常量,静态变量等编译器编译之后的代码等数据
会产生内存OutOfMemoryError异常,这个区域内存的回收主要是常量池的回收和对类型的卸载
线程隔离
(3)java虚拟机栈:执行java方法
对于每一个执行的方法都有一个栈帧(存放局部变量表,返回出口,操作数栈,动态链接等)当超过栈的大小时会出现*Error异常,当无法申请足够的内存会抛出OutOfMemoryError异常
(4)本地方法栈:执行Native本地方法,其他部分和java虚拟栈类似
(5)程序计数器:用于记录java字节码指令的地址,从而保证线程切换之后能恢复到执行的位置,每个线程都有一个线程程序计数器,唯一没有规定OutOfMemoryError异常

常量池:是方法区的一部分,主要存储编译器产生的各种字面量和符号引用
直接内存:在jdk1.4引入的NIO类,引入基于通道和缓冲区的i/o方式,使用native函数库赖直接分配堆外内存,并通过存储在java堆中的DirectByteBuffer来操作这部分内存,也存在OutOfMemoryError异常
内存溢出和内存泄漏之间的区别:
内存泄漏:内存泄漏是指程序申请内存后,无法释放已分配的内存,内存的泄漏会导致最终内存被耗光
内存溢出:内存溢出时指程序在申请内存时,没有足够的内存空间可以使用

2.对象的创建
虚拟机遇到一个new指令,首先检查指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否被加载、解析和初始化过,若没有则加载,则涉及类加载过程
若已经加载,接下来为新生对象分配内存,二种分配方式,具体依据Java堆是否规整,若规整则采用指针碰撞(也就是将指针向未使用的堆移动对象的大小),反之使用空闲列表,具体的取决于垃圾收集器是否带有压缩整理的功能。
分配内存对多线程还需要考虑同步的问题。然后对对象进行必要的设置,对象头中(哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄)执行new指令之后,还要执行<init>方法,把对象按照程序员意愿初始化,才产生一个完全的对象

3.对象在内存的布局
(1)对象头(哈希吗,分代年龄、锁状态标志、偏向线程id,第二部分为类型指针,指向它的类元数据指针,用来确定属于哪个类的实例,如果是数组还有一个记录数组长度的数据)
(2)实例数据:对象真正有效的数据
(3)对齐填充:不是必然存在的,保证是8字节的整数倍

4.对象访问定位
(1)使用句柄:在堆中分出一块内存作为句柄池,存储对象的句柄地址(包括对象实例数据和对象类型数据的具体地址信息)
(2)直接指针:存放对象的地址(速度快)
jvm中的常见参数的设置:
-Xmx 用于指定堆大小的最大值
-Xms 用于指定堆大小的最小值 一般使得最大值和最小值相等来固定堆的大小
-Xmn 堆中新生代的大小
-Xss 指定栈的大小

5.对象是否存活判断方法
(1)引用计数(存在相互循环引用的问题,导致无法回收)java并没有采用这种方式,可以使用相互引用来测试
(2)可达性分析:选择一些GC roots作为起始点向下搜索,走过的路径形成引用链,当一个对象没有任何引用链相连,则为不可达
作为GC Roots的点:虚拟机栈和本地方法栈中引用的对象,方法区中类静态属性和常量引用的对象

6.四种引用
(1)强引用:最常见的A a = new A();这种引用,只要强引用在,不会回收所指向的对象
(2)软引用:用来描述有用但非必须的对象,当系统要发生内存溢出时,将这些对象列入回收范围进行第二次回收
(3)弱引用:和软引用类似,但强度更弱,只能生存到下一次垃圾收集之前
(4)虚引用:使用虚引用的目的是在对象被收集器回收时能收到一个系统通知
其中定义其他引用的目的是为了缓存

7.回收方法区
主要对常量和类进行回收,对常量的回收只需要判断没有其他地方用到该常量即可
对类的回收需要满足以下:
类的所有实例已经被回收
类的加载器已经被回收
类对应的java.lang.Class对象没有在任何地方引用,无法通过反射访问类的方法

8.垃圾回收算法
标记-清除(mark-sweep)
首先标记出所有需要回收的对象,然后统一回收,存在效率问题,标记和清除都效率不高,并且回收之后存在大量不连续的碎片,并导致以后程序需要分配大对象时不得不提前出发一次垃圾回收动作
复制
将内存分为相等的二块,每次使用其中一块,当用完了就将还存活的对象复制到另一块上,然后把这一块全部清理掉,实现简单,运行高效但是内存被缩小为一一半了,虚拟机中一般是采用eden,survivor,survivor,其中一块survivor空闲,但是当复制时survivor不足够存放还存活的对象,需要使用老年代来担保,这个可行的原因是新生代中的绝大多数对象都是朝生夕死的
标记-整理
前面处理和标记清除类似,就是在后续处理中是将所有存活的对象移动到一端

9.枚举根节点,安全点和安全区域
GC停顿(stop the word)为保证一致性,在可达性分析中需要停止执行线程
解决办法是在特定位置记录栈和寄存器中那些位置是引用,这样的位置被称为安全点,安全点的选择一般是是否具有让程序长时间执行为特征来选定,因为如果每条指令执行时间都很短,不会因为指令序列过长而长时间运行

10.垃圾收集器
新生代:Serial、ParNew 、Parallel Scavenge 采用复制收集算法
老年代:cms(concurrent mark sweep)、Serial old 、Parallel old 采用标记整理或者标记清除
G1
Serial收集器:一个单线程收集器,使用它时必须暂停其他所有工作线程,直到收集结束。简单而高效,并且没有线程交互的开销,可以获取最高效率的单线程收集效率。一般client模式
ParNew收集器:其实就是Serial的多线程版本,除了serial外只有他能和cms配合使用,在Server模式使用
Parallel Scavenge收集器:更加关注吞吐量(用户代码运行时间/cpu总消耗时间),主要适合在后台运算而不要太多交互的任务
Serial Old收集器:使用标记整理收集算法,单线程收集器,主要在client模式使用,如果在server模式,主要有二个用途,一是jdk1.5以及之前和parallel Scavenge配合使用,二是当cms发生cmf(concurrent model failure)时作为后备方案
Parallel old收集器:采用多线程和标记整理,但是仍然不能实现用户线程和垃圾回收线程并发执行
CMS收集器:主要目的是获取最短的回收停顿为目标的收集器,适合追求服务响应速度,停顿时间短,用户体验高的需求,标记清除
分为四个过程:
初始标记、并发标记、重新标记、并发清初
初始标记主要对Gc root能直接关联的对象进行标记,初始标记和重新标记仍然会stop the world,但是这二个过程耗时很少
而比较耗时的引用trance过程和清除过程可以与用户线程并发执行
缺点:对cpu敏感、对浮动垃圾(一起并发中产生的垃圾,可能出现concurrent model failure)而导致full gc、最后就是使用标记清除,存在碎片
G1收集器:
特点:并发与并行、分代收集、空间整合(整体标记整理,局部复制算法)、可预测停顿
将整个java堆划分为多个大小相等的区域,虽然保留新生代和老年代概念,但是不再物理隔离,有计划的避免整个堆得垃圾回收,而是跟踪每个垃圾堆积的价值(回收所能获得空间大小和回收所需要时间的经验值),维护一个优先队列表,每次回收价值最大的区域,并且每个区域都有一个renmenber set来保证不用全堆扫描
过程:
初始标记、并发标记(这段时间对象变化记录到remembered set logs中)、最终标记(合并logs到remembered set中)、筛选回收
新生代:主要采用停止-复制回收算法
老年代:主要采用标记-清除、标记-整理回收算法
Minor GC:发生在新生代的垃圾收集动作,回收频繁,速度较快
Full GC:发生在老年代,一般会至少伴随一次Minor GC,速度很慢

11.类文件结构
(1)Class文件主要采用类似c语言数据结构的伪结构来存储数据。只有无符号数和表二种数据类型
无符号数:u1 u2 u4 u8
表:由多个无符号数或者其他表符合而成
一般前四个字节为魔数CA FE BA BY,接着u2 的次版本号,u2的主版本号
接着是常量池的常量的个数和每个常量(记录字面量和符号引用)
最后包含字段、方法等的个数和描述

12.类加载的整个生命周期
加载、连接(验证、准备、解析)、初始化、使用、卸载
类和接口加载过程的区别:当一个类在初始化时,要求其父类全部都已经加载,但是对接口来说并不要求其父接口全部都完成初始化,只有在真正使用到父接口(如父接口中的常量?这一点是否和类不一样,使用类中的常量并不会引起初始化)的时候才会初始化
加载:通过一个类的全限定名来获取定义此类的二进制字节流,然后将字节流所代表的静态数据结构转化为方法区的运行时数据结构,最后生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组类并不由类加载器创建,而是由虚拟机直接创建

验证:为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全
如果验证到文件不符合Class文件格式的约束,就会抛出java.lang.VerifyError异常或其子类异常
(1)文件格式验证:是否已魔数开头 0xCAFEBABE、主次版本号是否为处理机的处理范围、常量池中常量是否有不被支持的常量类型、指向常量的各种索引值是否有不存在的常量或者不符合类型的常量等。主要是为了保证字节流能正确的解析并存储在方法区内,以保证符合Java类型信息的要求,只有通过这个验证才会进入内存方法去存储
(2)元数据验证:对字节码描述的信息进行语义分析,保证符合java语言的规范
这个类是否有父类、父类是否继承了不允许继承的类、这个类是否为抽象类、是否实现了父类或者接口中需要实现的所有 方法等
(3)字节码验证:通过数据流和控制流来确定程序语义是否合法、符合逻辑,保证 校验类的方法在运行时不会做出危害虚拟机安全的 事件
(4)符号引用验证:字符串描述的全限定名能否找到对应的类、制定类中是否符合方法字段描述符以及简单名称所描述的方法和字段、类、字段、方法的可访问性

准备:是正式为类变量分配内存并设置类变量初始值得阶段 但是对于static final修饰的会直接赋予程序中的值,都在方法区中分配
解析:虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:就是以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能够无歧义的定位到目标就可以,与虚拟机的内存布局无关,引用的目标不一定已经加载到内存中
直接引用:可以直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄,与虚拟机的内存布局相关,目标在内存中存在
类或接口的解析
字段的解析
类方法的解析
接口方法的解析

初始化:这个阶段才真正开始执行java代码(字节码),初始化阶段是执行类构造器<clinit>()方法的过程((1)这个方法由编译器收集类中所有类变量和静态语句块中的语句合并而成的,收集的顺序由语句在源文件中出现的顺序决定的,注意:静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,静态语句可以赋值,但是不能访问 (2)这个方法和构造函数不同,不需要显示调用父类的构造器,虚拟机会保证在调用子类该方法时,父类的该方法已经执行完毕(3)如果类或者接口没有静态语句块,也没有对变量的赋值操作,编译器就不会为这个类生成该方法(4)接口执行该方法时不需要先执行父接口的该方法,接口实现类的初始化也不会执行接口的该方法,只有当接口中定义的变量使用时,父接口才会使用(5)虚拟机会保证类的该方法在多线程环境下呗正确的加锁,同步)

13.类加载器
对于任意一个类,它需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性
双亲委派模型
从java虚拟机的角度:启动类加载器(Bootstrap classloader)和其他(全部都继承java.lang.ClassLoder)。
从java开发人员:启动类加载器、负责<java_home>/lib目录中的类库、扩展类加载器(extension classloader)<java_home>/lib/ext目录中、应用类加载器(application classloader)也称为系统类加载器,负责加载用户类路径上指定的类库,一般情况下是程序中默认的类加载器
但是注意加载器的父子关系是通过组合来实现的而不是继承
双亲委派模型工作流程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都是应该传送到顶层的启动类加载器中,只有父类加载器反馈无法完成这个请求,子加载器才会尝试自己去加载
好处:让java类随着它的类加载器一起具备了一种带有优先级的层次关系

13.破坏双亲委托
1.在jdk1.2发布之前
2.双亲委托的缺陷:当基础类需要调用回用户的代码(比如JNDI),解决这个问题使用了一个不太优雅的设计:线程上下文类加载器(thread context classloader),这个类加载器请求子类加载器去完成类加载的动作(JNDI、JDBC)
3.第三次破坏:用户对程序的动态性的追求所导致的,代码热替换、模块热部署等
OSGI实现模块化的热部署,使用了自己的类加载机制:每个模块都有一个自己的类加载器,当需要替换一个模块是,就连同类加载器一起换掉实现代码的热替换
不再是树状结构而是网状结构
1)java.*开头的类委托给父类加载器加载
2)否则,将委托名单内的类委托给父类加载器加载
3)否则,将import列表的类委派给export这个类的bundle的类加载器加载
4)否则,查找当前bundle的classpath,使用自己的类加载器加载
5)否则,查找类是否在自己的fragment bundle中,如果在,则委派给fragment bundle的类加载器加载
6)否则,查找Dynamic import列表的bundle,委派给对应的bundle的类加载器加载
7)否则,类查找失败
osgi使得类加载之间为平级模型不在传统的层次模型,形成网状的结构,但是这样也增加了实现的复杂度,同时还可能出现思索

14.java内存模型
java线程---工作内存--(save和load操作)---主内存
主要目标就是定义程序中各个变量的访问规则,在虚拟机中将变量存储到内存和从内存中去中变量这样的底层细节
java内存模型定义了8中操作来完成上述操作
(1)lock
(2)unlock
(3)read
(4)load
(5)use
(6)assgin
(7)store
(8)write
volatile关键字:java虚拟机提供的最轻量级的同步机制
保证它修饰的变量对所有线程的可见性,是指当一个线程修改了这个变量的值,新值对其他线程来说是可以立即得知的
volatile也会存在不一致的情况,因为java中的运算可能会不是原子操作,导致变量并发下一样是不安全的,比如i++,实际上是三个操作(获取,+1,赋值)
volatile只保证可见性,并不能保证原子性
volatile的第二个特性就是防止指令重排序优化(通过插入一些内存屏障),指令的重排序要在不影响程序的正确结果的前提之下
并发过程主要围绕:原子性、可见性、和有序性三个特征
有序性除了依靠volatile和synchronized关键字来保证,更重要的是先行发生原则
(1)程序次序规则:就是在一个线程内,按照代码的顺序,前面的代码先行发生于后面的
(2)管程锁定原则:一个ulock必须先行发生于后面对同一个锁的lock操作
(3)volatile变量规则:对一个volatile变量的写操作先行发生于后边对这个变量的读操作
(4)线程启动规则:线程start()方法先行发生于其他动作
(5)线程的中止规则:线程的所有操作都先行噶剩余线程终止的检测
(6)对象终结规则:对象的初始化完成先行发生于finalize()方法的开始

15.线程
线程是进行处理机调度的最小单元。
状态:新建、运行(就绪和运行)、无限期等待、有限期等待、阻塞、结束

16.公平锁和非公平锁
依据是否按照请求锁的顺序
synchronized使用的是非公平锁
而lock提供了公平和非公平锁

17.锁的优化:
无锁
偏向锁
轻量级锁
重量级锁