JVM总结一

JVM基础知识

Java的跨平台性

注意:我们提到Java的跨平台性,就会想到JVM,但是能跨平台的是 Java 程序,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM
我们编写的 Java 源码,编译后会生成一种 .class 文件,称为字节码文件。Java 虚拟机(JVM)就是负责将字节码文件翻译成特定平台下的机器码然后运行,也就是说,只要在不同平台上安装对应的 JVM,就可以运行字节码文件,即运行我们编写的 Java 程序。
而这个过程,我们编写的Java 程序没有做任何改变,仅仅是通过 JVM 这一 “中间层” ,就能在不同平台上运行,真正实现了 “一次编译,到处运行” 的目的。
在这里插入图片描述

jvm概念

JVM,即 Java Virtual Machine,Java 虚拟机
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

JRE/JDK/JVM是什么关系
  • JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
  • JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
  • JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
    在这里插入图片描述
    在这里插入图片描述
    JDK=JRE+多种Java开发工具
    JRE=JVM+各种类库

    这三者的关系是一层层的嵌套关系。JDK>JRE>JVM
JVM的声明周期
启动和消亡

JVM负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。

JVM运行起点

Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序。而这个main()方法必须是共有的(public)、静态的(static)、返回值为void,并且接受一个字符串数组作为参数。任何拥有这样一个main()方法的都可以作为Java程序运行的起点。

JVM两种守护线程

守护线程和非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。
但Java程序也可以把创建的线程标记为守护线程。而Java程序中的初始线程——main()的线程是非守护线程。
只要还有任何非守护线程在运行,那么这个Java程序也在继续运行。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。
假若安全管理器允许,程序本身也能够通过调用Runtime类或者System类的exit()方法来退出。

JVM内存模型

Java内存布局介绍

