文章目录
JVM位置
- 在操作系统之上,java开发环境之下,而操作系统之下是硬件体系
- 也正是因为java是运用在操作系统之上, 所有会有native方法, 这个方法是操作操作系统的东西,jvm是处理不了的, 所以使用native方法区调用操作系统的c++/c代码
JVM体系结构
- 简单来说执行过程就是, .java源文件通过javac编译,生成.class字节码文件, 然后类加载器对字节码文件进行加载,加载好的类对象放入jvm中, Jvm通过执行引擎,将字节码文件,转化为可执行的机器码指令。
类加载过程
- 当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
加载
- 加载指的是将类的class字节码文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
- 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。(AppClassLoader, ExtClassLoader, BootClassLoader)
链接
- 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的字节码数据合并到JRE中。类链接又可分为如下3个阶段。
- 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
- 解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
- 初始化 :为类的静态变量赋予正确的初始值, 执行静态代码块,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“XXX”))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
双签委派模型
- 涉及到的三个类加载器
appclassLoader(自己定义的一些类)
extclassLoader(java中扩展的一些类, 比如导入包什么的)
bootclassLoader(java库中自带的类)
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器
3.启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
4.重复步骤三
- 使用双亲委派模型就是为了类加载的安全
- 在java中很多代码是java自己的,我们是不能对其修改的, 比如基础数据类型的代码和String的代码, 看如下的代码
package java.lang;
/**
* Created with IntelliJ IDEA.
* Description: If you don't work hard, you will be a loser.
* User: Listen-Y.
* Date: 2020-11-29
* Time: 11:07
*/
public class String {
public String toString() {
return "hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
}
}
- 这个代码就是在AppClassLoader中向上传递给ExtClassLoader加载, 然后在给bootclassLoader找到对应的类java.long.String找到了对应的类, 但是找不到main方法了, 就抛出了错误
- 对于这些错误吗就引出了沙箱安全机制
沙箱安全
-
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境.
-
沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
-
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
-
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。
-
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限(在底层就是加了一个是否受信任的判断)。
-
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制(将不同的权限分为不同的组)。
-
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限
本地方法栈
- 凡是带了native 关键字的方法,说明java的作用范围达不到了,回去调川底层c语言的库!会进入本地方法栈
- 调用本地方法本地接口 JNI
- JNI作用:扩展ava的使用,融合不同的编程语言为ava所用!最初:C、C++.l/ Java诞生的时候C、C++战行,想要立足,必须要有调川c、C++的程序
- 它在内存区城中专门开辟了一块标记区域: Native Nethod Stach,登记 native 方法在最终执行的时候,加载本地方法库中的方法通过JNI
- Java程序驱动线程, 打印机这些
程序计数器
- 程序计数器:Program Counter Register
- 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例对象存在堆内存中,和方法区无关
栈
- 每执行一个方法就会产生一个栈帧, 程序正在执行的方法一定在栈的顶部, 执行完就退出了, 但是调用下一个方法的时候就会压入栈,先进后出
- 所以栈溢出就是俩个或俩个以上的方法的父帧和子帧互相引用, 导致所有的栈帧不能完全出栈
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题一旦线程结束,栈就Over! - 每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。
- 其中虚拟机栈中的局部变量表部分是人们比较关心的部分。局部变量表存放了编译期可知的各种基本数据类型和returnAddress类型,需要注意的是其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
简单类对象的实例化过程
1、加载得到类信息放入方法区中,类对象放到堆中;
2、在栈内存申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性进行默认初始化,类成员变量显示初始化;
5、构造方法进栈,进行初始化;
6、初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈;
子类对象的实例化过程
1、在先加载父类,再加载子类,加载得到类信息放入方法区中,类对象放到堆中;
2、在栈中申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性(包括父类的属性)进行默认初始化;
5、子类构造方法进栈;
6、显示初始化父类的属性;
7、父类构造方法进栈,执行完毕出栈;
8、显示初始化子类的属性;
9、初始化完毕后,将堆内存中的地址值赋给引用变量P,子类构造方法出栈;
堆
一个JVM只有一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,就会保存我们所有引用类型的真实对象
堆内存中还要细分为三个区域:
·新生区(伊甸园区)Young/New
·养老区OLD
.永久区
OOM异常就是堆内存不够了, 所以jvm调优也是主要调节这里
年轻代与老年代
-
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。
-
新生代又分为
-
Eden 区: Java 新对象的出生地 (如果新创建的对象占用内存很大,则直接分配到老年代)。当 >> Eden 区内存不够的时候就会触发MinorGC,对新生代区进行 一次垃圾回收。
-
SurvivorFrom: 上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
-
SurvivorTo : 保留了一次 MinorGC 过程中的幸存者。
-
在年轻代由于对象更新快, 所以使用的是轻GC的复制算法 + 标记清除
1、复制:将eden,survivorFrom 不能死的对象复制到 survivorTo,年龄+1,如果空间不够这个对象有很大的时候,则放到老年区
2、清空:将eden和survivorFrom中的对象清空
3、互换:survivorTo和survivorFrom互换,原来的survivorTo作为下一次GC的survivorFrom区。 -
老年代主要存放应用程序中生命周期长的内存对象。
-
老年代的对象比较稳定,所以 GC 不会频繁执行。使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 重GC 进行垃圾, 回收腾出空间。
-
重GC 采用标记清除 + 整理算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。重GC 的耗时比较长,因为要扫描再回收。重GC 的清除算法会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配所以再使用整理算法。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。
永久代
- 指内存的永久保存区域,主要存放 Class 和 MetaSpace(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出OOM 异常。
- 这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface接口数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;
jdk1.6之前︰有永久代,常量池是在方法区
jdk1.7:永久代慢慢的退化了,去永久代,常量池和类的静态变量放在堆中
jdk1.8之后:无永久代, 元空间使用本地内存
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制, 这也是为什么我们使用PringGCDetails看到没有这个区域的原因。类的元数据放入 native memory, 字符串池和类的静态变量放入 java堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
堆内存调优
年老代年轻代大小划分是否合理
内存泄漏
垃圾回收算法设置是否合理
- 默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存: 1/64
内存快照分析工具MAT, Jprofiler, lVM(VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。)
- 分析Dump内存文件,快速定位内存泄漏
- 获取堆中的数据
- 获得大对象
- 获得每个线程对堆内存的一个消耗
常见参数:
-XX:+HeapDumpOnOutOfMemoryError //如果抛出oom异常就输出dump文件
-XX:+PringGCDetails //输出GC清理垃圾的细节
-XX:MaxTenuringThreshold=15 //经过15次的GC才会进入老年代
-XX:PretenureSizeThreshold //设置值让大对象直接进入老年代
-Xmx 最大堆内存
-Xms 初始堆内存
-Xmn 年轻代大小
-Xss 创建一个线程分配的内存大小
常见垃圾收集器与算法
- Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法,年老代主要使用标记-整理垃圾回收算法,因此java虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器
- Serial: Serial是一个单线程的收集器,使用复制算法, 它不仅仅只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程降低效率,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
- ParNew: 是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
- Serial Old: 老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
- CMS: 年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,CMS收集器是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作。
- G1: 相比与CMS收集器,G1收集器两个最突出的改进是:a.基于标记-整理算法,不产生内存碎片。b.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。而且G1还是一个老少通吃的垃圾收集器, 它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。
垃圾收集算法
- 主要分为加个块, 一个是判断这个对象是不是该死, 一个是去杀死这个该死的对象
引用计数法
- 引用计数器就是一个消耗, 而且如果是循环引用就会出错
可达性分析
- 判断这个对象可不可以到达, 以GCRoot为起点在栈中的局部变量表中, 方法区的静态引用, JNI的引用去找
复制算法
- 需要额外空间, 而且复制起来效率低
标记清除算法
- 对活着的对象进行标记,死了的去掉, 会有碎片化问题,而且是俩次扫描
标记整理
- 如果对象多的话, 移动起来还是很费事
时间复杂度: 复制算法 > 标记清除算法 > 标记整理算法
内存整齐度: 复制算法 =标记整理 > 标记清除算法
内存利用率: 标记整理 > 标记清除 > 复制算法
JMM内存模型
- JMM是java内存模型, 是一个抽象的概念
- 作用: 提供缓存一致性协议, 定义多线程下数据读写的规则
- JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。
- 因此JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
JMM指令
- 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM规则
- JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
模型特征
- 原子性:例如上面八项操作,在操作系统里面是不可分割的单元。被synchronized关键字或其他锁包裹起来的操作也可以认为是原子的。从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。
- 可见性:每个工作线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。volatile关键字要求被修改之后的变量要求立即更新到主内存,每次使用前从主内存处进行读取。因此volatile可以保证可见性。除了volatile以外,synchronized和final也能实现可见性。synchronized保证unlock之前必须先把变量刷新回主内存。final修饰的字段在构造器中一旦完成初始化,并且构造器没有this逸出,那么其他线程就能看到final字段的值。
- 有序性:java的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,volatile和synchronized也能保证指令不进行重排序。(重排序包括编译器的重排序, 指令的重排序, 操作系统内核的执行重排序)
Happen-Before(先行发生规则)
- 先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。
- Happen-Before的规则有以下几条
- 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
- 管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
- volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动作
- 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作
- 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
- 对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
- 传递性(Transitivity):如果对于同一个线程的三个操作,操作A先于操作B、操作B先于操作C,则操作A先于操作C