栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java基础JVM虚拟机

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java基础JVM虚拟机

前言:

本篇博文全文大约5万字左右,如果你觉得对你有帮助请给博主一个三连喔,就是对我的最大肯定,最近在学习It老齐的图解轻松学JVM,视频讲解的非常的详细,所以花了三天的时间进行了整理,通过本篇博文进行记录。

本篇博文目录:
      • 一.JVM相关基本概念
        • 1.JVM与混合语言编程
        • 2.JVM发展及种类
      • 二.JVM的组成
        • 1.字节码
          • (1) 什么是字节码
          • (2) 字节码要素
          • (3) 字节码指令简介
          • (4) 字节码指令分类概述
        • 2.类加载执行过程
          • (1) 加载阶段
          • (2) 连接Linking阶段
          • (3) 初始化Initialization阶段
          • (4) 类加载器种类
          • (5) 双亲委派模型
        • 3.运行时数据区的组成
          • (1) 程序计数器
          • (2) 虚拟机栈
          • (3) 本地方法栈
          • (4) 堆Heap
          • (5) 方法区
      • 三. Grabage Collection(GC垃圾回收机制)
        • (1) 对象的创建和销毁
        • (2) 三种垃圾回收算法
        • (3) GC垃圾收集器
        • (4) JVM优化
        • (5) JVM监控命令
        • (6) 调优软件Arthas

一.JVM相关基本概念 1.JVM与混合语言编程

( JVM并不只为Java而开发,可理解为是一个跨平台开发的平台,不同的语言通过相应的编译器生成规范一致的.class字节码文件,JVM虚拟机就会去执行该字节码文件 )

2.JVM发展及种类

JVM是一钟规范标准,只要自己编写的虚拟机符合该标准,并且取得相应认证(Oracle TCK验证),就可以认为是一个合格的虚拟机。


目前虚拟机按厂商进行划分可以划分为二类SUN公司与其他厂商:
SUN(Oracle公司)虚拟机各个版本信息如下:

  • Sun Classic VM 1.0

  • Sun Exact VM 1.2

  • Sun Hotspot VM

其他厂商虚拟机信息如下:

  • JRockit VM

  • IBM J9 VM

  • Taobao VM


Oracle/SunJDK与OpenJDK的异同:

二.JVM的组成

1.字节码 (1) 什么是字节码
  • java源码经过javac编译生成的二进制(文件),称为字节码(文件)
  • JVM通过字节码保证平台无关特性
  • Java并不是唯一生成class文件的语言
  • Class是结构紧凑的二进制流,其格式固定,要求严谨
(2) 字节码要素

字节码的组成结构:

魔数

魔数就是区分文件类型的依据

例子:
OxCAFEBABE是Java字节码文件的魔数,保存在前4个字节

实操:使用winhex软件查看文件的魔术:

文件版本号

  • 5~6字节是次要版本号
  • 7~8字节是主要版本号,Java 8 = 52.0

实操查看JVM版本(从下图可知Java版本为1.5):

我们也可以通过 javap -v -l -c 命令进行查询(下图查询的字节码文件是通过Java8编写):

注意: 字节码版本高于JVM版本,产生Unsupported ClassVersionError

为了更好的观察字节码文件可以使用IDEA JClassLib插件,安装过程如下:


构建项目:

先选择要查看的字节码文件

选择View --> Show Bytecode With Jclasslib

常量池

通过IDEA JClassLib插件观看字节码中常量池信息:
在一般信息这里可以看到常量池计数为84


常量池中显示的下标从1开始到83结束,但是在一般信息中却显示有84个信息,这是因为有一个0下标为状态位预留的位置。

ConstantPoolSample类中的字符串信息如下(勾选不重复字符串)

我们看看常量池中字符串信息(刚好有六个):

并且这六个字符串就是我们在ConstantPoolSample类中勾选的不重复字符串

但是在保存的时候字符串并没有保存对应的字面量(字面量就是final修饰的数值和相关的字符串的值如: <姓名:> ),而是保存对应的地址(一种关联关系)如下图中的 cp_info #58

该地址下就存放了该字符串对应的详细数据:

符号引用就是上面这种关联关系:

ConstantPoolSample类中与类相关的信息如下图:

在常量池中类信息包含显示和隐式,并且也是采用关联关系

