JVM.md
文章目录
第一章-类加载器和类加载过程
尚硅谷JVM周阳视频(简介).https://www.bilibili.com/video/BV1vE411D7KE?from=search&seid=4436711570232529843
尚硅谷JVM宋红康(详情):https://www.bilibili.com/video/BV1PJ411n7xZ?p=49
别人笔记:https://www.cnblogs.com/yanl55555/p/12610952.html
1类的加载过程
-
1 加载 类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
-
2链接
- 1验证
确保class文件的字节流所含信息符合当前虚拟机要求,保证被加载类的正确性。主要包括,文件格式,元数据,字节码验证,符合引用验证。 - 2准备 为类变量分配内存,并且设置类变量的初始默认值,即零值。 不包含final修饰的static,因为final在编译时就会分配内存,准备阶段会直接赋值。 类变量分配在方法区,实例变量会随着对象分配到Java堆中
- 3解析 把符号引用转化为直接引用
- 1验证
-
3初始化 就是执行类构造器方法
()的过程。此方法不需定义,是javac编译 器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 构造器方法中指令按语句在源文件中出现的顺序执行。 ()不同于类的构造器。(关联: 构造器是虚拟机视角下的 ()) 若该类具有父类,JVM会 保证子类的 ()执行前,父类的 () 已经执行完毕。 虚拟机必须保证一个 类的 ()方法在多线程下被同步加锁。
1加载
类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
2链接
- 1验证
确保class文件的字节流所含信息符合当前虚拟机要求,保证被加载类的正确性。主要包括,文件格式,元数据,字节码验证,符合引用验证。- 2准备 为类变量分配内存,并且设置类变量的初始默认值,即零值。 不包含final修饰的static,因为final在编译时就会分配内存,准备阶段会直接赋值。 类变量分配在方法区,实例变量会随着对象分配到Java堆中
- 3解析 把符号引用转化为直接引用
3初始化
就是执行类构造器方法
()的过程。此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。(必须要有静态代码和静态代码块) 构造器方法中指令按语句在源文件中出现的顺序执行。 ()不同于类的构造器。(关联: 构造器是虚拟机视角下的 ()) 若该类具有父类,JVM会 保证子类的 ()执行前,父类的 () 已经执行完毕。 虚拟机必须保证一个 类的 ()方法在多线程下被同步加锁。
idea插件 double shift收缩plugins 然后下载jClasslib。
运行/build后,进入到class文件下
两个线程同时加载一个类,是线程安全的。
1类加载器
jvm类加载https://blog.csdn.net/qq_15237993/article/details/72916868
负责加载class文件,class文件在 文件开头有特定的文件标示, 将class文件字节码内容加载到内存中,并将这些内容转换成==方法区==中的 ==运行时数据结构==,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
1.1类加载器的类型
-
启动类加载器(Bootstrap ClassLoader)
作用:负责加载以下类。
存放在
\lib
目录中的类(rt.jar下的com.java.util等等,所以一开始就可以用Object)被
-Xbootclasspath
参数所指定路径中、并且是被虚拟机识别的类库使用C++编写,加载Object等,但通过new Object().getClass().getClassLoader()获得的是Null,因为其是C++编写,不是Java对象。
-
扩展类加载器(Extension ClassLoader)
作用:负责加载以下类:
\lib\ext
目录中的类库(ext下的*.jar)- 被
java.ext.dirs
系统变量所指定的路径中的所有类库
特别注意
- 由
sum.misc.Launcher$ExtClassLoader
类实现 - 开发者可以直接使用扩展类加载器
-
应用程序类加载器(Application ClassLoader)
作用:负责加载 用户类路径(
ClassPath
)上所指定的类库特别注意
- 也称为系统类加载器,因为该类加载器是
ClassLoader
中的getSystemClassLoader()
方法的返回值 - 由
sum.misc.Launcher$AppClassLoader
类实现 - 开发者可以直接使用该类加载器
- 若开发者 没 自定义类加载器,程序默认使用该类加载器
==以上为Java虚拟机自带的类加载器==
4.用户自定义加载器 Java. lang. ClassLoader的子类,用户可以定制类的加载方式。
使用自定义类加载器的原因:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄露。
- 也称为系统类加载器,因为该类加载器是
1.2双亲委派机制
步骤总结:若一个类加载器收到了类加载请求
- 把 该类加载请求 委派给 父类加载器去完成,而不会自己去加载该类
每层的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中。
- 只有当 父类加载器 反馈 自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载
优点:
Java
类随着它的类加载器一起具备了一种带优先级的层次关系
- 如:类
java.lang.Object
(存放在rt.jar
中)在加载过程中,无论哪一个类加载器要加载这个类,最终需委派给模型顶端的启动类加载器进行加载,因此Object
类在程序的各种类加载器环境中都是同一个类。- 若没有使用双亲委派模型(即由各个类加载器自行去加载)、用户编写了一个
java.lang.Object
的类(放在ClassPath
中),那系统中将出现多个不同的Object
类,Java体系中最基础的行为就无法保证
保证java类的安全
大家所熟知的Object类,直接告诉大家,Object默认情况下是启动类加载器进行加载的。假设我也自定义一个Object,并且制定加载器为自定义加载器。现在你会发现自定义的Object可以正常编译,但是永远无法被加载运行。
这是因为申请自定义Object加载时,总是启动类加载器,而不是自定义加载器,也不会是其他的加载器。
1.3沙箱安全机制
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
1.4补充
1.在jvm中表示两个class对象是否为同一个类存在的两个必要条件
类的完整类名必须一致,包括包名
即使类的完整类名一致,同时要求加载这个类的ClassLoader(指ClassLoader实例对象)必须相同;是引导类加载器、还是定义类加载器
第二章-运行时数据区
==方法区和堆是进程(虚拟机)共享的==
栈、程序计数器、本地方法栈是线程共享的
1程序计数器
程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器
==它是当前线程所执行的字节码的行号指示器。==
PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码。由执行引擎读取下一条指令。
1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
2.在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。
4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
5.字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令
6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收
利用javap -verbose xxx.class查看编译后的字节码
1.使用PC寄存器存储字节码指令地址有什么用呢(为什么使用PC寄存器记录当前线程的执行地址呢)
(1)多线程宏观上是并行(多个事件在同一时刻同时发生)的,但实际上是并发交替执行的
(2)因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行 (3)JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
所以,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。 2.PC寄存器为什么会设定为线程私有? (1)我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,如何保证分毫无差呢?
(2)为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是**为每一个线程都分配一个PC寄存器,**这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
2java栈
栈是由多个栈帧组成,一个栈帧中存在:局部变量表、操作数栈、动态链接、方法返回值,一个栈帧对应一个方法。
作用:主管java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、部分结果,并参与方法的调用和返回。
-
局部变量:相较于成员变量(成员变量或称属性)
-
基本数据变量:8种基本数据类型
-
引用类型变量:类,数组,接口
可以设置栈内存大小 -Xss
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)
** 栈帧内部结构**
每个栈帧中存储着
1.局部变量表(Local Variables)
2.操作数栈(Operand Stack)(或表达式栈)
3.动态链接(Dynamic Linking)(或执行"运行时常量池"的方法引用)—-深入理解Java多态特性必读!!
4.方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
5.一些附加信息
—-分割—-
第一章-JVM
0JVM内存模型
本地方法库:非java语言编写的方法。 本地方法栈:java中native关键字修饰的方法,表示这个方法不是用java编写的。 执行引擎:负责具体的代码调用及执行过程
程序计数器:它是当前线程所执行的字节码的行号指示器。
栈stack
栈数据线程独有,生命周期随着线程的死亡而结束。
它用来存放,8种基本类型,对象的引用变量,实例方法,都是在栈内存中分配。
java方法=栈帧。栈帧保存3类数据, 本地变量:输入,输出参数。 栈操作:记录出栈、入栈的操作 栈帧数据:包括类文件,方法等。
SatckOverFlowError栈内存溢出,出现原因,方法中调用本身,导致栈内存不足。
线程共享
1方法区,2堆
1方法区
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool) 、字段和方法数据、构造函数和普通方法的字节码内容。上 面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space) 和元空间(Metaspace)。
class类加载
- 通过类全限定名获取定义此类的二进制字节流;
- 将字节流代表的静态存储结构转换为方法区的运行时数据结构;
- 在内存中生成此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
JVM规范并没有规定java.lang.Class类的实例要放到Java堆中,对于HotSpot虚拟机,是放到方法区里面的。这个class对象作为程序访问方法区中的这些类型数据的外部接口。
/
2堆heap
判断对象是否已经死亡的算法:引用计数算法,可达性分析算法; 四个垃圾收集算法:标记清除算法,复制算法,标记整理算法,分代收集算法;
JDK1.8永久区变成了元空间
堆的比例大小。
2内存调优
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于:
永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
1 2 3 4 5
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。 long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。 System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB"); System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
发现默认的情况下分配的内存是总内存的“1 / 4”、而初始化的内存为“1 / 64”
VM参数: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
生产环境必须:初始内存Xms和 最大Xmx一致,避免内存忽高忽低。(避免GC和应用程序争抢内存)
-XX:+PrintGCDetails打印GC信息。
GC日志解读:
1 2 3 4 5 6 7 8 9 10 11 12
GC产生原因 分配失败** [GC (Allocation Failure) 发生了YoungGen ** 第一个箭头左边指Gc前的新生代内存大小 、GC后的新生代内存大小、GC区域新生代的总大小。** 第二个箭头指的是发生了YoungGen前堆的大小,发生后的堆的大小。** [PSYoungGen: 1519K->472K(1536K)] 6302K->6262K(8704K), 0.0010356 secs] YoungGC用户耗时,YoungGC系统耗时 YoungGC总耗时** [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 472K->0K(1536K)] [ParOldGen: 5790K->1692K(7168K)] 6262K->1692K(8704K), [Metaspace: 3249K->3249K(1056768K)], 0.0064030 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
3GC
GC是垃圾回收的意思,主要发生在堆内存中。
GC是什么(分代收集算法) 次数上频繁收集Young区 次数上较少收集Old区 基本不动元空间
判断对象是否已经死亡的算法:引用计数算法,可达性分析算法; 四个垃圾收集算法:标记清除算法,复制算法,标记整理算法,分代收集算法;
1判断死亡2个
引用计数算法,可达性分析算法;
2垃圾回收4个
1复制算法、2标记清除算法,3标记整理算法,4分代收集算法;
1复制算法
因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在==年轻代的垃圾回收算法使用的是复制算法==,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
但会浪费一半空间。
2标记清除算法
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。 其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,
优点:不需要额外空间。
3标记整理算法
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
标记整理,不再做垃圾回收,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存。
劣势:效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
4JMM
java memory model Java内存模型
JMM是一种抽象的概念并不真实存在,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量(包含实例成员变量,静态字段,构成数组对象的元素)的访问方式。
特征:原子、可见、有序
CPU>内存>硬盘
类加载:
静态构造块>构造块>构造方法
静态代码块只加载一次
文章作者 卢森林
上次更新 2020-08-03