Java内存布局共有 5 大块,它们分别是堆区(Java Heap)、虚拟机栈(Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、元空间(Meta Spaces)、程序计数器(Program Counter Register)。
在这里插入图片描述

程序计数器

特点:
占用的 JVM 内存空间较小
每个线程生命周期内独享自己的程序计数器(内部存放的是字节码指令的地址引用)
不会发生 OOM

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

作用:
1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、异常处理。
2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪个位置。

注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,
它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

特点:

  • 内部结构是栈帧,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法返回地址等信息
  • 某方法在调用另一个方法是通过动态链接在常量池中查询方法的引用,进而完成方法调用
  • 某方法在调用另一个方法的过程,即是一个栈帧在虚拟机中的入栈到出栈的过程
  • 虚拟机中的方法入栈的顺序和方法的调用顺序是一致的

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。
在这里插入图片描述
局部变量表
主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
详细的虚拟机栈的执行:
在这里插入图片描述
程序计数器反映了程序运行的位置,当方法A入栈之后,局部变量表记录了保存了参数,然后将参数加载到操作数栈中去执行,当进入一个方法b时,方法a调用方法b,通过动态链接,在常量池中找到方法b,然后方法返回,它还是基于栈这种数据结构来实现,保留栈的特点。
Java 虚拟机栈会出现两种异常:
StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError:
    若Java虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
  • OutOfMemoryError:
    若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
对于 JVM 中虚拟机栈参数的设置
-Xss :用于设置栈的大小,栈的大小决定了方法调用的深度。

# 设置线程栈大小为 512k(以字节为单位)
-Xss512k
本地方法栈

和虚拟机栈非常相似,但是还是有区别:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,**用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。**方法执行完毕后相应的栈帧也会出栈并释放内存空间,
也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

特点:

  • 存放 Java 对象和数组
  • 虚拟机中存储空间比较大的区域
  • 可能出现OutOfMemoryError异常区域
  • 该区域是 GC 的主要区域,堆区由年轻代和老年代组成,年轻代又分为 Eden 区、S0区(from survivor)、S1 区(to survivor);新生代对应 Minor GC(Young GC),老年代对应 Full GC(Old GC)

Java 虚拟机所管理的内存中最大的一块, Java 堆是所有线程共享的一块内存区域,在**虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:
在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在这里插入图片描述
对于 JVM 中堆区参数的设置

# 设置堆区的初始大小
-Xms1024m
# 设置堆区的存储空间最大值,一般与堆区的初始大小相等
-Xmx1024m
# 设置年轻代堆的大小
-Xmn512m
# 设置如下参数,在出现OOM时进行堆转储
-XX:+HeapDumpOnOutOfMemoryError
# 设置以上设置时,需配置以下参数,堆转储文件输出的位置
-XX:HeapDumpPath=/usr/log/java_dump.hprof

永久代说明:

  • Jdk1.6及之前:常量池分配在永久代。
  • Jdk1.7:有,但已经逐步“去永久代” 。
  • Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
  • 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域
    (永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)
方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

HotSpot 虚拟机中方法区也常被称为 “永久代”本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
运行时常量池
**运行时常量池是方法区的一部分。**Class 文件中除了有类的版本、字段、方法、接口等描述信息外,
还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,
当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

方法区与永久代
  • 方法区被所有线程共享。采用永久代的方式实现了方法区。
  • jdk 8 以前(不包括 jdk8)存在永久代(Perm区),jdk 8 以后(包括 jdk 8)移除了永久代。如下图所示。
  • 在这里插入图片描述
    方法区在不同 JDK 版本的变化
    在这里插入图片描述
    jdk1.7以后运行常量池就划分好了old区,而不存在prem区了。
    方法区和元空间的区别:
    在这里插入图片描述

jdk8以后除去了永久代,而是由元空间单独控制
对于 JVM 中永久代或元空间参数的设置

# jdk1.7 设置永久代内存初始大小
-XX:PermSize=512m
# jdk1.7 设置永久代内存最大值
-XX:MaxPermSize=512m
# jdk1.8 设置元空间内存初始大小
-XX:MetaspaceSize=1024m
# jdk1.8 设置元空间内存最大值
-XX:MaxMetaspaceSize=1024m
new Object()过程

如何创建-> 在哪个地方分配内存->如何分配->对象如何定位->对象的内存布局->对象如何回收

  1. 对象的创建
    先在虚拟机栈创建栈帧,栈帧内创建对象的引用,在方法区进行类的加载,然后去 Java 堆区进行分配内存并内存初始化,再回到栈帧中初始化对象的数据,完成对象的创建。
    在这里插入图片描述
    2)Java堆内存分配过程
    在堆空间中找到空闲区域进行对象分配
    3)对象的访问定位
  • 句柄访问
  • 在这里插入图片描述
    句柄池是 Java 堆分配用于存放对象指针的内存空间。
    优点:在垃圾回收的时候对象要经常转移,这时候只需改变句柄中指向对象实例数据的指针即可(不用修改 reference)。
  • 直接访问
  • 在这里插入图片描述
    优点: 相对于句柄访问定位的方式,减少了一次指针定位的开销(也减少了句柄池的存储空间),HotSpot JVM 实现采用的是直接访问的方式进行对象访问定位。
    4)对象的内存布局
    对象的组成:对象头(对象自身运行时数据和类型指针)、实例数据和对齐填充。
    在这里插入图片描述
    5)垃圾回收(四大引用)

Java类加载机制

JVM的工作过程

在这里插入图片描述
类加载的子系统:保证加载正确的类到JVM中
运行时数据区:类存储及对象及对象运行时变量等存储区域
执行引擎:将java的字节码交给执行引擎,根据字节码逐个执行

类加载的子系统

java的动态类的装载由装载子系统来实现的,进行加载,连接,在运行第一次引用的类时,进行初始化文件
加载过程:功能是类的加载,共有三种类加载器:Bootstrap ClassLoader,Extension ClassLoader和Application ClassLoader
链接:
验证:验证字节码的正确性
准备:主要是针对静态变量,内存分配并给定默认值
解析:将所有符号引用替换为方法区域的原始引用
初始化:
静态变量被赋值,静态代码块会被执行

运行时数据区

方法区: 类级别数据,静态变量存储在这里,线程共享的
堆区:对象及其实例变量和数据存储位置,线程共享
虚拟机栈: 线程私有,java程序执行过程中的调用关系,局部信息都会以栈帧形式存储在该位置,线程私有
程序计数器:记录程序下一个执行的位置,线程私有
本地方法栈: 保存和OS交互的native方法信息,线程私有

执行引擎

执行存在的字节码信息,提供执行就是来解析字节码,优化字节码,生成中间代码,提供一些组件:垃圾回收、和本地native交互的本地库等等…

类加载机制

完成字节码.clas文件的加载