如字符串拼接实际上采用的是StringBuilder类进行字符串拼接


ConstantPoolSample中的字段信息

常量池中也对应了相关字段信息:

该字段也是采用符号引用

数据类型对应的也是一个符号引用

这里的D表示的就是double

ConstantPoolSample类中的方法信息如下:

常量池中方法的信息如下:

相关类名,名字和描述符都是采用符号引用


还存在一些底层调用的方法如StringBuilder进行拼接调用的方法:

还包括构造方法,信息如下:

访问标志

  • 类访问标志


ACC_SUPER+ACC_PUBLIC ==>0x0020+0x0001=0x0021

  • 字段访问标志


ACC_PRIVATE+ACC_FINAL==>0x0002+0x0010=0x0012

  • 方法访问标志


ACC_PUBLIC+ACC_STATIC===>0x0001+0x0008=0x0009

类/父类索引与接口集合

  • 类/父类索引


由于ConstantPoolSample没有使用关键字extends继承其他类,所以默认父类为Object

  • 接口集合

ConstantPoolSample没有使用接口所以接口数为0

这里编写二个接口A,B并且让ConstantPoolSample去实现二个接口

然后再进行编译再次查看一般信息,此时接口数就变为2:

可以在接口中进行查看:

字段/方法/属性表

  • 字段表 - 描述接口或类中声明的字段(类变量 - static修饰和实例变量)

  • 方法表 - 描述接口或类的实例方法与静态方法

  • 属性表 - 为类/字段/方法提供更为详细的辅助信息

(3) 字节码指令简介

• 字节码指令是包含在字节码的指令
• 字节码指令将源码编译时由编译器生成保存在Method描述中
• 字节码与平台无关,运行时JVM读取后翻译各平台底层指令
• 字节码指令总数不超过256个

字节码指令格式:
字节码指令 [参数列表]

例如:

  • 执行StringBuilder对象的append方法
 invokevirtual #8 //cp_info#8 : java/lang/StringBuilder.append
  • 实例化新的StringBuilder对象
new #6 //cp_info#6 : 

IDEA JClassLib插件中查看字节码指令:

(4) 字节码指令分类概述


加载与存储指令中前面的字母分别表示数据类型( 挺重要 ):

字节码指令图:

2.类加载执行过程

• 类加载子系统负责从文件或者网络加载Class字节流
• 类加载子系统会读取字节码中的信息,运行时存储到JVM内存
• 任何Class要被类加载子系统加载,都要符合JVM字节码规范

类加载过程如下:

(1) 加载阶段

• 读取字节码二进制流
• 解析字节码二进制流的静态数据转换为运行时JVM方法区数据
• 生成类的java.lang.Class对象,放入堆中,作为方法区的访问入口
• 在加载类过程中,必然会触发父类加载

示意图:

字节码的常见来源
• 编译后本地.class文件
• 网络传输获取二进制流
• Jar/War包中解压后读取
• 动态运行生成,JDK动态代理/CGLib

Class实例何时被创建

  • new 实例化
    A a= new A()

  • 反射
    Class clzA = Class.forName(“com.itlaoqi.A”)

  • 子类加载时父类同时加载

  • JVM启动时,包含main方法的主类

  • 1.7的动态类型语言支持
    https://www.infoq.cn/article/jdk-dynamically-typed-language/

(2) 连接Linking阶段

• 验证Verify:确保字节码符合虚拟机要求
• 准备Prepare: 为字段赋予初始值
• 解析Resolve: 符号引用转换为直接引用

验证阶段(针对字节码二进制流进行处理):

准备阶段:
为类变量static赋予初始值
例子:
数据类型默认初始值:

解析阶段:
• 将字节码符号引用转换为直接引用(包括类解析,字段解析,方法解析,接口解析)
• 说人话:将字节码的静态字面关联转换JVM内存中的动态指针关联

(3) 初始化Initialization阶段

• 初始化阶段是执行类构造器方法 () 的过程
() 方法用于完成类的初始化操作
() 方法并不需要显式声明,由Java编译器自动生成

备注:加载/验证/准备/解析是由虚拟机主导与代码无关;初始化则是通过代码生成clint,完成类初始化过程。

