jvm:jdk中包含了jvm和“屏蔽操作系统差异的组件”
jvm各个操作系统之上是一致的;
屏蔽操作系统差异的组件:在各个PC上各有不同;
jdk包含了jre(jvm)
类生命周期
生命周期:类的加载->连接->初始化->使用->卸载
类的加载:查找并加载类的二进制数据(Class文件)
硬盘上的class文件,加载到jvm内存中
连接:确定类与类的关系,
验证:.class正确性校验;
准备:static 静态变量分配内存;并赋初始化默认值;
static int num=10; 在准备阶段,会把num=0,之后将0修改10;
在准备阶段,JVM中只有类没有对象。
初始化顺序:static>非static>构造方法
public class Student{
static int age ;//在准备阶段,将age=0;
String name;
}
解析:把类中符号引用,转为直接引用;
前期阶段,还不知道类的具体内存地址,只能使用com.fh.student来代替Student;com.fh.student就称为符号引用;
在解析阶段,JVM将com.fh.student映射成实际的内存地址,用内存地址来代替student,这种使用内存地址来使用类的方法称为直接引用。
初始化:给static变量赋予正确的值,(用户自定义的值)
static int num =10;由0变10;
使用:对象的初始化,对象的垃圾回收,对象的销毁
卸载:
JVM结束生命周期的时机:
正常结束;
异常结束/错误;
手动:System.exit():
操作系统异常;

JVM内存模型(java memory model 简称JMM)

JMM:用于定义(所有线程的共享变现,不能是局部变量)变量的访问规则;
JMM将内存划分为两个区:主内存区,工作内存区
主内存区:真实存放变量;
工作内存区:主内存中变量的副本,供各个线程所使用;工作内存为各个线程私有。从而产生多线程,造成脏读的根本原因;
注意:1.各个线程只能访问自己私有的工作内存(不能访问其他线程的工作内存,也不能访问主内存)
2.不同线程之间可以通过主内存作为中介访问其他线程的工作内存;
完成的步骤:不同线程之间交互数据时经历的步骤:
1.Lock:将主内存中的变量,表示为一条线程的独占状态;
2.Read:将主内存中的变量,读取到工作内存中去;
3.Load:将第二步中读取的变量拷贝到变量副本中;
4.Use:把工作内存中的变量副本,传递给线程去使用
5.Assign:把线程正在使用的变量,传递给工作内存中的变量副本中
6.Store:将工作内存中变量副本的值,传递到主内存中
7.Write:将变量副本作为一个主内存中的变量进行存储
8.Unlock:解决线程的独占状态;

JVM要求以上8个动作是原子性的,但是对于64位的数据类型(Long,Double)有些非原子性协议。说明什么问题:可能会出现半个;
如何避免:1.商用的JVM已经充分考虑该问题;2.如果你的JVM是原生态的,可以通过volatile double num标记double变量,避免读取半个数据的问题;

volatile

概念:JVM提供的一个轻量级的同步机制
作用:
1.防止JVM对long/double等64位的非原子性协议进行的误操作(读取半个数据的情况)
2.可以是变量对所有线程立即可见,(某个线程如果修改了工作内存中的变量副本,那么加上volatile之后,该变量就会立即同步到其他线程的工作内存中)
3.禁止指令的“重排序”优化
原子性:num=10;
非原子性: int num=10;->int num; num=10;
重排序:排序的对象就是原子性操作,目的就是为了提高执行效率,也就是优化程序;
int a=10;//1
int b;//2
b=20;//3
int c=a*b;//4
重排序的原则:不会影响单线程的执行结果;因此以上程序在经过重排序之后,可能的执行结果:1234;2314;且结果不被影响。
在双重检查式的懒汉式单例模式中:instance=new Singleton();//和上述情况类似,它不是一个原子性操作。(关于单例模式,自行了解)
在执行是会被拆分成一下动作:
1.JVM会分配内存地址、内存空间;
2.使用构造方法实例化对象;
3.instance=第一步分配好的内存地址;
根据重排序的知识,可知,以上三个动作真正执行时可能是123,也可能是132的顺序((在单线程中)不会影响的执行结果,)。
但是在多线程的环境下,使用132可能出现问题:
假设线程A刚刚执行完一下的步骤(即刚刚执行13,还未执行2)
1.正常0X123;
3.instance=0X123;
此时线程B进入单例程序,直接得到instance对象(注意,此instance是刚才线程A并没有new的对象)就去使用该对象,例如instance.xxx()就会出错。解决方案就是禁止执行重排序;(private volatile static Singleton instance =null)//单例
volatile通过内存屏障防止重排序问题:
1.在volatile写操作前,插入StoreStore屏障;
2.在volatile写操作后,插入StoreLoad屏障;
3.在读操作前:插入LoadLoad;
4.在写操作后:插入LoadStore;

