Java虚拟机概述


文件创建时间

Linux 的文件系统保存有三个时间戳,利用 stat 指令查看文件信息可以获取。他们分别是 ATime、MTime 和 CTime

  • ATime 文件的最近访问时间,只要读取文件,ATime 就会更新,对应的是 stat 命令获取的 Access 的值。
  • MTime ——文件的内容最近修改的时间,当文件进行被写的时候,MTime 就会更新,对应的是 stat 命令获取的 Modify 的值。
  • CTime 文件属性最近修改的时间,当文件的目录被修改,或者文件的所有者,权限等被修改时,CTime 也就会更新,对应的是 stat 命令获取的 Change 的值。
$ stat 1.txt 
  File: ‘1.txt’
  Size: 5             Blocks: 8          IO Block: 4096   regular file
Device: fc01h/64513d    Inode: 659003      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/hqinglau)   Gid: ( 1000/hqinglau)
Access: 2021-09-16 11:58:36.807042682 +0800
Modify: 2021-09-16 11:58:35.068999449 +0800
Change: 2021-09-16 11:58:35.068999449 +0800
 Birth: -
 
$ echo Lei >> 1.txt 
$ stat 1.txt 
  File: ‘1.txt’
  Size: 9             Blocks: 8          IO Block: 4096   regular file
Device: fc01h/64513d    Inode: 659003      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/hqinglau)   Gid: ( 1000/hqinglau)
Access: 2021-09-16 11:58:56.807540187 +0800
Modify: 2021-09-16 11:58:56.228525784 +0800
Change: 2021-09-16 11:58:56.228525784 +0800
 Birth: -
$

但是没有文件创建时间,可以用 debugfs 查看 crtime

$ debugfs -R 'stat <5255117>' /dev/sda7
# <inode> 磁盘路径

HotSpot 虚拟机

SUN 的 JDK 版本从1.3.1开始运用 HotSpot 虚拟机, 2006年底开源,主要使用 C++ 实现,JNI 接口部分用 C 实现。

HotSpot 是较新的 Java 虚拟机,用来代替 JIT (Just in Time),可以大大提高 Java 运行的性能。

Java 原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。而 HotSpot 将常用的部分代码编译为本地 (原生,native) 代码,这样显着提高了性能。 HotSpot JVM 参数可以分为规则参数 (standard options) 和非规则参数 (non-standard options)。

规则参数相对稳定,在JDK未来的版本里不会有太大的改动。 非规则参数则有因升级JDK而改动的可能。

即时编译器的执行效率很高,为什么不将它全部提前编译好缓存起来呢?

  • 全部提前编译,首次启动响应速度慢,会有卡顿的感觉,因为编译需要大量时间。(主要原因)
  • 缓存代码,需要放在方法区,占用内存空间,容易溢出。
  • 翻译成为机器指令,则这部分缓存的 CodeCache 是不能够直接跨平台,因为不同环境的机器指令是不大一样的,只能每次运行前就全部编译。

HotSpot 名称来源主要是热点代码探测技术

  • 通过计数器找到最具有编译价值的代码,触发即时编译和栈上替换。
  • 编译器和解释器协同工作,可以在响应时间和最佳执行性能中取得平衡。解释器负责是启动时间,而编译器主要是针对执行效率。
image-20210916110233421

运行时数据区域

before JDK1.8

image-20210916114956983

JDK1.8

image-20210916115118487

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

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)

局部变量表主要存放编译期可知的数据类型(如booleanchar...),和对象引用。

Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

本地方法栈:虚拟机栈执行 Java 方法,字节码,本地栈为虚拟机用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所有线程共享,唯一目的:存放对象实例,几乎所有对象实例和数组都在这里分配内存(几乎是因为优化,可能在栈上)。

JDK 7及之前:

image-20210916124510565

JDK 8之后,方法区没了,变成了直接内存中的元空间。

分代

分代是因为:从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

HotSpot JVM 把年轻代分为了三部分:1个 Eden 区和2个Survivor区(分别叫FromTo)。默认比例为8:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为FromSurvivor区,SurvivorTo是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到 To,而在 From 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到To区域。经过这次GC后,Eden区和From区已经被清空。这个时候, FromTo会交换他们的角色,也就是新的 To就是上次GC前的From,新的From就是上次GC前的 To。不管怎样,都会保证名为ToSurvivor区域是空的。Minor GC会一直重复这样的过程,直到 To区被填满, To区被填满之后,会将所有对象移动到年老代中。

image-20210916125313908

方法区

线程共享,存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码,别名Non-Heap,和堆区别开。

永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

元空间(Metaspace)

任何文件系统中的数据分为数据元数据。数据是指普通文件中的实际数据,而元数据指用来描述一个文件的特征的系统数据,诸如访问权限、文件拥有者以及文件数据块的分布信息 (inode) 等等。

public class OOMTest {
    public static void main(String[] args) {
        try{
            //准备url
            URL url = new File("D:/classes").toURI().toURL();
            URL[] urls = {url};
            //获取有关类型加载的JMX接口
            ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
            //用于缓存类加载器
            List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
            while(true) {
                //加载类型并缓存类加载器实例
                ClassLoader classLoader = new URLClassLoader(urls);
                classLoaders.add(classLoader);
                classLoader.loadClass("visualvm.ClassA");
                //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
                System.out.println("total: "+ loadingBean.getTotalLoadedClassCount());
                System.out.println("active: "+ loadingBean.getLoadedClassCount());
                System.out.println("unloaded: "+ loadingBean.getUnloadedClassCount());
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

可以用visualvm查看:

image-20210916143607148

image-20210916143600808

永久代替换为元空间替换原因

整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

HotSpot 虚拟机对象

对象创建

类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

分配内存

两种方式:

image-20210916152540771

线程安全

CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。

初始化零值

设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中

执行init方法

对象布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

String 类和常量池

String s1 = new String("abc");这句话创建了几个字符串对象?

将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

image-20210916153749043

参考

在linux上获取文件的创建时间和实战一例

JVM笔记 -- JVM经历了什么?

Java 内存区域详解

Java 8: 元空间(Metaspace)

元数据(Metadata)

新生代Eden与两个Survivor区的解释

Idea中安装配置VisualVM查看监控jvm