JVM概念
- JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
- JDK中包含JVM和底层屏蔽操作系统差异的组件,不同系统的中JVM是相同的,但是底层屏蔽操作系统差异的组件是不相同的,所以要下在不同版本的JDK。
类的生命周期
-
声明周期:加载–>连接–>初始化–>使用–>卸载
-
加载:在硬盘中查找类的二进制文件(class文件),并加载进JVM中。
-
连接:确定类与类之间的关系,例如:Student stu = new Student(address)中Student和address类之间的关联关系,并且连接中包含三个阶段,分别为验证、准备、解析:
验证:验证class文件的正确性,或者是否存在修改;
准备:为静态变量分配内存,并且赋初始默认值,在准备阶段只有类,没有对象;
解析:在解析阶段,JVM会使用符号引用,类似“indi.dsl.entry.Studnet”的形式代替“Student”,最后将“indi.dsl.entry.Studnet”映射成为实际的内存地址来代替“Student”。 -
初始化:将各个静态变量赋予真正的值,例如:默认值int = 0;变为 int = 10;
-
使用:对象的初始化、对象的垃圾回收、对象的销毁;
-
卸载:被自定义加载器加载的对象清除出内存区域。
JVM结束生命周期的时机
- 正常结束
- 异常结束/错误
- System.exit():操作系统推出
- 操作系统异常
针对于JVM运行时内存区域第一种划分方式—JVM内存模型(Java Memory Model,简称JMM)
- 不同线程之间数据交互时经历步骤,如图所示:
- 特别注意:
1.各个线程只能访问自己私有的工作内存,不能够访问其他的工作内存和主内存;
2.各个线程可以通过主内存间接的访问其他线程的工作内存。 - JVM要求以上图片中的8个动作必须是原子性的,但是对于64位的数据类型(long或者double)会产生非原子性问题,即读取该数据类型中的数据一半的数据,造成数据读取异常,有一下两种方式进行避免:
1.商用JVM已经充分考虑了此问题,正常使用无需特殊操作;
2.使用Volatile关键字解决JVM只读取半个数据的问题,例如:volatile long num;的方式去定义变量。
Volatile关键字
-
概念:JVM提供的一个轻量级的同步机制
-
作用:
1.放置JVM对long/double等64位的非原子性数据的误操作(只读取半个数据)-------不保证原子性
2.如果某个变量定义时添加volatile关键字,可以使该变量对所有线程立即可见,如果某一个添加voltile关键字的变量修改了工作内存中某个变量副本中的值,那么该变量的值会立刻同步到其他线程中的该变量的值。---------保证可见性
3.禁止指令的“重排序”优化。-------禁止指令重排 -
原子性和线程安全性
1…volatile关键字是不能保证原子性,并且不是线程安全的。
2.如果要原子性和线程安全的,可以使用原子包java.util.cocurrent.aotmic中的类,能够保证原子类的核心为该类中提供compareAndSet()方法,该方法提供了cas算法(无锁算法)
重排序
- 概念:排序的对象就是原子性操作,目的是为提高执行效率,是一种JVM对代码的一种优化方式。
- 对于单线程的重排序,是不会影响代码的执行结果。
JVM运行时内存区域第二种划分方式
线程私用区
-
划分图解:
-
程序计数器
-
概念:行号指示器,用于指向当前线程所执行字节码指令的地址,简单理解为class文件的行号。
- 注意:
1.一般情况下程序计数器是class文件的行号,但是如果程序中正在执行的方法是native方法(系统方法),则程序计数器的值为undefined。
2.程序计数器是唯一一个不能产生“内存溢出的”(Out Of Memory–OOM)的区域。
- 注意:
-
虚拟机栈
-
概念:描述JDK中或者自行编写的方法执行的内存模型。
-
栈帧:每个方法在虚拟机栈中为一个栈帧,栈帧中包含:局部变量表、操作数据栈、动态链接、方法出口。
-
局部变量表:其中存放着方法中的局部变量,可以是简单类型也可以是复杂类型(对象),例如:int num = 10;
-
操作数据栈:其中存放在局部变量表中的所有的值,例如:int num = 10;中的10就会存放在操作数据栈中。
-
动态链接:符号引用(例如:indi.dsl.entry)会在实际代码执行时(多态),转化为不同地址;因此,转换行为会在每一次运行时都会发生一次。类似于如下代码:
Person per=null; if(per instanceof Worker){ Person per = new Worker(); }else{ Person per = new Student(); }
-
方法出口:本次方法执行完成后,指向下一步所要进行的信息。
-
-
本地方法栈
- 原理与结构与虚拟机栈一致,不同点:虚拟机栈中存放着JDK或者自己 编写的方法,而本地方法栈调用的是操作系统底层的方法。
线程共享区
-
结构图:
-
堆(Heap):
1.堆中存放为对象实例(数组、对象);
2.堆是内存区域中最大的一块区域,在JVM启动时就已经创建完成;
3.GC主要管理堆区域;
4.堆本身是线程共享的,但是堆的内部可以划分出多个线程私有的缓冲区;
5.堆允许物理空间不连续,只要逻辑连续即可;
6.堆可以分为新生代、老生代两大区域,并且大小比为:新生代:老生代=1:2
7.新生代中包含:eden、S0(From Survivor)、S1(To Survivor),并且大小比为:eden:s0:s1=8:1:1;
8.新生代的使用率一般在90%,在新生代的使用中eden只能和一块S区域(S0\S1)一起使用,原因时底层采用“复制算法”,为了避免存储空间产生碎片。
9.新生代存放生命周期较短的对象或者是小对象,老生代存放生命周期长的对象或者大的对象(集合、数组、字符串),新生代的对象声明周期可以通过虚拟机参数:-XX: PretenureSizeThreShold;
10.从新生代到老生代的年龄机制:MinorGC是新生代中的垃圾回收器,如果Eden中的对象在一次回收中存活,就会被转移到S区域中;之后MinorGC再次回收时,已经在S区域中的对象如果依然存活,则年龄+1处理,直到年龄达到一定数值时,对象会被转移到老生代区。简言之,在新生代中的对象,在每一次MinorGC时,有三种可能出现:①从Eden区域–>S区、②已经在S区中的对象年龄+1、③转移到老生代中(可以从Eden区域中转入或者从S区域中转入)。
11.老生代的对象声明周期可以使用虚拟机参数:-XX:MaxTenusingThreshold,老生代的垃圾回收器为MajorGC或者FullGC(也会回收全部堆空间)。-
新生代特点:
1.大部分对象存在与新生代;
2.新生代的对象回收频率高、效率高。 -
老生代特点:
1.空间大
2.增长速度快
3.频率低
-
-
方法区:
- 存放:类的元数据(描述类的信息)、常量池(常量)、静态成员变量(静态全局变量)、方法信息(方法数据、方法代码),其中常量池为编译期产生的字面量(例如:“abc”)、符号引用。
- GC只会回收方法区域中的元数据和常量池
- 方法区域中的数据如果太多,也会出现OutOfMemory(内存溢出)
-
执行解析图:
-
特别注意:产生内存溢出的异常,除了虚拟机中的4个区域以外,还存在“直接内存”,直接内存为系统内存,即本身电脑的系统内存溢出。
类的使用方式–类的初始化
- 类的初始化:JVM只会在“首次主动使用”一个类或者接口时,才会初始化类。
主动使用
- 使用new时:即使用new来构造类时
- 访问类或者接口的静态成员(属性、方法)时也会初始化类
- 使用反射Class.forName(“xxx.xx”)方法执行时使用的类会被初始化
- 初始化子类时,父类也会被初始化
被动使用
- main()方法也是静态方法,在main方法被执行时,main方法所在的类也会被初始化
- 如果成员变量既是static,又是final,即为常量,则成员变量被调用时,成员变量所在类不会被初始化
- 如果成员变量既是static,又是final并且该成员变量的值为随机数,则成员变量被调用时,成员变量所在类会被初始化
四种对象的引用方式
- 四种对象的引用方式:强引用、软引用、弱引用、虚引用,并且从左至右越容易被回收。
- 强引用(例如:Person per = new Person();)只有在对象的生命周期结束或者被置为null时(Person per =null),才会被GC回收,除了以上两种情况外,任何时候GC都不会回收强引用。
- 软引用:利用Refence类的子类SoftRefence新建立的对象成为软引用,根据JVM内存情况,如果内存重组则GC不会随便回收软引用;如果JVM内存情况不足,GC则会主动回收软引用对象。
- 弱引用:利用Refence类的子类WeakRefence新建立的对象成为软引用,只要GC执行,就会将弱引用对象回收。
- 虚引用(幻影引用或者幽灵引用):使用java.lang.ref.PhontomRefence来创建虚引用对象,是否使用虚引用,和引用对象无关,无法通过虚引用来获取对象本身;虚引用不能够单独使用,回合引用队列一起使用。因为虚引用的执行机制是:GC–>如果有虚引用–>虚引用入队–>虚引用出队–>回收对象,所以可以在对象回收之前增加其他操作
双亲委派
-
双亲委派概念:双亲是指JVM自带的类加载器(在JVM的内部所包含,C++)和用户自定义类的加载器(独立于JVM之外的类加载器),二者称为双亲,委派是指二者对类的加载过程。
-
JVM自带的加载器
1.根加载器–BootStrap:加载jre\lib\rt.jar(包含编写的代码中大部分JDK中的API),也可以让根加载器指定加载某个jar(通过设置JVM参数:-Xbootclasspath=a.jar)
2.扩展类加载器–Extension:jre\lib\ext/*.jar;也可以让根加载器指定加载某个jar(通过设置JVM参数:-Djava.ext.dirs=xxx.jar)
3.应用加载器或者系统加载器–App/System:加载classpath下的类;也可以指定加载某个jar或者某个类(通过设置JVM参数:-Djava.class.path=xxx.jar/xxx类) -
用户自定义加载器
都是抽象类java.lang.ClassLoader的子类
-
类加载过程
-
双亲委派过程:当一个加载器加载类的时候,自己先不加载,而是逐层向上交由双亲去加载,如果双亲中存在某一个合适的加载器加载成功,会向下回馈成功信息,如果所有双亲都无法加载,则会报出类加载异常。
-
小结:
1.如果类是rt.jar中的,则该类会被bootstrap(根加载器)加载,如果是classpath中的类(自家编写的类),则该类会被AppClassLoader(系统加载器或者叫应用加载器)加载。
2.定义类加载:最终加载类的加载器;
3.初始化类加载器:直接面对加载任务的类,即第一次直接面对的类的加载器类。
-双亲委派机制优势:可以有效防止用户自定义类和rt.jar中的类重名,而造成的混乱。
如果自己编写的类与rt.jar中的类重名,控制台会报错如下图所示:
- 报错原因:根据双亲委派,越上层的加载器越优先执行,最顶层的加载器是根加载器,根加载器会加载rt.jar中的类,因此rt.jar中的类中类的方法会被优先加载,即最终加载的不是自己编写的类中的同名方法,而在加载rt.jar中的类时没有自行编写的main()方法,因此会报错无法找到main()方法。
类的卸载
- 系统自带(根加载器、扩展加载器、系统加载器),这些加载器的类是不会被卸载的。
- 用户自定义的加载器的类,会被GC卸载的。
GC调优
-
调优实际上是一种取舍,通常是以XX换取XX的策略。因此在调优之前必须明确调优的方向:低延迟?OR 高吞吐量?
-
有两种方式需要考虑:
1.在已知条件相同的情况下,牺牲低延迟 来换取高吞吐量,还是反之处理。
2.随着软硬件的技术提升,可能二者都会提升。 -
目前在已知现有的条件下,可以尝试调大新生代的空间,或者调大新生代到老生代的年龄阈值,从而降低短生命周期对象从新生代转移到老生代的概率。