JVM模型
Class Loader类加载器
负责加载class文件,class文件在文件开头有特定的文件标识,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
Method Area方法区(永久区)
方法区是被所用的线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也是再此定义。简单说,所有定义的方法的信息都报错在该区域,此区间属于共享区间。 静态变量+常量+类信息+运行时常量池存在方法区中,它存储了每一个类的结构信息
注意:Java8去永久代,用元空间 替代。永久代使用的是JVM的堆内存,但是元空间并不在虚拟机中华而是使用本机的物理内存。
Heap堆
栈管运行,堆管存储
堆是线程共享区域,是java虚拟机管理最大的一块区域。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
PC Register 程序计数器
每一个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。线程私有。此区域是唯一一个没有OutOfMemoryError(OOM)的区域。
VM Stacks 虚拟机栈
栈管运行,堆管存储
线程私有。
虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
Native Method Stack 本地方法栈
它的具体做法是 Natice Method Stack 中登记native 方法,在Execution Engine 执行时加载native libraies。和虚拟机栈一样,都是线程私有的。
Native Interface
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,想要立足,必须有调用C/C++程序。目前,该方法使用的越来越少了,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以用WebService等等。
栈:一般指虚拟机栈
1)对于栈来说不存在垃圾回收问题。
2)基本类型的变量和对象的引用变量都在函数的栈内存中分配。
栈帧
栈帧(Stack Frame)是用于支持虚拟机运行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
堆
堆内存示意图
Eden:S0:S1=8:1:1
新生代:老年代=1:2 (新生代占堆的1/3,老年代占堆的2/3)
在新生代中存活15(默认)次以上才被放入老年代中
堆内存调优
参数 | 含义 | demo |
-Xms | 设置初始分配大小,默认为物理内存的“1/64” | -Xms5m |
-Xmx | 最大分配内存,默认为物理内存的“1/4” | -Xms20m |
-XX:+PrintGCDetails | 输出详细的GC处理日志 | -XX:+PrintGCDetails |
-XX:+PrintCommandLineFlags | 可以将隐式或者显式传给虚拟机的参数输出 | XX:+PrintCommandLineFlags |
-Xmn | 设置新生代的绝对大小,新生代一般会设置整个堆空间的1/3到1/4左右 | -Xmn20m |
-XX:SurvivorRatio | 用来设置新生代中eden空间和form/to空间的比例 -XX:SurvivorRatio=eden/from=eden/to | -XX:SurvivorRatio=2 |
-XX:NewRatio | 设置新生代和老年代的比例 -XX:NewRatio=老年代/新生代 | -XX:NewRatio=2 |
-Xss | 制定线程的最大栈空间,整个参数也直接决定了函数课调用的最大深度 | -Xss5m |
-XX:MaxTenuringThreshold | 新生代经过多少次GC后进入老年代,默认为15 | -XX:MaxTenuringThreshold=15 |
-XX:PretenureSizeThreshold | 设置对象的大小超过在制定的大小之后,直接晋升老年代 ,后面的1000为100k | -XX:PretenureSizeThreshold=1000 |
在实际工作中,我们可以直接将初始的堆大小与最大堆大小设置相等,这样的好吃是可以减少程序运行时的垃圾回收次数,从而提高性能。
收集算法
口诀
频繁收集Yong区 较少收集Old区 基本不动Perm区
标记清除算法
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
优点:不需要额外空间(和复试算法的缺点2比)
缺点:1)有碎片,空闲空间不连续
2)两次扫描,效率低
3)需要一个内存空间记录哪是空,哪是非空
复制算法
年轻代中使用的Minor GC ,这种GC算法采用的是复制算法(Copying)
优点:没有碎片
缺点:1)如果对象存活率很高(极端一点,100%),将全部数据复制费时费力
2)浪费一了 survice 1 的空间
标记整理算法
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
优点:无碎片,不需要额外空间
缺点:效率低,低于复制算法
垃圾收集器
Serial收集器
serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。
复制算法收集器。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本。
复制算法收集器。
Parallel Scavenage收集器
关注点为吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
复制算法收集器
Serial Old收集器
标记-整理算法
Serial收集器的老年版
Parallel Old收集器
标记-整理算法
在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Scavenge 加 Parallel Od
Parallel Old是Parallel Scavenge收集器的老年带版本
CMS收集器
标记-清除算法
CMS收集器数一种以获取最短回收停顿时间为目标的收集器。
收集器步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清除
优点:并发收集、低停顿
缺点:CMS收集器对CPU资源非常敏感
无法处理浮动垃圾
标记-清除法的缺点,有碎片
G1收集器
特点:并发与并行、分代收集、空间整理、可预测停顿
补充
类加载机制
注意:
初始化阶段会调用静态代码块
初始化会调非静态代码块和构造方法
先加载类在初始化类,加载只加载一次
加载和初始化不一样
加载的是类的模板
初试化的是类的实例对象
1)静态的大于代码块,代码块大于构造方法
静态块先执行,并且只执行一次
代码块比构造方法先执行,并行new几次运行几次(运行结果)
public class Person {
public Person() {
System.out.println("Person 构造方法");
}
static {
System.out.println("Person 静态代码块");
}
{
System.out.println("Person 普通代码块");
}
}
public class Start {
public static void main(String[] args) {
Person person = new Person();
System.out.println("-------");
Person person2 = new Person();
}
}
2)通过数组定义引用类,不会触发此类的加载
通过数组定义引用类,不会加载此类,因为加载此类必定会调用static静态代码快
public class Start {
public static void main(String[] args) {
Person[] persons = new Person[10];
}
}
3)加载子类会同时加载和初始化父类
先执行父类的静态块(看下图运行结果)
在执行子类的静态块(看下图运行结果)
在执行父类的普通块和构造方法(看下图运行结果)
最后执行子类的普通块和构造方法(看下图运行结果)
public class Man extends Person {
public Man() {
System.out.println("Man 构造方法");
}
static {
System.out.println("Man 静态代码块");
}
{
System.out.println("Man 普通代码块");
}
}
public class Start {
public static void main(String[] args) {
Man man = new Man();
}
}
4)通过子类引用父类的静态变量,不会导致子类加载和初始化,会导致父类加载,不会导致父类初始化
通过子类使用父类的静态变量,不会加载子类,会加载父类
会调用父类的静态代码块,不会调用父类的普通代码块
不会加载子类,自然不会调用子类的静态代码块
public class Person {
public Person() {
System.out.println("Person 构造方法");
}
static {
System.out.println("Person 静态代码块");
}
{
System.out.println("Person 普通代码块");
}
//静态成员变量
public static int value = 1024;
}
class Man extends Person {
static {
System.out.println("Man 静态代码块");
}
}
public class Start {
public static void main(String[] args) {
System.out.println(Man.value);
}
}
5)常量(final)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的加载和初始化。
public class Person {
static {
System.out.println("Person 静态代码块");
}
{
System.out.println("Person 普通代码块");
}
//静态final成员变量
public static final int value = 1024;
}
public class Start {
public static void main(String[] args) {
System.out.println(Person.value);
}
}
内存分配
1)对象的内存分配,往大的方向讲,就是在堆上分配
2)对象优先在Eden分配
3)大对象直接进入老年代
4)长期存活的对象进入老年代:对象在Survivor区每“熬过”一次Minor GC,年数加1,当它的年数增加到一定的程度(默认为15次),该对象就会晋升到老年代
类与类加载器
类加载器虽然只用于实现类的加载动作,但是它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由这加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。这句话可以表达得更通俗一些:如果比较两个类是否“相等”,只有这两个类是由同一个个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要他们的类加载器不同,那这两个类就必定不相等。
双亲委派模型
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次层次关系。例如类java.lang.Object,它存在rt.jar中,无论哪一个类加载器要加载这个类,最终都要委派给处于模型最顶端的启动类加载器进行加载,因为Object类在程序的各种累加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由于各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基层的行为也就无法保证,应用程序也将会变的一片混乱。
双亲委派模型中,ClassLoader 在加载类的时候,会先交由它的父 ClassLoader 加载,只有当父 ClassLoader 加载失败的情况下,才会尝试自己去加载。这样可以实现部分类的复用,又可以实现部分类的隔离,因为不同 ClassLoader 加载的类是互相隔离的。
Java内存模型(JMM)
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。
原子性
线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。即存在原子性问题。
缓存一致性(可见性)
在多核CPU,多线程的场景中,每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
有序性
除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
线程状态转换
新建(New):创建后尚未启动的线程处于这种状态。
运行(Runable):Runable包括了操作系统现线程状态中的Runing和Ready,也就是处于次状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
无线等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式的唤醒。
期限等待(Timed Waiting):处于这种状态的县城也不会被分配CPU执行时间,不过无需等待被其它线程显式的唤醒,在一定时间之后它们会由系统自动唤醒,在一定时间之后它们会由系统自动唤醒。
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待的时候有一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;“等待状态”则是等待一段世间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程进入这种状态。
结束(Terminated):已终止线程的线程状态,线程已经结束执行。
参考
深入理解Java虚拟机:JVM高级特性与最佳实践 第2版