初始化知识点:
• 初始化阶段对类(静态)变量赋值与执行 static 代码块
• 子类初始化过程会优先执行父类 ()
• 没有类变量及 static{} 代码块就不会产生 ()
• -XX:+TraceClassLoading 查看类加载过程
() 方法默认会增加同步锁,确保 () 只执行一次

知识点1:初始化阶段对类(静态)变量赋值与执行 static 代码块
代码详情:

看看加载阶段的指令,确实对静态变量进行赋值了:

知识点2:子类初始化过程会优先执行父类 ()
运行效果:

知识点3:没有类变量及 static{} 代码块就不会产生 ()


知识点4: -XX:+TraceClassLoading 查看类加载过程


点击ok,然后运行ClassInitSample项目:

知识点5: () 方法默认会增加同步锁,确保 () 只执行一次

(4) 类加载器种类

( 加载器关系为上下级,而非继承 )

  • 启动类加载器


什么是沙箱机制:

上图来源于:https://blog.csdn.net/weixin_41490593/article/details/99412315

代码实例:

package com.itlaoqi.classloader;

import sun.security.ec.SunEC;


public class ClassLoaderSample {
    public static void main(String[] args) {
        //启动类加载器,因为启动类加载器使用C语言编写,没有被JVM管理,所以启动类加载器返回null
        ClassLoader bootstrapClassLoader = Object.class.getClassLoader();
        System.out.println(bootstrapClassLoader);//null

    }
}

运行效果( 启动类加载器,因为启动类加载器使用C语言编写,没有被JVM管理,所以启动类加载器返回null ):

  • 扩展类加载器


代码实例:

package com.itlaoqi.classloader;

import sun.security.ec.SunEC;


public class ClassLoaderSample {
    public static void main(String[] args) {
        //扩展类加载器
        ClassLoader extClassLoader = SunEC.class.getClassLoader();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader

    }
}

SunEC类就在…/jre1.8/lib/ext文件下:

运行效果:

  • 应用程序(系统)类加载器


代码实例:

package com.itlaoqi.classloader;

import sun.security.ec.SunEC;


public class ClassLoaderSample {
    public static void main(String[] args) {
        //对于用户自定义类来说:默认使用应用程序类加载器进行加载
        ClassLoader appClassLoader = ClassLoaderSample.class.getClassLoader();
        System.out.println(appClassLoader);//sun.misc.Launcher$AppClassLoader



    }
}

运行效果:

  • 自定义加载器

面试题:Class实例在JVM是唯一的吗?

自定义ClassLoader三要素:

  1. 继承自ClassLoader,重写findClass()
  2. 获取字节码二进制流
  3. defineClass加载生成Class实例

自定义加载器1代码:

package com.itlaoqi.classloader.custom;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;


public class MyClassLoader1 extends ClassLoader {
    private final String CLASS_PATH = "c://ClassSample";