类加载的时机
  • 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时,如果类型没有遇到初始化,则需要先触发初始化阶段,能够生成这四条指令的典型java场景:
    1)new关键字实例化对象
    2)读取或者设置一个类型的静态字段,被final修饰,已在编译期把结果放入常量池的静态字段除外
    3)调用一个类型的静态方法
  • 进行反射调用
  • 初始化类发现父类还没初始化,需要先触发父类的初始化
  • 虚拟机启动时,用户需要指定一个要执行的主类
  • jdk7 加入动态语言支持时…
  • jdk8 新加入的默认方法,被default修饰的接口方法时,如果这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。
    小编大意了 整理了两次,找不同,嘻嘻…
主动初始化的方式

1、创建对象实例,new对象的时候,会对类进行初始化,前提是类没有被初始化
2、调用类的静态属性或者是类的静态方法
3、通过class文件反射创建对象
4、初始化一个类的子类,使用子类的时候先初始化父类
5、java虚拟机启动时被标记为启动类的类,如:main方法所在的类
注:java类的加载是动态的,并不会一次性加载所有的类到JVM中才执行,保证程序能够正常运行的基础类,其他的类则在需要时才会加载,节约内存开销

不会进行分类加载的情况
1、在同一个类加载器下只能初始化类一次,已经初始化的就不会在进行初始化
2、在编译的时候就能确定下来的静态变量,不会对类进行初始化,比如final修饰的静态变量

类加载器

在这里插入图片描述
各个加载器的工作责任:
Bootstrap ClassLoader:
负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
Extension ClassLoader:
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
App ClassLoader:
负责加载classpath中指定的jar包及目录中class

双亲委派模型

在这里插入图片描述
双亲委派模型的工作过程:
1、当前的类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则返回已经加载的类型
2、如果没有找到,就去委托父类加载器去加载,父类加载器采用相同的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托其父类去加载,直到委托到启动类加载器为止,如果父类加载为空,
3、如果启动加载器失败,就是使用扩展类加载器来尝试加载,继续失败则会使用Application ClassLoader加载器类加载,继续失败就会抛出一个异常:ClassNotFoundException

使用双亲委派的好处:
1、安全性,避免用户自己编写的类动态替换java的核心类
2、避免类的重复加载,因为JVM判定两个类是否是同一个类,不仅仅根据类名是否相同进行判定,还需要判断加载该类的类加载器是否是同一个类加载器,相同的class文件被不同的类加载器加载得到的结果就是两个不同的类。

类加载详细过程

加载: 查找并加载指定的类的字节码文件
连接: 连接包含三块内容:验证、准备、解析
验证: 验证加载的class的文件格式、元数据、字节码、符号引用验证
准备: 为类的静态的变量分配内存,并初始化默认值 //给变量num分配内存,给定默认值是0
解析阶段:把类中的符号引用转化为直接引用
初始化:为类的静态变量赋予正确的初始值并且执行静态代码块 //num值给定位5
在这里插入图片描述
单例模式

//非线程安全的
public class Single2 {

    private static Single2 s = null;
    private Single2() { }

    public static Single2 getInstance() {
        //两个线程同时进行if (s == null)判断,则都会进入if条件吗,就会创建对个实例
        if (s == null)
            s = new Single2();
        return s;
    }
}

//不是线程安全的?
//通过getInstance()来使用当前的类
Single2.getInstance();
调用该方式时,通过Single2类来调用静态方法,该类第一次使用,进行类加载的过程,在整个加载过程中当前类实例的静态变量s还是为null
,并完成了类的加载过程。在使用阶段,有可能同时两个线程调用了在调用getInstance方式,可能两个线程同时判定s == null 为真,每个线程
都会new Single2();意味着当前JVM中会存在两个Single2实例,所以存在线程不安全的风险



//线程安全的
public class Single4 {
    private final static Single4 singleton4;
    private Single4() { }

   //静态代码块
    static {
        singleton4 = new Single4();
    }

    public static Single4 getInstance() {
        //使用之前将singleton4属性通过静态代码块实现
        return singleton4;
    }
}

//
Single4.getInstance();Single4.getInstance()调用时,如果该类没有被加载,就需要进行加载操作,在加载的过程中,在链接阶段中准备过程中对当Single4
中静态变量singleton4分配内存,在初始化阶段来执行静态代码块,静态代码块逻辑是来完成对singleton4变量实例化,整个阶段在加载过程完成的
在使用阶段不管多个线程也好,都是在使用阶段来调用getInstance,此时对象已经被实例化出来了

所有当前是线程安全的,线程安全的机制是由类加载机制来保证的:同一个类只会被jvm加载一次,而当前类的静态变量和静态代码块都是在加载过程中完成的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值