深入理解JAVA虚拟机
2022-08-02 16:55:59 22 举报
AI智能生成
java虚拟机一图概括
作者其他创作
大纲/内容
性能调优
知识+工具+数据+经验
案列一
背景:效绩考核系统,会针对每一个考核员生成一个各考核点的考核结果,形成一个Excel文档,供用户下载。文档中包含用户提交的考核点信息以及分析信息,Excel文档由用户请求的时候生成,下载并保存在内存服务器一份。64G内存。
问题:经常有用户反映长时间卡顿的问题
处理思路:
优化SQL(无效)
监控CPU
监控内存发现经常发生 Full GC 20-30s,运行时产生大对象(每个教师考核的数据WorkBook),直接放入老年代,MinorGC不会去清理,会导致FullGC,且堆内存分配太大,时间过长。
解决方案:部署多个web容器,每个web容器的堆内存4G,单机TomCat集群
案列二
背景:简单数据抓取系统,抓取网络上的一些数据,分发到其它应用
问题:不定期内存溢出,把堆内存加大,无济于事,内存监控也正常。
处理方法:NIO使用了堆外内存,堆外内存无法垃圾回收,导致溢出
类记载机制
说明:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制,JVM是懒加载(节约系统资源)。
类加载的时机
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,但解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始。虚拟机规范严格规定了有且只有五种情况必须立即对象进行“初始化”;
加载
加载过程:
1.通过类型的完全限定名,产生一个代表该类型的二进制数据流(没有指明从哪里获取、怎么获取,是一个非常开放的平台),加载源包括:文件(Class文件,jar文件)、网络、计算生成(代理$Proxy)、由其它文件生成(jsp)、数据库中;
2.解析这个二进制数据流为方法区内的运行时数据结构;
3.创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口。
校验
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成4个阶段的校验工作:文件格式、元数据、字节码、符号引用。可以通过设置参数略过。
准备
准备阶段正式为类变量分配内存并设置变量的初始值,这些变量使用的内存都将在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化是随着对象一起分配在java堆中。
初始值通常是数据类型的零值;对于:public static int value=123,那么变量value在准备阶段过后的初始值为0而不是123,这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。对于:public static final int value =123;编译时javac将会为value生成ConstantValue(常量)属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123.
解析
说明:解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是符号约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在。
初始化
说明:到了初始化的阶段,才是真正开始执行类中定义的java程序代码
初始化阶段是执行类构造器<clinit>()方法的过程,它是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的渔具何冰产生的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
父类中定义的静态语句块要优于子类的变量赋值操作
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
类加载器
说明:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。
启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。getClassLoader()方法返回null。
扩展类加载器(Extension ClassLoader):这个加载器有sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):这个类加载器由sun,misc.Laucher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
说明:双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合的关系复用父类加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试这个加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系
虚拟机字节码执行引擎
运行时的栈帧结构
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
子主题
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Varisble Slost)为最小单位。一个Slost可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用。对于64位的数据类型(long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slost空间,是线程安全的
为了节省栈帧空间,局部变量Slost可以重用,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量的Slot就可以交给其他变量使用,这样的设计会带来一些额外的副作用,比如:在某些情况下,Slot的复用会直接影响到系统的收集行为。
操作数栈
操作数栈(Operand Stack)是一个后入先出栈,当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写和提取内容,也就是出栈/入栈操作
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;
方法返回地址
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
方法调用-解析
方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本(继承和多态)。
“编译期可知,运行期不可变”的方法*(静态方法和私有方法),在类加载器的解析阶段,会将其符号引用转化为直接引用(入口地址).这类方法的调用称为解析(Resolution)
方法调用-分派
静态分派最典型的应用就是方法重载;在运行期根据类型确定方法执行版本的分派过程称为动态分派。最典型的应用就是方法重写。
java语言的静态分派属于多分派类型,动态分派属于单分派类型
走进 java
JDK、JER与JVM之间的关系
JDK 全称为 java SE Development Kit (java 开发工具),提供了编译和运行java程序所需的各种资源和工具,包括:JER+java开发工具
JER 全称为 java runtime environment(java运行环境),包括:虚拟机+java的核心类库
JVM 是运行java程序的核心虚拟机
java的发展
java之父:詹姆斯.高斯林
最早语言为Oak,用于嵌入式系统,没有成功;
1995年互联网发展,改名为java,开始火爆,提出Write once run anywhere的原则;
1996年1月 发布JDK1.0,jvm为Sun Classic VM;
1996年5月 首届javaOne大会;
1997年2月 JDK1.1(内部类、反射、jdbc、javabean、rmi);
1998年 JDK1.2 发布J2Se J2EE J2ME swing jit Hotspot VM;
2000年5月 JDK1.3 Timer java2d;
2002年2月 JDK1.4 Struts Hibernate Spring 正则表达式 NIO 日志 Xml解析器;
2004年9月 JDK1.5(tiger)自动装箱拆箱 泛型 注解 枚举 增强for 可变参数 Spring2。X;
2006年 JDK6 javaSe javaEE javaME 提供脚本语言支持 支持http服务器api;
2009年 java7 Jigsaw模块化 Oracle(甲骨文)74亿收购Sun;
2014年 java8 Lambda表达式 函数式接口 方法引用 默认方法 Stream;
2017 java9 模块化
java 技术体系
java程序设计语言
各硬件平台上的java虚拟机
Class文件格式,可以自己设计语言,自己编写编译器,生成相同的class文件即可
java API
第三方的java类库
java虚拟机
Sun Classic VM
已经淘汰,是世界上第一款商用虚拟机,只能使用纯解释器(没有JIT Just in time编译器)的方法来执行java代码
Exact VM
Ecact Memory Management准确式内存管理;编译器和解释器混合工作以及两级及时编译器。
HotSpot VM
热点代码技术,使用最多的虚拟机产品,并非有Sun公司开发。官方JDK均采用 HotSpot VM
KVM
kilobyte 简单、轻量、高度可移植。在手机平台运行,运行速度慢。
J9
IBM开发 原名:IBM Techn0ology for java Virtual Machine IT4j
Dacik
不是Java虚拟机,寄存器架构而不是线结构,执行dex(dalvik Executable)文件
Microsoft JVM
只能运行在windows下面
Azul VM Liquid VM
高性能的java虚拟机,在HotSpot基础上改进,专用的虚拟机
Taobao VM
淘宝公司开发
java虚拟机的内存管理
说明:分为线程共享区和线程独占区
程序计时器
程序计数器(处于线程独)占区是一个非常小的内存空间,它可以看成是当前线程所执行的字节码的行号指示器。此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟字节码指令的地址。如果正在执行的是native方法,那么这个计数器的值为undefined。
注:java中没有goto,为保留字
注:java中没有goto,为保留字
虚拟机栈
虚拟机栈描述的是java方法执行的动态内存模型
本地方法栈
本地方法栈为虚拟机执行native方法服务
java堆
java虚拟机最大的内存区域,存放对象实例,也是垃圾收集器管理的主要区域,分为新生代(由Eden 与Survivor Space 组成)和老生代,可能会抛出OutOfMemoryError异常。
方法区
存储虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态常量,即时编译后的代码等数据,也可能会抛出OutOfMemoryError异常。方法区与永久代实际并不等价,对于HotSpot中才有永久代的概念。
运行时常量池
每一个运行时常量池都在java虚拟机的方法区中分配。直接内存:jdk1.4中增加了NIO,可以分配堆外内存(系统内存替代用户内存),提高了性能。
对象的创建
一个对象创建的过程为:1.new对象(对象的创建)2.根据new的参数在常量池中定位一个类的符号引用 3.如果没有找到这个符号引用,说明类还没有被加载,则进行类的加载、解析和初始化 3.虚拟机为对象分配内存(位于堆中)4.将分配的内存化为零值(不包括对象头)5.调用对象的<init>方法
如何在堆中给对象分配内存
说明:两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么垃圾回收机制了,如果压缩整理,可以使用指针碰撞的分配方式。
指针碰撞:假设java堆中内存是绝对规整的,所有用过的内存度放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,做分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
空闲列表:有一个列表,其中记录中哪些内存块有用,再分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录,这就叫做空闲列表。
线程安全性问题
有两个线程同时创建对象时,可能会造成空间分配的冲突,解决方案有:线程同步(但执行效率过低)或给每一个线程单独分配一个堆区域TLAB Thread Local Allocation Buffer(本地线程分配缓冲)
对象的结构
Header(对象头)
instanceData:数据实例,即对象的有效信息,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前
Padding:占位符填充内存
垃圾回收
说明:对于一般java程序员开发的过程中,不需要考虑垃圾回收
如何判定对象为垃圾对象
1.引用计数法
2.可达性分析法
如何回收垃圾对象
1.回收策略(标记清除、复制、标记整理、分带收集算法)
2.常见的垃圾回收器(Serial、Parnew、Cms、G1)
何时回收垃圾对象
判定垃圾对象
引用技术算法
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就加1,当引用失效的时候(变量记为null),计数器的值就减1.但java虚拟机中没有使用这种算法,这是由于如果堆内的对象之间相互引用,就始终不会发生计数器-1,那么就不会回收。
可达性分析法
说明:此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots)到这个对象不可达时,证明此对象不可用。
可作为GC Roots的对象:
1.虚拟机栈
2.方法区的类属性所引用的对象
3.方法区中常量所引用的对象
4.本地方法栈中引用的对象
垃圾回收算法
标记清除算法
先标记出要回收的对象(一般使用可达性分析算法),再去清除,但会有效率问题和空间问题:标记的空间被清除后,会造成我的内存中出现越来越多的不连续空间,当要分配一个大对象的时候,在进行寻址的要花费很多时间,可能会再一次触发垃圾回收。
复制算法
说明:复制算法是将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,浪费较大。
堆:
新生代、Eden 伊甸园
Survivor 存活期
Tenured Gen 老年区
老年代
标记整理算法
对于老年代,回收垃圾较少时,如果采用复制算法,则效率较低。标记整理算法的标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针
分代收集算法
针对不同的年代进行不同算法的垃圾回收,针对新生代选择复制算法,对于老年代选择标记整理算法
垃圾收集器
说明:java的应用很广,内存区域也很多,可以使用不同的垃圾收集器
Serial收集器
单线程垃圾收集器、最基本、发展最悠久。它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。偶尔用在桌面应用中。
ParNew收集器
可多线程收集垃圾,收集新生代,使用收集算法
Parallel收集器
多线程收集垃圾,收集新生代,使用收集算法。Parallel收集器更关注系统的吞吐量,可以通过参数来打开自适应调节策略
吞吐量:CPU用于运行用户代码的时间与CPU消耗的总时间的比值
吞吐量=(执行用户代码时间)/(执行用户代码时间+垃圾回收占用时间)
CMS收集器
Concurrent Mark Sweep,采用标记-清除算法,用于老年代,常与ParNew协同工作。优点在于并发收集与低停顿;注:并行是指同一时刻同时做多件事情,而并发是指同一时间间隔内做多件事情
初始标记:标记老年代中所有的GC Roots对象和年轻代中活着的对象引用到的老年代的对象,时间短;
并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象;
重新标记:用来处理前一个阶段因为引用关系改变导致没有标记到的存货对象,时间短
并发清理:清除那些没有标记的对象并且回收空间
缺点:占用大量的cpu资源、无法处理浮点垃圾、出现Concurrent MarkFailure、空间碎片
G1收集器
说明:G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入到JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。
优势:并行(多核CPU)与并发;分代收集(新生代和老年代分布明显);空间整合;限制收集范围,可预测的停顿
步骤:初始标记、并发标记、最终标记和筛选回收
内存分配
原则:
1.优先分配到Eden
2.大对象直接分配到老年代
3.长期存活的对象分配到老年代
4.空间分配担保
5.动态对象的年龄判断
0 条评论
下一页