volatile是否保证原子性、保证线程安全?—不能

要想保证原子性/线程安全,可以使用原子包:java.util.cocurrent.aotmic中的类,该类能够保证原子性的核心,是因为提供了compareAndSet()方法,该方法提供了CAS算法(无锁算法)

JVM运行时的内存区域

线程私有(对于JMM中工作内存):(三个)
虚拟机栈-VM Statck:
本地方法栈-Native Method Statck:
程序技术器-Program Counter Register:当前线程所执行的字节码的行号指示器;
简单可以理解为:class文件中的行号
注意:
1.一般情况下,程序计数器是行号,但如果正在执行的方法是native方法,则程序计数器的值undefined;
2.程序计数器是唯一一个不会产生内存溢出问题的区域;
java中没有goto,且不是关键字,是唯一的保留字;可以改变程序计数器的顺序;

虚拟机栈

定义:描述方法执行的内存模型
方法在执行的同时,会在虚拟机栈中创建一个栈帧
栈帧中包含:方法的局部变量表,操作数据栈,动态链接(我们常说的java的多态特性),方法出口等信息;

本地方法栈

和虚拟机栈原理差不多,不同点:虚拟机栈中存放的是jdk或我们自己写的方法,而本地方法栈是操作系统底层的方法;

线程共享

Person person=new Person();
左边是栈:本地方法栈:
右边是堆中:Heap堆;等号以右;
Heap:是存放对象实例(数组,对象),堆是虚拟机区域中最大的一块,在JVM启动时已经创建;
GC主要管理的区域;
堆本身是线程共享,但是堆内部可以划分多个线程私有的缓冲区;
堆允许物理空间不连续;只要逻辑连续即可;
堆可以分为新生代和老生代;新生代:老生代=1:2
新生代:eden:S0:S1=8:1:1
新生代的使用率一般是在90%。使用时,只能使用一个eden和一块S区间(s0或s1);
新生代:存放生命周期比较短的对象,和小的对象;反之就存放到老年代。对象的大小可以通过参数设置-XX:PretenureSizeThredshold。一般而言大对象一般是数组、集合、字符串;
新生代、老生代中年龄:MinGC回收新生代中的对象。如果Eden区中的对象在一次回收后仍然存在,就会被转移到S区,(年龄不增加),之后,如果MinorGC再次回收,已经在S区中的对象仍然存活,则年龄+1。如果年龄增长到一定的数字,则对象会被转移到老生代中。简言之;在新生代中的对象,没经过一次MinorGC,有三种可能:1.从Eden区转移到S去;2.已经在S区的年龄+1;3.已经在S区增加到一定年龄的,则转移到老生代;
新生代在使用时,只能同时使用一个S区(form;to):底层采用的是复制算法,为了避免碎片的产生;(空间连续便于寻址)
eden;
s0;
s1;
老生代:1.生命周期较长;2.大的对象;
新生代的特点:大部分对象都存在于新生代;且新生代的回收频率高、效率高;
老生代的特点:空间大;增长速度慢;频率低;
意义:可以根据项目中对象大小的数量,设置新生代或老生代的空间容量,从而提高GC的性能;
当然如果对象太多,也会导致内存异常;
虚拟机参数
-Xms128m:JVM启动时的大小;
-Xmn32m:新生代大小
-Xmx128:总大小;
JVM的总大小=新生代+老年代+永久代(永久代不存在了,存元数据;已经废弃了)

方法区

存放:类的元数据(描述类的信息)、常量池、方法信息、(方法数据,方法代码);
gc:在该区域回收类的元数据(描述类的信息)、常量池;
方法区中数据如果太多,也会抛出异常OutOfMemory异常;
常量池:存放编译期间产生的字面量(“abc”)、符号引用;

注意

导致内存溢出的异常;处理虚拟机中的4个区域以外,还可能是直接内存。在NIO(非阻塞IO)零拷贝技术;当java代码通过虚拟机直接操作操作系统的内存。