本文主要是介绍jvm做了什么?(68h)158,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
one.内存与垃圾回收
一.JVM的整体结构(主要针对于HotSpot)
常见的虚拟机:HotSpot, J9, JRockit
二.虚拟机的生命周期
a.启动->Java虚拟机的启动是通过引导类加载器创建一个初始类来完成的, 这个类由虚拟机的具体实现来指定; b.执行-> 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序. 所以, Java虚拟机在Java程序开始执行时才会运行, 在Java程序结束时就会停止. 然而, 执行一个所谓的Java程序的时候, 真正执行的是一个Java虚拟机的进程. c.退出-> 程序正常执行结束时; 程序执行过程中遇到了异常或错误而异常终止; 调用System类的exit方法等; |
三.类的加载
1.类的加载过程: 加载: a.通过一个类的全限定名获取定义此类的二进制字节流; b.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; c.在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问接口. 验证: 主要为了确保class文件的字节流中包含的信息是否符合当前的虚拟机要求,为了让虚拟机不受危害. .class文件的字节流以 "CA FE BA BE"开头 准备: a.为类变量分配内存并且设置该变量的默认初始化值; b.这里不包括static final 修饰的变量, 因为这种变量在编译阶段就会被分配初始化值, 而在准备阶段会直接进行显式初始化; c.这里不会为实例变量分配初始化, 类变量会分配在方法区中, 而实例变量会随着对象一起分配到java堆中 初始化: a.初始化阶段就是执行类构造器方法<clinit>()的过程; b.此方法不需要定义, 它是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来; c.构造器方法中的指令按照语句在源文件中出现的顺序执行; d.<clinit>()不同于类的构造器.(构造器是虚拟机视角下的 <init>()); e.若该类具有父类, jvm会保证子类的<clinit>()执行前, 父类的<clinit>()已经执行完毕; f.虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁执行. 2.类的加载器 ClassLoader类, 它是一个抽象类, 除了引导类加载器以外都继承于它. 加载规则: 引导类加载器: 负责加载java的核心类库; 扩展类加载器: 加载包java/lib/ext下的类; 应用类加载器: 双亲委派机制: jvm对class文件采用的是按需加载, 也就是说当需要使用该类时才会将它的class文件加载到内存生成Class对象, 而且在加载类的class文件时, jvm采用的是双亲委派机制. 工作原理: a.如果一个类加载器收到了类加载请求, 它并不会先去加载, 而是把这个请求委托给父类的加载器去执行; b.如果父类加载器还存在其父类加载器, 则进一步向上委托, 依次递归, 加载请求最终将到达顶层的启动类加载器; c.如果父类加载器可以完成类加载任务, 就返回成功, 倘若父加载器无法完成此加载任务, 子加载器才会尝试去加载, 这就是双亲委派机制. 双亲委派机制的作用: a.避免类的重复加载; b.保护程序安全, 防止核心API被恶意篡改; 沙箱安全机制: 自定义一个java.lang.String类并写一个main()方法并运行, 但是在加载自定义类String的时候会率先使用引导类加载器进行加载, 而引导类加载器在加载的过程中会先加载jdk自带的String,就会报错说没有找到main()方法, 这样可以保证对java核心API的保护, 这就是沙箱安全机制. 3.java程序对类的使用方式: a.主动使用,分为七种方式: A.创建类的实例; B.访问某个类或接口的静态变量, 或者对静态变量进行赋值; C.调用类的静态方法; D. 反射; E.初始化一个类的子类; F.jvm启动时被表名启动类的类; G.JDK 7开始提供的动态语言支持: java.lang.invoke.MethodHandle实例的解析结果; REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化则进行初始化; b.被动使用 除了以上七种情况, 其他使用java类的方式都可以被看作是对类的被动使用, 类的被动使用不会进行类的初始化(类的加载过程中的初始化,会调用<clinit>()方法) 注: 1.ClassLoader只负责.class文件的加载, 至于它是否可以运行, 则由Execution Engine决定. 2.引导类加载器:是c和c++语言编写的,其他的加载器都是使用java实现的 |
四.运行时数据区
内存是硬盘和CPU的中间仓库和桥梁,
1.程序计数器(线程私有)
1.介绍和作用:pc寄存器用来存储指向下一条指令的地址, 即将要执行的指令代码. 由执行引擎读取下一条指令. 2.为什么使用pc寄存器记录当前线程的执行地址? 因为CPU需要不停地切换各个线程, 当再次切换会当前线程后, 就可以通过线程私有的pc获取到接下来从哪儿开始继续执行; . |
2.虚拟机栈(线程私有)
由于跨平台的设计, java的指令都是根据栈来设计的.不同平台的CPU架构不同, 所以不能设计为基于寄存器的. 栈内部保存着一个个栈帧,每个方法对应一个栈帧. 作用: 主管java程序的运行, 用于保存方法的局部变量、部分结果, 并参与方法的调用和返回. 调整栈内存的大小: -Xss500m -->> 给栈内存的大小设置为500m 栈的存储单位: a.每个线程都有自己的栈, 栈中的数据都是以栈帧为基本单位进行存储. 栈帧是一个内存区块, 是一个数据集, 维系着方法执行过程中的各种数据信息. 每个栈帧中存储什么? a.局部变量表(Local Variables); b.操作数栈(Operand Stack) ,或叫表达式栈; c.动态链接(Dynamic Linking), 或叫指向运行时常量池的方法引用; d.方法返回地址(Return Address), 或叫方法正常退出或异常退出的定义; e.一些附加信息;
注: 1.栈是运行时的单位, 而堆是存储的单位. 栈解决程序的运行问题, 即程序如何运行, 或者说如何处理数据. 栈解决的是数据存储的问题,即数据怎么放、放在哪儿. 2.jvm对虚拟机栈的操作只有两个, 就是压栈和出栈.所以在一条活动线程中, 一个时间点上, 只会有一个活动的栈帧, 即当前正在执行的方法的栈帧(栈顶栈帧)是有效的, 这个栈帧被称为当前栈帧, 当前栈帧对应的方法叫做当前方法, 定义这个方法的类就是当前类.所以执行引擎运行的所有字节码指令只针对当前栈帧进行操作. 3.不同线程中所包含的栈帧是不能相互引用的, 即不可能在一个栈帧中引用另外一个线程的栈帧. 4.如果当前方法调用了其他方法, 方法返回之际, 当前栈帧会传回此方法的返回结果给前一个栈帧, 接着, 虚拟机会丢弃当前栈帧, 使得前一个栈帧成为当前栈帧. 5.java方法有两种返回函数的方式:一种是正常的函数返回, 使用return指令; 另一种是抛出异常, 两种方式都会导致栈帧被弹出. |
3.堆(线程共享)
jdk7及以前逻辑上分为:新生代,老年代,永久代 jdk8及以后逻辑上分为:新生代,老年代,元空间 1.关于TLAB
2.我们new 的对象只会存在于堆中吗? -->> 逃逸分析(默认开启)
垃圾回收过程图示
注: 1.java虚拟机规范规定, 堆可以处于物理上不连续的内存空间中, 但在逻辑上它应该被视为连续的. 2.堆还可以划分现成私有的缓冲区(Thread Local Allocation Buffer, TLAB). 3.堆空间默认大小:初始内存(物理内存的1/64),最大内存(物理内存的1/4). 4.不是所有对象都是在伊甸园区new出来的. 5.伊甸园区与幸存者区的大小比例默认8:2, 但实际上只有6:2. 6.如果幸存者区中相同年龄的所有对象大小的总和大于幸存者空间的一半, 则年龄大于或等于该年龄的对象可以直接步入老年代而无需等到15岁. 7.jvm空间分配担保策略: 在Minor GC之前, 虚拟机会检查得知老年代最大可用的连续空间大于新生代所有对象的总空间或者历次晋升的平均空间大小就直接进行Minor GC, 否则将进行Full GC. |
4.方法区/永久代/元空间(线程共享)
1.栈, 堆, 方法区的交互关系 2.元空间与永久代不同, 如果不指定大小, 默认情况下, 虚拟机会耗尽所有的可用系统内存.所以我们可以设置初始的元空间大小.对于一个64位的服务器端jvm来说, 其默认值是21MB.这就是初始的高水位线, 一旦触及这个水位线, Full GC就会被触发并卸载没用的类(即这些类对应的类加载器不再存活), 然后这个高水位线将会重置.新的高水位线的值取决于GC后释放了多少空间, 如果释放空间后不是很充足, 那么会适当提高该值.如果释放空间过多, 则会适当降低该值. 3.方法区的变化 注: 1.方法区又叫永久代(jdk1.7)和元空间(jdk1.8), 永久代使用的是jvm的内存,元空间使用的是本地内存. 2.要想分析OOM, 我们首先要导出来各个内存使用情况的快照(我们可以通过jVisual得到这个文件), 我们可以称之为dump文件, 然后通过JVisual等工具导入此dump文件, 通过堆dump文件的分析推论出到底是出现了内存泄漏(简单理解就是有些对象不用了但是还有指针指向它)还是内存溢出(OOM). 3.方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码缓存等.
4.字节码的常量池被加载到方法区以后叫做运行时常量池. |
五.对象的实例化、内存布局和访问定位
1.对象实例化大致过程: 类的加载 -> 为对象分配内存 -> 初始化属性 -> 设置对象头 -> <init>() a.判断对象对应的类是否加载、链接、初始化 b.为对象分配内存 如果内存规整 -->> 指针碰撞 如果不规整 -->> 虚拟机需要维护一个空闲列表(列表记录哪些内存没有被占用) c.处理并发安全问题 采用cas失败重试、区域加锁保证更新的原子性 每个线程预先分配一块TLAB d.初始化分配到的空间 所有属性设置默认初始化值, 保证对象实例字段在不赋值的前提下也能使用 e.设置对象头 f.执行<init>()方法进行初始化 2.内存布局: a.对象头 A.哈希值 -> 对象所处的内存位置 B.GC分代年龄 C.锁状态标志 D.持有锁的线程 E.偏向线程ID F.偏向时间戳 G.类型指针 -> 指向类元数据, 确定该对象所属的类型 如果是数组, 还需记录数组的长度. b.实例数据 c.对齐填充 3.如何访问定位 a.句柄访问 b.直接指针 指针碰撞: 如果内存是规整的, 那么虚拟机将采用的是指针碰撞法来为对象分配内存. 意思是所有用过的内存都在一边, 空闲的内存在另一边, 中间放着一个指针作为分界点. 分配内存就仅仅是把指针往空闲的那边挪动一段与对象大小相等的距离罢了. |
六.执行引擎
任务: 将字节码指令解释/编译为对应平台上的本地机器指令 1.工作过程: a.执行引擎在执行的过程中究竟需要执行什么字节码指令完全依赖于PC; b.每当执行完一条指令操作后, PC寄存器就会更新为下一条需要被执行的指令地址; c.方法在执行的过程中, 执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在java堆中的对象实例信息, 以及通过对象头中的类型指针定位到当前对象的类型信息. 2.解释器和JIT编译器 a.解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行. b.JIT编译器:就是虚拟机将源代码直接编译成和本地机器语言. |
七.StringTable
String被声明为final的, 所以不可以被继承.jdk1.8底层使用char型数组存储.1.9以后使用byte数组存储. StringTable存在垃圾回收行为. 1.String pool的基本特性: String pool是一个固定长度的HashTable, 默认长度为60013(jdk1.7), 使用HashTable的好处是字符串常量池中不会存储相同内容的字符串. 2.字符串拼接操作: a.常量与常量的拼接结果在常量池, 原理是编译期会直接进行优化; b.常量池中不会存在相同内容的常量; c.拼接的过程中只要有一个是变量(被final修饰的是常量), 结果就在堆中.此变量拼接的原理是StringBuilder; d.如果拼接的结果调用了intern()方法, 则主动将常量池中还没有的字符串对象放入池中, 并返回此对象地址; 3.关于intern()方法:(jvm视频127) 调用intern()方法可以让jvm主动在字符串常量池中放入一个字符串常量. |
八.垃圾回收
jdk中默认的垃圾回收器:G1 |
8.1.垃圾回收相关算法
标记阶段: 1.引用计数法: 2.可达性分析: a.可达性分析算法以根对象集合(GC Roots)为起点, 然后判断某个对象是否能够被GC Roots所直接或间接的连接. b.使用可达性分析算法后, 内存中存活的对象都会被GC Roots中的对象直接或间接的连接着, 这个连接称为引用链. 清除阶段: 1.标记清除算法(Mark sweep) 执行过程: 标记:Collector从引用根节点进行遍历, 标记所有被引用的对象, 在对象的对象头中进行记录. 清除:Collector对堆内存从头到尾进行线性遍历, 如果发现某个对象在其对象头中并没有被标记为可达对象, 则将其回收. 缺点:这种方式清理出来的内存是不连续的, 会产生内存碎片.所以还需要维护一个空闲列表. 何为清除? 清除并不是把不可达的对象直接置空, 而是把需要清除的对象地址保存在空闲的地址列表里. 下次有新对象需要加载时, 判断垃圾的内存空间是否足够, 如果足够直接存放. 2.复制算法 from区和to区使用的算法. 3.标记压缩算法(Mark Compact) 执行过程: 标记:从根节点开始标记所有被引用的对象 压缩:将所有的存活对象压缩到内存的一端, 按顺序存放.然后清理边界外所有的空间. GC Roots包含哪些元素? a.虚拟机栈中引用的对象; b.本地方法栈中引用的对象; c.方法区中类静态变量所引用的对象; d.方法区中常量所引用的对象; e.所有被同步锁synchronized持有的对象; f.java虚拟机内部的引用. 对象的finalization机制: 在垃圾回收之前, 总是会先调用该对象的finalize()方法.此方法可以用于在对象被回收时进行资源释放,比如关闭文件、套接字、数据库连接等. 由于finalize()方法的存在, 虚拟机中的对象一般处于三种可能得状态: a.可触及的:从根节点开始, 可以到达这个对象 b.可复活的:对象的所有"被引用"都被释放, 但是对象可能在finalize()中复活 c.不可触及的:如果对象的finalize()方法被调用, 而且当前对象没有复活,那么就会进入不可触及状态.不可触及的对象不可能复活, 因为finalize()方法只可能被调用一次. MAT 它是Memory Analyzer的简称, 它是一款强大的java堆内存分析工具,用于查找内存泄漏和查看内存消耗情况. 分代收集算法:每个不同的内存区域结合自身的特点采用不同的垃圾回收算法. 增量收集算法:垃圾收集线程只收集一小片的内存区域(尽量少的stop the world), 接着切换至用户线程. 分区算法:基本原理和增量收集算法差不多. |
8.2.垃圾回收相关概念
1.System.gc() 如果上述代码被调用, 会显式触发Full GC, 它只是提醒jvm进行GC, 具体什么时候GC不确定. System.runFinalization() -->> 如果在System.gc()以后调用了该代码, 则会强制调用失去引用的对象的finalize()方法,也就是强制进行gc. 2.内存溢出和内存泄漏 内存泄漏: 严格来说, 只有对象不会被程序所使用, 但gc又不能回收的情况才可以叫内存泄漏. |
two.字节码与类的加载
three.性能监控与调优
four.面试总结,
这篇关于jvm做了什么?(68h)158的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!