生活是苦难的,我又划着我的断桨出发了..... 博客首页

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技术中会使用直接内存。

每天写一篇 !!!

posted @ 2020-12-12 15:35  黑黑XW  阅读(114)  评论(0编辑  收藏  举报