JVM工作原理
JVM 主要由 ClassLoader
和 执行引擎
两子系统组成.
任何一个Java类的main方法运行都会创建一个JVM实例, 当main函数结束时, JVM实例也就结束了. JVM实例启动时默认启动几个守护线程, 比如: 垃圾回收的线程, 而 main 方法的执行是在一个单独的非守护线程中执行的.只要母线程结束, 子线程就自动销毁, 只要非守护main 线程结束JVM实例就销毁了.
JVM的工作原理如下:
- 根据系统环境变量, 创建装载JVM的环境与配置;
- 寻找JRE目录, 寻找jvm.dll, 并装载jvm.dll;
- 根据JVM的参数配置, 如: 内存参数, 初始化jvm实例;
- JVM实例产生一个引导类加载器实例(Bootstrap Loader), 加载Java核心库, 然后引导类加载器自动加载扩展类加载器(Extended Loader),加载Java扩展库, 最后扩展类加载器自动加载系统类加载器(AppClass Loader), 加载当前的Java类;
- 当前Java类加载至内存后, 会经过验证、准备、解析三步, 将Java类中的类型信息、属性信息、常量池存放在方法区内存中, 方法指令直接保存到栈内存中, 如: main函数;
- 执行引擎开始执行栈内存中指令, 由于main函数是静态方法, 所以不需要传入实例, 在类加载完毕之后, 直接执行main方法指令;
- main函数执行主线程结束, 随之守护线程销毁, 最后JVM实例被销毁;
类加载
类生命周期
类: 需要由加载它的类加载器和这个类本身共同保证其在JVM中的唯一性
加载
通过类的全路径名获取类的二进制字节流,将类的静态内容和对象信息加载进方法区,在堆中创建对象,作为方法区数据的访问入口.
具体是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,作为这个类封装在方法区内的数据结构的入口.
类的加载最终是在堆区内的Class对象,Class对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口.
怎样加载一个类:
- 命令行启动应用时候由JVM初始化加载
- 通过
Class.forName()
方法动态加载 - 通过
ClassLoader.loadClass()
方式动态加载,如ClassLoader.getSystemClassLoader().loadClass("org.luvx.User")
一个类被加载,当且仅当其某个静态成员(静态方法等、构造器)被调用时发生,加载一个类时,其内部类不会同时被加载。
验证
检查Class文件数据的正确性,是否符合当前虚拟机的要求 ,是否会危害JVM的安全等,是类加载过程中最复杂耗时的过程.
细分为以下过程:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
正式为类的静态内容分配内存并设置变量初始值
- 进行内存分配的仅是静态变量,不包括对象变量,对象变量在对象实例化时随着对象分配在堆内存中.
- 设置初始值并不是是什么就是什么,而是设置对应类型的初始值,如定义
static int num = 12
,此时设置为0,12是在上图初始化阶段设置,但static final
修饰除外,直接就是12
解析
将常量池中的符号引用替换为直接引用,主要针对类或接口、字段、类方法、接口方法四类符号引用进行
符号引用不一定要已经加载到内存,而直接引用必定存在于存中 关于符号引用, 查下以下代码的字节码:
12345678910111213 |
// 符号引用public class Test { public static void main() { String s="adc"; System.out.println("s=" + s); }}// 直接�引用public class Test { public static void main() { System.out.println("s=" + "abc"); }} |
初始化
类加载的最后阶段,对静态内容进行初始化操作
Java类初始化顺序:
父类静态变量->父类静态代码块->子类静态代码块->父类非静态变量->父类非静态代码块->父类构造函数->子类非静态变量->子类非静态代码块->子类构造函数
不会加载类的情形:
- 通过子类使用父类的静态字段,不会加载子类
- 定义对象数组
- 使用类名获取Class对象
- 使用
Class.forName()
加载类时,指定参数initialize
为false
- 使用
ClassLoader
的loadClass()
方法加载类
类加载
Java中的类都是在程序运行期间加载的,虽然会降低性能,但这种动态加载机制增加了灵活性,如面向接口编程中,只有运行时才能知道具体的类,可以自定义类加载器,动态加载指定的二进制数据创建对象.
类加载的时机
JVM规范中并没有约束类加载时机,但约束了5种情况需对类进行初始化操作,其之前的操作自然就需要完成.
- 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时(new对象时,调用静态方法,读/写静态变量,final修饰的除外)
- 对
java.lang.reflect
包的方法对类进行反射调用 - 初始化一个类,其父类仍没有初始化,就需要初始化父类
- jvm启动,初始化含有
main()
方法的类 - Java7的动态语言支持下,
java.lang.invoke.MethodHandle
的解析结果对应的类没有初始化,则需要初始化
自己写的两个不同的类是被同一个类加载器加载的吗?为什么
类加载器
类加载器的作用就是从字节码创建一个类,并负责加载 Java 应用所需的资源.
只有当一个类要使用的时候,类加载器才会加载这个类并初始化
- 启动类加载器:加载Java核心库(JAVA_HOME/lib),如
rt.jar
- 扩展类加载器:加载Java扩展库(JAVA_HOME/lib/ext)
- 应用类加载器:记载当前Java类(java.class.path)
- 开发者可以通过继承
java.lang.ClassLoader
实现自定义类加载器.
User user = new User()
实质就是User user = Class.forName("org.luvx.User", false, this.class.getClassLoader()).newInstance();
双亲委托模型
工作过程为:一个类收到类加载的请求,首先不会自己去尝试加载,而是委派为父加载器去加载,只有当父类反馈无法加载时,才会尝试自己去加载. 如果所有加载器均加载失败, 则会抛出ClassNotFoundException
异常.
意义: 可以保证java的一些重要类如Object在各种类加载器加载下都是同一个类,因为最终都是由启动类加载器加载,保证的类的唯一性.
存在的问题:模型本身决定的,例如基础类要掉回用户代码 怎么解决了:线程上下文类加载器
开发者可以继承java.lang.ClassLoader
并重写findClass()
方法即可创建自定义类加载器.
一个类的类型是类本身和加载该类的加载器一起确定的
NoClassDefFoundError
NoSuchMethodError
ClassCastException:同一个类如果被不同的加载器加载,那他们就不是同一个类,也无法将一个类强转为另一个类,会报类转换异常,这也是ClassLoader隔离
问题.
Q&A
java 的对象分配策略 在Eden中, 大对象直接进入老年代, 长期存活的对象进入老年代, 动态年龄分配, 空间分配担保
内存结构
- 堆内存
- 年轻代(8:1:1)
- Eden空间
- From Survivor
- To Survivor
- 老年代
- 年轻代(8:1:1)
- 方法区
- 栈
- java虚拟机栈
- 本地方法栈
区域 | 作用 | 共享性 | 存储内容 |
---|---|---|---|
堆内存 | 存放对象实例,可以细分为新生代和老年代 | √ | new出来的对象(属性,方法的地址(指向方法区)) |
方法区 | 内有运行时常量池,也有人称之为永久代 | √ | 常量,static变量,类信息(属性,方法) |
运行时常量池 | 方法区的一部分 | √ | 运行时常量池(各种字面量和符号引用) |
程序计数器 | 比较小的内存区域,指示当前线程所执行的字节码的位置 | × | 正在执行的VM字节码指令地址(Java方法,native方法时为空) |
VM栈 | 记录方法调用 | × | 局部变量表,对象的引用指针 |
本地方法栈 | 执行Native方法时使用 | × | - |
程序计数器(Program Counter Register)
一块较小的内存空间, 属于线程私有.
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成. 如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果是Native方法,则计数器为空;多线程时, 存在多个程序计数器.
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.
VM栈(VM Stacks)
线程私有, 生命周期与线程相同
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息.
每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时该方法对应的需要在栈帧中分配多大的局部变量空间是完全确定的.
对这个区域规定了两种异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError
异常; 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展, 只不过Java虚拟机规范中也允许固定长度的虚拟机栈), 当扩展时无法申请到足够的内存时会抛出OutOfMemoryError
异常.
栈帧: 一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁.栈帧内存放着方法中的局部变量,操作数栈等数据
堆内存(Heap)
Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存.
Java堆是垃圾收集器管理的主要区域, 因此很多时候也被称做"GC堆" 多采用分代收集策略,所以细分为新生代和老年代,再细分可分为Eden空间,From Survivor空间,To Survivor空间,在GC的复制算法中起着重要作用,HotSpot VM默认的Eden和Survivor大小比例为8:1
堆内存可以是物理上不连续的内存空间, 逻辑上连续即可,
在堆中没有内存完成实例分配, 并且堆也无法再扩展时, 将会抛出OutOfMemoryError
异常
方法区(Method Area)
与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,有时也被称为永久代(PermGen)
Java虚拟机规范对这个区域的限制非常宽松, 除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外, 还可以选择不实现垃圾收集
这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载
当方法区无法满足内存分配需求时, 将抛出OutOfMemoryError
异常
运行时常量池
是方法区的一部分,存放编译期生成的各种字面量和符号引用,当JVM运行的时候会将这些常量池的信息加载进方法区.
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
本地方法栈(Native Method Stacks)
虚拟机栈为虚拟机执行Java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到的Native方法服务
异常抛出类型和JVM栈相同
直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁的使用.
也可能导致OutOfMemoryError
异常出现
比较
- 栈内存用来存储局部变量和方法调用.
- 堆内存用来存储Java中的对象.无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中.
内存溢出异常
Exception in thread "main": java.lang.OutOfMemoryError: Java heap space
原因: 对象不能被分配到堆内存中
Exception in thread "main": java.lang.OutOfMemoryError: PermGen space
原因: 类或者方法不能被加载到永久代.它可能出现在一个程序加载很多类的时候, 比如引用了很多第三方的库;
Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因: 创建的数组大于堆内存的空间
Exception in thread "main": java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
原因: 分配本地分配失败.JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间.
Exception in thread "main": java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)
原因: 同样是本地方法内存分配失败, 只不过是JNI或者本地方法或者Java虚拟机发现
java.lang.OutOfMemoryError: unable to create new native thread
原因: 创建了太多的线程,而能创建的线程数是有限制的,导致了异常的发生
JVM调优工具
工具 | 作用 |
---|---|
jps | 进程状态工具,查看正在运行的JVM进程 |
jstat | 统计信息监视工具,实时显示JVM进程中类装载、内存、垃圾收集、JIT编译等数据 |
jinfo | 配置信息工具,查询当前运行着的JVM属性和参数的值 |
jmap | 内存映射工具,生成VM的内存转储快照 |
jhat | 堆转储快照分析工具,分析使用jmap生成的dump文件 |
jstack | 堆栈跟踪工具,生成当前JVM的所有线程快照,线程快照是虚拟机每一条线程正在执行的方法,目的是定位线程出现长时间停顿的原因. |
jconsole | jvm监视管理控制台,图像化显示堆栈等使用情况,可以手动进行GC,非常实用 |
jcmd |
JVM参数:
工具 | 作用 |
---|---|
-Xmx | 最大堆内存 |
-Xms | 最小堆内存, 通常设置成跟最大堆内存一样,减少GC |
-Xmn | 设置年轻代大小,官方推荐设置为堆的3/8 |
-Xss | 指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError) |
-XX:PermSize | 指定方法区(永久区)的初始值,默认是物理内存的1/64, 在Java8永久区移除, 代之的是元数据区, 由-XX:MetaspaceSize指定 |
-XX:MaxPermSize | 指定方法区的最大值, 默认是物理内存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元数据区的大小 |
-XX:NewRatio=n | 年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1 |
-XX:SurvivorRatio=n | Eden区与一个Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与两个Survivor区的大小比值是8:1:1,因为Survivor区有两个(from, to) |
GC
GC的工作内容就是就是回收内存,包括哪些内存需要回收,什么时候回收,怎么回收等3件工作.
堆内存也被称为GC堆,是因为GC的主要进行场所就是堆内存,方法区是堆内存的一部分,同样也可以GC的对象,
但Java虚拟机规范不要求虚拟机在方法区实现GC,而且在方法区进行GC的"性价比"一般都比较低,这和方法区被称为永久代有着相同的原因.
对象是否可回收
在GC进行前首先要确定的就是对象是否还活着
(是否还在直接或间接的被使用中)
判断对象的使用常有以下2种策略:
引用计数算法
存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,JVM必须适当增减引用数.当某对象的引用数为0时,便可以进行垃圾回收. 优点: 实现简单、效率高 缺点: 很难解决对象之间相互引用问题
可达性分析算法
通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的.
可作为GC Roots的对象
包括:
- JVM栈(栈中的本地变量表)中引用的对象
- 方法区中的类静态属性,常量引用的对象
- 本地方法栈中Native方法引用的对象
四种引用
- 强引用: 只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象.
- 软引用(SoftReference): 指还有用,但是非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收.回收后仍不足就会抛出内存溢出异常.
- 弱引用(WeakReference): 非必须的对象,比软引用还要弱,只能生存到下一次垃圾回收发生之前.
- 虚引用(PhantomReference): 对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例.关联虚引用唯一目的就是能在对象被回收器回收时收到系统通知
垃圾回收策略
标记-清除算法
Mark-Sweep,适用于老年代,最基础的算法,后续的算法都是基于这种思想改进而来,
标记或清除过程的效率都不高,产生大量不连续的内存碎片,在分配大对象时候因无法找到符合的连续空间而再次进行GC
标记-整理-清除算法
在标记-清除算法的标记的基础上,将不被回收的对象向同一端移动,然后清理到边界外的内存,解决了内存碎片
复制算法
为解决标记清除算法效率和造成的不连续碎片问题而生,适用于对象存活率低的新生代
将内存分为一块较大的Eden空间和两块较小的Survivor,每次使用Eden和其中一块Survivor,回收时,将Eden和Survivor中存活的对象一次性地拷贝到另一块Survivor空间,最后清理掉Eden和Survivor空间
分代回收策略
不是一种具体的GC算法,是一种不同代采取不同的回收算法的策略.
新生代的对象生命周期短,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集. 而老年代中因为对象存活率高,没有额外的空间进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收.
回收方法区(永久代)
此区域的回收主要有两项内容:废弃常量和无用的类
垃圾回收器
垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,开发者不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收. 常见垃圾回收器有:
- 串行垃圾回收器(Serial Garbage Collector):Client模式下的默认新生代收集器
- ParNew 收集器:串行回收器的多线程版本,Server模式下首选的新生代收集器
- 并行垃圾回收器(Parallel Garbage Collector):复制算法
- 并发标记扫描垃圾回收器(Concurrent Mask Sweep Garbage Collector)
- G1垃圾回收器(G1 Garbage Collector):基于标记-整理算法,可以精确控制停顿.基本不牺牲吞吐量的前提下完成低停顿的内存回收
CMS
应用标记-清除算法,具体过程有初始标记-并发标记-重新标记-并发清理-并发重置
并发收集、低停顿 ;但也有对CPU资源非常敏感、无法处理浮动垃圾,以及算法本身所具有的会产生内存碎片的缺点
G1收集器
Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器机器、大内存机器.
应用标记-整理算法,相对于CMS能非常精确地控制停顿,高度符合垃圾收集暂停时间的目标,同时实现高吞吐量.可以实现在基本不牺牲吞吐量的情况下完成低停顿的回收
内存划分方式: 它是将堆内存被划分为多个大小相等的 heap 区,每个heap区都是逻辑上连续的一段内存(virtual memory). 并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间, 优先回收垃圾最多的区域(这也是Garbage First名称的由来). 总而言之,区域划分和有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率.
内存分配
对象主要分配在新生代的Eden区,少数情况下会直接分配在老年代中.
分配策略:
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
对象年龄判定
JVM给每个对象都定义一个age计数器,若对象在Eden出生并经过第一次Minor GC后仍存在并被Survivor容纳,对象age为1,之后在Survivor中每经过一次Minor GC,age加1.当age达到一定数值(默认15)就会成为老年代,默认值可以通过-XX:MaxTenuringThreshold
修改.
实际上,JVM并不是总要求对象的年龄必需达到MaxTenuringThreshold
才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代.
GC收集器参数
参数 | 说明 |
---|---|
-XX:+UseSerialGC | 在新生代和老年代使用串行收集器 |
-XX:+UseParallelGC | 新生代使用并行回收收集器 |
-XX:+UseParallelOldGC | 老年代使用并行回收收集器 |
-XX:+UseParNewGC | 在新生代使用并行收集器 |
-XX:+UseConcMarkSweepGC | 新生代使用并行收集器,老年代使用CMS+串行收集器 |
-XX:+UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理 |
-XX:UseCtpancyOnly | 表示只在到达阀值的时候,才进行CMS回收 |
-XX:SurvivorRatio | 设置eden区大小和survivior区大小的比例 |
-XX:NewRatio | 新生代和老年代的比 |
-XX:ParallelGCThreads | 设置用于垃圾回收的线程数 |
-XX:ParallelCMSThreads | 设定CMS的线程数量 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发 |
-XX:CMSFullGCsBeforeCompaction | 设定进行多少次CMS垃圾回收后,进行一次内存压缩 |
-XX:+CMSClassUnloadingEnabled | 允许对类元数据进行回收 |
-XX:CMSInitiatingPermOccupancyFraction | 当永久区占用率达到这一百分比时,启动CMS回收 |
-XX:+PrintGCDetails | 开启后,GC时打印内存回收日志,并在线程退出时输出内存分配情况 |
空间分配担保机制
Q&A
Minor GC与Full GC分别在什么时候发生?什么时候触发Full GC;
类型 | GC对象 | 发生时机 |
---|---|---|
Minor GC | 回收年轻代, 包括Eden 和 Survivor 区域 | 无法为一个新的对象分配空间时 |
Major GC | 永久代 | |
Full GC | 整个堆空间 |
GC收集器有哪些?CMS收集器与G1收集器的特点。
Java中的大对象如何进行存储;
为什么新生代内存需要有两个Survivor区?
G1停顿吗,CMS回收步骤,CMS为什么会停顿,停顿时间;
每个算法的优缺点啊, 怎么简单的解决啊
增加堆的大小, 增加后台线程, 提前开始并发周期等
有没有了解G1收集器这些, G1的流程, 相比CMS有哪些优势.
Minor GC发生的频繁的原因?�GC的时间长的原因是什么 对象太小, 对象太大
Full GC次数太多了,如何优化;
文章转载于:https://www.javazhiyin.com/24771.html