    protected Class findClass(String name) {
        try {
            //获取字节码二进制流
            FileInputStream in = new FileInputStream(this.CLASS_PATH);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len = -1;
            while ((len = in.read(buf)) != -1) {
                baos.write(buf, 0, len);
            }
            in.close();
            byte[] classBytes = baos.toByteArray();
            //加载Class字节码
            return defineClass(classBytes, 0, classBytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

ClassSample位于C盘下:

自定义加载器2代码:

package com.itlaoqi.classloader.custom;


import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;



public class MyClassLoader2 extends ClassLoader {
    private final String CLASS_PATH = "d://ClassSample" ;
    protected Class findClass(String name) {
        try {
            FileInputStream in = new FileInputStream(this.CLASS_PATH) ;
            ByteArrayOutputStream baos = new ByteArrayOutputStream() ;
            byte[] buf = new byte[1024] ;
            int len = -1 ;
            while((len = in.read(buf)) != -1){
                baos.write(buf , 0 , len);
            }
            in.close();
            byte[] classBytes = baos.toByteArray();
            return defineClass( classBytes , 0 , classBytes.length) ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }
}

ClassSample位于D盘:

二个不同的加载器加载相同实例:
代码如下:

package com.itlaoqi.classloader.custom;

public class Application {


    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader c1 = new MyClassLoader1() ;
        //利用自定义加载器1加载对象
        //调用ClassLoader.loadClass()加载字节码会自动调用findClass方法
        Class clz1 = c1.loadClass("ClassSample");
        System.out.println(clz1.getClassLoader() + "|hashcode:" + clz1.hashCode());

        ClassLoader c2 = new MyClassLoader2() ;
        //利用自定义加载器1加载对象
        Class clz2 = c2.loadClass("ClassSample");
        System.out.println(clz2.getClassLoader() + "|hashcode:" + clz2.hashCode());
        Class clz3 = c2.loadClass("ClassSample");
        System.out.println(clz2.getClassLoader() + "|hashcode:" + clz3.hashCode());

        System.out.println("结论:同一个Class被不同的类加载器加载后在JVM中产生的类对象是不同的");
        System.out.println("推导:在同一个类加载器作用范围内Class实例加载时才会保持唯一性");

    }
}


运行效果( 同一个Class被不同的类加载器加载后在JVM中产生的类对象是不同的,在同一个类加载器作用范围内Class实例加载时才会保持唯一性 ):

(5) 双亲委派模型

加载类时加载器逐级将加载任务向上委派至引导类加载器.然后逐级向下尝试加载,直至加载完成.

优点:
• 双亲委派机制保护了类不会被重复加载(子类加载,优先加载父类,Object类是所有类的父类,避免了每次都需要加载object)
• 加载机制提供沙箱机制,禁止用户污染java开头核心包

实例验证( 加载机制提供沙箱机制,禁止用户污染java开头核心包 ):


运行效果:

3.运行时数据区的组成


线程私有

  • 程序计数器 - 存储线程执行位置
  • 虚拟机栈 - 存储Java方法调用与执行过程的数据
  • 本地方法栈 - 存储本地方法的执行数据

线程共有

  • 堆 - 主要存储对象
  • 方法区 - 存储类/方法/字段等定义(元)数据
  • 运行时常量区 - 保存常量static数据
(1) 程序计数器


为什么会有程序计数器?

使用程序计数器可以记录指令执行位置,比如下图中单线程通过划分多个时间片实现多线程工作,上一个线程执行指令到哪了,就可以通过计数器知道,然后执行下一个地址的指令。


关于执行native方法计数器为空?

使用native方法,我们知道native方法都是通过和操作系统进行绑定的,如windows下的库大多数调用C/C++库方法,C/C++方法都是运行在C语言的运行内存中,和Java运行内存并不是一回事,所以计数器为空。

(2) 虚拟机栈

什么是栈?

栈是一种数据结构,是一种连续紧密存储结构,出入顺序先进后出,二种操作入栈和出栈。

Data1和Data2数据块入栈

Data1和Data2入栈结果如下:

Data2出栈Data3入栈:

Data2出栈Data3入栈效果如下:

什么是虚拟机栈?

① 虚拟机栈(栈)保存方法的调用过程
② 栈说明了程序运行中的瞬时状态
③ 栈是线程私有的,生命周期与线程相同
④ 每次方法的调用,都会产生对应栈帧


实例练习:

程序运行调用main()方法,栈帧main入栈,处于栈底,执行main()方法的代码,调用method1(),栈帧method1入栈,执行method1()代码,调用method2(),栈帧method2入栈执行method2()代码,没有方法执行了开始出栈( 按照先进后出顺序出栈 ),栈帧method2出站,然后栈帧method1出栈,最后栈帧main出栈,执行完毕,线程销毁,整个过程就是栈的生命周期。


虚拟机栈的特点

① 线程私有的
② 不会被垃圾回收
③ 栈的生命周期和线程相同
④ 栈深度有限制

设置虚拟机栈的空间

① Java1.5后默认每个栈空间为1MB,之前版本256KM
② Java启动参数: -Xss 数值[k|m|g] 设置栈的空间
③ 栈分配的内存决定了栈的最大深度

虚拟机栈有两种空间设置

① 固定长度(推荐): 达到上限,StackOverflowError
② 动态扩展: 可用内存不足,OutOfMemoryError( 不推荐使用 )

实例练习设置固定长度:
执行如下代码:

package com.itlaoqi.runtime.stack;


public class StackOverflowSample {
    private static long count = 0;
    public static void main(String[] args) {
        test();
    }

    public static void test(){
        count++;
        int a,b,c,d,e,f,g,h,i,j,k=0;
        System.out.println("正在第" + count + "次调用方法");
        test();
    }
}

运行效果:

设置栈的固定长度:


输入数值: -Xss10m( 格式:-Xss 数值[k|m|g] k:KB,m:MB,g:GB )

再次运行(执行方法次数明显增加了):

局部变量也会影响栈的容量(将上面代码中的局部变量注释掉,次数明显有增加):

栈的容量要么采用默认1MB或者使用512KB

备注:动态扩展会消耗内存容量,如果调用死循环,会使一些原本可以运行的程序造成崩溃。

虚拟机栈的栈帧

栈帧由 局部变量表 , 操作栈 , 动态链接 , 返回地址 四部分组成。
① 局部变量表:局部变量
② 操作数栈:保存中间计算的临时结果
③ 动态链接:将符号引用转为直接引用
④ 返回地址:存放调用方法的程序计数器值

栈帧的组成:


局部变量表

局部变量表按定义顺序存储两块内容:存储方法参数和存储方法内的局部变量

局部变量表特点

① 线程私有,不允许跨线程访问,随方法调用创建,方法退出销毁
② 编译期间长度已确定,局部变量元数据存储在字节码中
③ 局部变量表是栈帧最主要的存储空间,决定了栈的深度

实例练习:
测试代码如下:

public class LocalVariableTableSample {

    public LocalVariableTableSample(){
        this.slotSample1();
    }

    public static String staticMethod(String name,int offset){
        String ret = "hello " + name;
        int count = 100 + offset;
        return ret;
    }

    public String instanceMethod(String name,int offset){
        String ret = "hello " + name;
        int count = 100 + offset;
        return ret;
    }

    public void slotSample1() {
        int a = 1;
        float b = 0f;
        boolean c = true;
        long d = 100;
        double e = 100;
        String f = "";
    }

    public void slotSample2() {
        int a = 1;
        if(1==1){
            int b = 0;
            b = a + 1;
        }
        int c = 100;
    }

    public static void main(String[] args) {
        LocalVariableTableSample.staticMethod("Lily" , 100);
        LocalVariableTableSample instance = new LocalVariableTableSample();
        instance.instanceMethod("Andy" , 200);
    }
}

观察main栈帧(一个方法的调用对应一个栈帧):

上图中的LineNumberTable叫做行号表,说明了字节码指令和源码行号的对应关系


上图中的LocalVariableTable在编译期间就保存在字节码中,长度固定,方法调用时随栈帧创建并载入,LocalVariableTable中单个变量的序号(index)叫做Slot( 存储局部变量的单位为Slot(变量槽) )

上图中的局部变量对应源码中如下:

局部变量的作用域由起始PC和长度决定,通过字节码指令查看

比如name,offset,起始PC为0长度为27(0-26),对应字节码如下:

又比如ret变量,起始PC为20长度为7(20-26),如下:

LocalVariableTable在构造方法与实例方法中,0号槽位(Slot)默认为this,指向当前类实例


static方法没有this关键字,所以在static方法中不能够使用this关键字


槽(Slot)按类型区分,32位以内类型(int/float/char/…/引用类型)占用1个Slot,64位类型(long/double)占用2个Slot:

上图对应下图:

当槽位有空余时,后产生的局部变量会重用之前的槽位,此特性称为"槽复用"

实例:
字节码局部变量表

代码如下:

start PC=0从字节码局部变量表可知,该solotSample2是一个实例方法,0卡槽为this

Start PC =2,从字节码局部变量表可知,卡槽1为a

start PC=4,从字节码局部变量表可知,卡槽2为b

由于length=4所以当start PC=7的时候b字段销毁

继续执行start PC=11时,卡槽2(index=2)为c,(这里就是因为卡槽复用所以卡槽2=c),然后执行完毕

操作数栈

字节码指令在执行过程中的中间计算过程存储在操作数栈。

通过字节码指令分析执行过程掌握操作数栈知识:
源代码:

public class OperandStackSample {
    public int compute1(){
        short i = 10;
        int j = 18;
        int k = i + j;
        return k;
    }
}

字节码指令表:

compute1()方法是一个实例方法,所以局部变量表中索引0为this


执行指令 0 bipush 10 将整型进行操作数栈入栈

2 istore_1 栈顶 出栈 保存至局部变量表1号槽

3 bipush 18和5 istore_2指令和上面操作一致,不做解释:

6 iload_1 将 1 号槽局部变量压入栈顶


7 iload_2:

8 iadd:iadd将栈顶与第二位做加运算存至栈顶

9 istore_3和10 iload_3:

11 ireturn :ireturn将栈顶数据出栈进行返回

控制台输出28


64位数据类型占两个栈深度( 在局部变量表中64位数据类型占二个Slot,在操作数栈中64位数据类型占二个栈深度 ):

下图栈深度为4

动态链接

将保存在字节码文件中 符号引用 转为运行时的内存 直接引用 。


符号引用就是通过字面量进行描述该数据存放的位置,该数据(元数据,存放在运行时常量区中),而栈帧中的动态链接就是对应的内存指针,就是对元数据的直接引用。

方法返回地址

保存该方法调用者的程序计数器值,用于方法正常执行后后续执行。

执行下图s1方法,当进入方法体时执行第一条打印语句,执行完之后遇见了this.s2();进入到s2()方法中,当s2()方法被执行完后,再次回到s1中执行剩下的代码,s2()方法执行完后是怎么知道s1中剩下代码从什么时候开始执行的呢?,其实就是通过方法返回地址确认后续执行。

(3) 本地方法栈

native本地方法:

① 一个native方法就是一个Java调用非Java代码的接口
② 在定义一个native方法时,并不提供实现体
③ native的可以调用其他语言接口实现对操作系统更底层的操作

本地方法栈图如下:

本地方法栈特点:

① 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定
② Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一

(4) 堆Heap


什么是堆Heap?

① 堆Heap是JVM最核心的内存区域,存放运行时实例化的对象实例
② 堆在JVM启动时就被创建,空间也被分配.是JVM内存的主要占用区域
③ 堆是线程共享的,堆内存在物理上可以是分散的,在逻辑上是连续的(堆中包含线程私有的缓冲区(TLAB)用于提高JVM的并发处理效率)

引用类型与堆的关系

① 引用类型本质是指针,指向存放在堆中的对象实例地址
② 堆是垃圾回收(GC)的重点区域,方法结束后堆对象不会被立即清除,在GC时才会被清理

执行如下代码实际上就是在堆上开辟了一个空间,存放B类对象,然后在变量表中b变量(栈帧)指向堆中的B类对象。

public void test(){
	B b = new B();
}

堆结构

新生代:是用来存放新生的对象,该区域对象会被频繁GC
老年代:新生代保存的稳定对象放入老年代,老年代不会频繁执行GC
元空间:内存的永久保存区域,主要存放Class元数据,几乎不会GC

堆( JDK1.8 )的结构如下图所示:

堆空间
默认堆空间的大小

初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存大小 / 4

执行如下代码可以在控制台输出初始内存和最大内存信息:

public class HeapSpaceSample {
    public static void main(String[] args) {
        //返回Java虚拟机中的堆内存总量
        long usedMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        System.out.println("堆当前占用 : " + usedMemory + "M");
        System.out.println("堆最大内存 : " + maxMemory + "M");
        
    }
}

运行效果:

我的电脑内存可用为7.85GB

通过上面的公式,算出预期的初始内存大小为125.6MB,最大内存大小为2009.6MB。

备注: 操作系统自身会占用一些空间,JVM分配内存的时候也会拿出来一些作为保留,所以实际分配的内存比我们预期的内存要少一些

年轻代与老年代的占用比例

① 年轻代固定占用1/3
② 老年代固定占用2/3

控制台打印GC详细信息:设置 -XX:+PrintGCDetails 参数

运行效果:

设置堆空间大小的参数

设置参数(m:MB,g:GB):
① -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
② -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
设置建议:
① 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
② Java整个堆大小设置建议,Xmx 和 Xms设置为老年代FullGC后存活对象的3-4倍

设置初始内存大小上限和最大内存上限为1024MB

运行效果:

VisualVM安装使用
VisualVM安装使用教程:VisualVM安装,插件安装,各个面板信息讲解

对象分配一般过程

① 绝大多数刚创建的对象会存入新生代的Eden伊甸园区
② Eden与S0/S1的内存分配比例是8:1:1


新生代进入老年代过程

  • 一般情况

当Eden第1次满MinorGC对Eden/S0/S1进行GC:当实例化对象的时候(有new操作时),优先将对象放入Eden区,当Eden区的对象满时,先将还存在引用的对象通过复制交换算法进入到S0中设置age=1,最后对Eden和S1中未引用对象进行GC操作( Eden满时,针对年轻代进行的垃圾回收称为YGC/MinorGC )。

GC之后:

Eden第2次满,对Eden/S0/S1进行MinorGC:Eden第2次满时,Eden将有引用关系的对象转移至S1区,设置对象age=1,原S0(From区)对象转移至S1(To区),且age+1,对Eden和S0进行GC操作。

GC之后:

Eden第3次满,对Eden/S0/S1进行MinorGC:Eden第3次满时,Eden将有引用关系的对象转移至S0区,设置对象age=1,原S1(From区)对象转移至S0(To区),且age+1,对Eden和S1进行GC操作。

GC之后:

n次Minor GC后当对象age超过15(阈值)对象晋升(Promotion)至老年代:

  • 特殊情况(GC整个流程图)

Minor GC / Full GC / Major GC分别是什么?

( 任何GC行为都会触发STW(Stop The World全局停顿) )

① Minor GC(YGC) 针对年轻代进行回收,执行效率高。
② Full GC,全堆回收,针对年轻代/老年代/方法区进行全面收集,执行效率低下,会导致系统长时间停滞,减少Full GC是JVM优化的重点。
③ Major GC,针对老年代回收,目前只有CMS收集器才存在Major GC。

为什么要对象分代(年轻代,老年代)?

① 堆中绝大多数对象”朝生夕灭” 出于性能考虑,
② 按照年龄age按区域进行划分,缩小内存扫描的范围
③ 针对不同特性的对象,采用不同的垃圾收集,最大程度提高执行效率

代码实例练习,GC日志分析:

public class HeapObjectSample {
    
    public static void case1() {
        List list = new ArrayList();
        while (true) {
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.add(new byte[1024 * 50]);
        }
    }

    public static void main(String[] args) {
        case1();
    }
}

设置VM参数:-Xms60M -Xmx60M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails

  • -Xms60M -Xmx60M 堆内存总大小60MB
  • -Xmn10M 强制设置新生代10MB,剩余50MB分给老年代
  • -XX:SurvivorRatio=8 设置Eden与Survivor比例为8:1:1 ,即eden为8MB / S0与S1各1MB
  • -XX:+PrintGCDetails 打印详细GC日志


运行效果:

分析执行情况:
GC (Allocation Failure):

  • GC (Allocation Failure) : Allocation Failure说明触发GC的原因是Eden空间不足
  • 8182K->1016K(9216K):GC前年轻代空间 -> GC后年轻代空间(年轻代最大占用空间)
  • 8182K->6838K(60416K):GC前堆占用空间->GC后堆占用(堆最大占用空间)
  • 0.1999019 secs: 本次GC使用时间


为什么年轻代最大空间是9216K,不是10240(10mb)?
答:年轻代组成为 Eden(8mb)+ 2*(Survivor(1mb)) ,但两个S区只有一个有数据,所以在内存分配时只需分配1个即:8mb+1mb=9216k。

Full GC (Ergonomics) :

  • Full GC (Ergonomics): Ergonomics(GC收集器策略执行)

  • Full GC (Allocation Failure):堆空间分配失败,会抛出OOM


visualvmGC执行情况如下:

(5) 方法区

① 方法区是线程共享区域,是物理分散存储而逻辑为整体的内存区域
② 方法区保存的内容:类加载器信息/类信息(包含字段/方法)/常量/即时编译器编译后的代码缓存/静态变量(1.6版本前,之后存放在堆中)
③ 方法区是个概念,JVM对方法区如何实现做更多要求.

永久代与元空间的区别

规范说明方法区是堆的逻辑部分,在实现中与堆内存无关,称为”非堆Non-heap”。


设置元空间

运行时常量区

方法区的历史变化
方法区是观念,HotSpot独有的永久代是方法区的实现.1.8被淘汰

  • JDK6

  • JDK7
  • JDK8

new对象永远放在堆中
( new 对象的实例不管在JDK1.6之前还是1.7之后都是放在堆中 )

三. Grabage Collection(GC垃圾回收机制) (1) 对象的创建和销毁

创建对象的几种方式

  • 用new语句创建对象
  • 运用反射手段(调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。)
  • 调用对象的clone()方法。
  • 运用反序列化手段(调用java.io.ObjectInputStream对象的readObject()方法。)

对象销毁

什么条件下对象才是垃圾?
当对象没有任何引用的时候,就代表对象无用了.

引用链路断开,后续对象也会成被标记为垃圾

如何发现对象已经是垃圾?

  • 引用计数(Reference Count)算法

引用计数是指采用计数器说明引用对象个数,当计数器=0时则代表
是垃圾


引用计数无法解决循环引用!

  • 根搜索算法(Root Searching)

也叫”可达性分析算法”,从GCRoot触发,有引用的对象都是”不可回收的”,其他可
标记后再回收,是JVM默认算法。

(2) 三种垃圾回收算法
  • Mark-Sweep 标记清除算法
  • Coping 复制(交换)算法
  • Mark-Compact 标记压缩算法

Mark-Sweep 标记清除算法


Coping 复制(交换)算法

Mark-Compact 标记压缩算法

(3) GC垃圾收集器

可分为分代收集器和不分代收集器

分代收集器(根据年轻代和老年代进行分类)
Serial 收集器

Parallel Scavenge(PS)收集器

ParNew 收集器

SerialOld 收集器

Parallel Old收集器

Concurrent Mark Sweep(CMS)

设置GC收集器组合
GC收集器表如下:

查看当前GC收集器组合:
java -XX:+PrintCommandLineFlags -version

设置GC收集器组合(程序运行时app.jar就会使用的收集器为年轻代采用Serial,老年代采用SerialOld):
java -jar -XX:+UseSerialGC app.jar

不分代收集器
G1收集器

年轻代回收Minor GC

新生代+老年代回收 Mixed GC

初始标记( 标记GCRoot与第一层对象 )

并发标记( 并发完成GCRoot引用链会产生并发问题 )

最终标记( 确认所有垃圾 )

标记-压缩算法回收STW

G1开辟一块最多5%堆空间的内存用于标记压缩的数据交换,过程产生STW, STW 200ms内最多回收10%垃圾最多的区域,回收后检查老年代是否低于45%, 未达标继续再来一遍,最多8次,8次未达标Serial Old(Full GC)。

低延迟收集器(STW小于10ms的收集器)
低延迟收集器:

  • ZGC
  • Shenandoah

各种拦击收集器并发程度对比:

ZGC

Shenandoah


Epsilon收集器( 不收集任何内存,在不需要内存回收的场景中使用 )

(4) JVM优化

JVM调优建议
① 大多数情况JVM生产环境考虑调整下面三方面:

  • 最大堆和最小堆大小
  • GC收集器的选择
  • 新生代(年轻代)大小

② 在没有全面监控、收集性能数据之前,调优就是扯淡
③ 99%的情况是你的代码出了问题,而不是JVM参数不对

JVM选项规则

① java -version 标准选项,任何版本JVM/任何平台都可以使用
② java -Xms10m 非标准选项,部分版本识别 java
③ -XX:+PrintGCDetails 不稳定参数,不同JVM有差异,随时可能会被移除,+代表开启/-代表关闭

JVM优化选项

① 1.8+优先使用G1收集器,摆脱各种选项烦恼
② -Xms与-Xmx设置相同,减少内存交换
③ 评估Xmx方法: 第一次起始设置大一点,跟踪监控日志,调整为堆峰值*2~3即可
④ 最多300毫秒STW时间,200~500区间,增大可减少GC次数,提高吞吐
⑤ -Xss128k/256k 虚拟机栈空间一般128K就够用了. 超过256k考虑优化,不建议超过 256k
⑥ G1一般不设置新生代的大小,G1新生代是动态调整的

参数设置推荐如下:
java -jar -XX:+UseG1GC -Xms2G -Xmx2G -Xss256k -XX:MaxGCPauseMillis=300 -Xloggc:/logs/gc.log -XX:+PrintGCTimeStamps -XX:+PrintGCDetails test.jar

(5) JVM监控命令

(6) 调优软件Arthas

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

Arthas用户文档地址: https://arthas.aliyun.com/doc/

文档学习目录情况:

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/849928.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号