JVM学习一
JVM: Java Virtual Machine
JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM内存划分
内存管理
对于Java运行时涉及到的存储区域主要包括程序计数器、Java虚拟机栈、本地方法栈、java堆、方法区以及直接内存等等。对于每个部分,都有其使用的条件。
1、程序计数器主要是取下一条指令,在Java里面主要是取下一条指令的字节码文件;
2、Java虚拟机栈主要是利用栈先进后出的特性存储局部变量表,动态链接等,主要包括堆内存和栈内存,对于程序员内存分析而言是特别重要的。
3、本地方法栈与上边的栈基本作用差不多,只不过这里是为Java方法而服务。
4、Java堆是内存管理中最大的一块,所有的线程共享这一块内容,同时该部分也是垃圾收集器的主要区域。
垃圾回收机制
虚拟机的垃圾回收机制是完善的,动态内存分配和回收是比较成熟的,在内存管理机制中,大部分都不需要我们考虑内存回收,只有Java堆和方法区需要我们考虑处理内存问题。
一般的对于内存回收首先就是判断某一个部分是生存还是死亡,主要是通过下面二种算法
-
引用计数算法,本算法实现简单,判定的效率也是比较高的,很多的软件都使用了该算法,但是主流的Java并没有选择该算法,核心的问题是该算法难以处理对象之间相互调用的问题。
-
根可达性分析算法,该算法核心思想是依靠判断对象是否存活来实现的,本算法是通过一系列的GC ROOTS的对象作为起始点,采用搜索的算法遍历引用链,如果搜索过程中没有发现该节点,则认为该节点是不可达的,即可回收的,在Java里面,一般可以使用该算法处理问题。
类的生命周期
生命周期:类的加载—>连接—>初始化—>使用—>卸载
1、类的加载
-
查找并加载类的二进制数据(class文件)
-
将硬盘上的class文件加载到JVM中
2、连接:确定类与类之间的关系
-
验证:.class文件的正确性校验
-
准备:static静态变量分配内存,并赋初始化默认值
static int num = 0 ; 在准备阶段 会把num赋值为 0 之后(初始化阶段)赋值为10
在准备阶段,JVM只有类,没有对象
初始化顺序:static --> 非static --> 构造方法
public class Student{
static int age ; //在准备阶段 将age赋值为0
String name ;
}
- 解析:把类中的符号引用,转为直接引用
//前期阶段,还不知道类具体的内存地址,只能使用“com.xingwei.pojo.Student”来代替Student,“com.xingwei.pojo.Student”就称为符号引用
//在解析阶段,JVM就可以将“com.xingwei.pojo.Student”映射成实际的内存地址,会使用内存地址来代替Student,这种使用内存地址来使用类的方式称为直接引用
3、初始化:给static赋予正确的值
static int num = 0 在连接的准备阶段 会把num = 0,之后的num赋值为10
4、使用:对象的初始化、对象的垃圾回收、对象的销毁。
5、卸载
JVM内存模型
JVM内存模型:Java Memory Model:简称 JMM
JMM:用于定义(所有线程的共享变量,不能是局部变量)变量的访问规则。
JMM将内存划为两个区:主内存区、工作内存区。
主内存:真实存放变量
工作内存区:主内存中变量的副本,供各个线程使用。
注意:
1、各个线程只能访问自己私有的工作内存(不能访问其他的工作内存,也不能直接访问主内存)
2、不同线程之间,可以通过主内存,间接的访问其他线程的工作内存。
JVM要求以上的8个动作必须是原子性的操作,但是JVM对于64位的数据类型(long、double)有些非原子性协议。
问题:在执行以上8个动作时,可能会 只读取(写入)了半个long、double数据,因此,出现错误。
如何避免?
-
商用JVM已经考虑了此问题,无需我们操作
-
可以过volatile避免此类问题(读取半个数据的问题)volatile double num ;
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
重排序不会影响 单线程的执行结果, 因此以上程序在经过重排序之后,可能的执行结果 1,2,3,4 / 2,3,1,4
// 2,3,1,4
int b ; //2
b = 20 ; //3
int a = 10 ; //1
int c = a + b ; //4
通过单例模式来分析volatile关键字
package com.xingwei.day01;
//双重检查的懒汉式单例模式
public class Singleton {
private static Singleton instance = null ; //单例
//构造器私有化
private Singleton(){}
//双重检测锁
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton(); // 不是一个原子操作
}
}
}
return instance ;
}
}
以上代码可能会出现问题,原因instance=new Singleton();不是一个原子性操作,会在执行时拆分成以下动作!
// 1、JVM会分配内存地址,分配空间
// 2、使用构造方法实例化对象
// 3、instance = 第1步分配好的内存地址
// 根据重排序的知识,可知,以上3个动作在真正执行时 可能 1、2、3 也可能 1、3、2
// 如果在多线程情况下 使用1、3、2可能会出现问题,假设线程A刚刚执行以下步骤(即刚执行1、3,还没有执行2)1、正常0x123,3、instance = 0x123,
//此时,线程B进入单例程序的if,直接会得到instance对象(注意,此instance是刚才线程A并没有new的对象),就去使用该对象,例如instance.xxx()。
// 解决方案,就是禁止程序使用1、3、2的重排序顺序。解决:
public volatile static Singleton instance = null ; //单例
volatile是否能保证原子性,保证线程安全?
不能的
package com.xingwei.day01;
import java.util.concurrent.atomic.AtomicInteger;
public class TestVolatile {
// static volatile int num = 0 ;
static AtomicInteger num = new AtomicInteger(0) ;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100 ; i++) {
//每个线程 将num类似3万次,100个线程,在线程安全时 综合的结果应该是300万
new Thread(()->{
for (int j = 0; j < 30000 ; j++) {
// num ++ ; //不是一个原子性操作
num.incrementAndGet() ;
/**
* num = num + 1 ;
* ① num + 1
* ② num = ①的结果
*/
}
}).start();
}
Thread.sleep(1000);
System.out.println(num);
}
}
要想保证原子性/线程安全 可以使用java.util.concurrent.aotmic中的类,该类能够保证原子性的核心,是因为提供了compareAndSet()方法,该方法提供了cas算法(无锁算法)。
JVM运行时的内存区域
程序计数器
概念:行号指示器,指向当前线程所执行的字节码指令的地址
Test.java —> Test.class
int num1 = 1 ; // 1
int num2 = 2 ; // 2
if (num1>num2) { // 3
... // 4--10
} else {
...
}
while(...){
...
}
简单的可以理解为:class文件中的行号。
注意:
1、一般情况下,程序计数器 是行号 但如果正在执行的是native方法,则程序计数器的值是undefined
2、程序计数器 是唯一一个不会产生 “内存溢出” 的区域 1G=1024M=10241024KB1024KB
虚拟机栈
定义:描述方法的内存模型
-
在方法执行的同时,会在虚拟机栈中创建一个栈帧
-
栈帧中包含:方法的局部变量,操作数据栈,动态链接,方法出口信息等
-
局部变量表用于存储方法参数和局部变量。当第一个方法被调用的时候,他的参数会被传递至从0开始的连续的局部变量表中。
-
操作数栈用于一些字节码指令从局部变量表中传递至操作数栈,也用来准备方法调用的参数以及接收方法返回结果。
-
动态连接用于将符号引用表示的方法转换为实际方法的直接引用
-
-
当方法太多时,就会发生栈溢出异常StrackOverflowError,或者内存溢出异常 OutofMermory (递归调用)
public class VMStrack {
public static void main(String[] args) {
main(new String []{"aaa","bbb"});
}
}
本地方法栈
原理和结构与虚拟机栈一致。
不同点:虚拟机栈中存放的是jdk或者是我们写的方法,而本地方法栈调用的是操作系统底层的方法。
堆
-
存放对象的实例(数组、对象)
-
堆是虚拟机区域中最大的一块,在jvm启动的时候已经创建完毕。
-
GC主要管理区域
-
堆本身是线程共享的,但是在堆内部可以划分出多个线程私有的缓冲区。
-
堆允许物理空间不连续,只要逻辑连续就可以。
划分
堆可以分为新生代、老生代。大小比例,新生代:老年代 = 1:2。
-
新生代中包含Eden、S0、S1 = 8:1:1,
-
新生代的使用率一般在90%。在使用时,只能使用一个Eden一块s区间(S0/S1)
-
新生代:1、存放生命周期较短的对象 2、小的对象,反之存放在老生代中。 3、对象的大小,可以通过参数的设置
-xx:PretenureSizeThredshold
,一般而言,大对象一般是集合、数组、字符串。生命周期参数的设置:-XX:MaxTenringThredshold -
新生代、老年代中年龄:MinorGC回收新生代中的对象,如果Eden区中的对象在一次回收后仍然存活,则会被转移到s区中;之后,如果MinorGC再次回收,已经在s区中的对象任然存活,则年龄+1,如果随着年龄增长到一定的数字,则会被转移到老生代中(默认是16)。
-
简言之,在新生代中的对象,没经过一次MinorGC,有三种可能 1、从Eden -> S 区 ; 2、已经在S区中的 年龄+1;3、转移到老生代中。
新生代在使用时,只能使用一个S区:底层采用的是复制算法;为了避免碎片的产生。
复制算法:就是将一个不连续的空间变成一个连续的空间。
老生代:生命周期较长的对象,较大的对象;使用的垃圾回收器:MajorGC、FullGC
新生代特点
- 大部分对象都存在于新生代里面
- 新生代的回收频率高、效率高
老生代特点
- 空间大
- 增长速度慢
- 垃圾回收频率低
划分的意义:可以根据项目中 对象的大小的数量,设置新生代或老年代的空间容量,提高GC的性能。
如果对象太多,也会导致内存异常。
虚拟机参数
-Xms128m:JVM启动时的大小
-Xmn32m :新生代大小
-Xmx128:总大小
以前:JVM总大小=新生代+老年代+永久代(元数据 Class)
现在:JVM总大小=新生代+老年代
堆内存溢出的示例(**OOM **)
package com.xingwei.day01;
import java.util.ArrayList;
import java.util.List;
public class TestHeap {
public static void main(String[] args) {
List list = new ArrayList();
while (true){
list.add(new int[1024*1024]);
}
}
}
// 执行结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at com.xingwei.day01.TestHeap.main(TestHeap.java:13)
方法区
存放:类的元数据(描述类的信息;jdk8以后)、常量池、方法信息(方法数据,方法代码)
GC:主要回收类的数据(描述类的信息)、常量池
方法区中的数据如果太多,也会抛出OutOfMemoryError异常
常量池:存放编译期间产生的字面量,符号引用
注意:导致内存溢出的异常,除了虚拟机的4个区域以外,还可能是直接内存。在NIO技术中会使用直接内存。
每天写一篇 !!!