【JVM】学习笔记1——JVM基本概念和结构

本文是个人JVM学习笔记整理,学习顺序主要基于 黑马程序员的JVM视频 相关知识点参考 深入理解Java虚拟机(第三版) 欢迎大家讨论交流并指正错误。


本篇笔记仅涉及JVM基本概念和结构。


1. Jvm

1.1. 定义

Java虚拟机(Java Virtual Machine)是一个抽象的计算机,JVM像一个真正的计算机一样,它有一个指令集,并在运行时操纵各种内存区域。JVM是Java字节码的运行环境,JVM在执行字节码时,会把字节码解释成不同平台上的机器指令执行。

1.2. 好处

  • 可以跨平台运行

在一个操作系统上编写后可以在别的操作系统中运行

  • 自动内存管理,垃圾回收功能

(相比较于C语言)

  • 数组下标越界检查

(相比较于C语言,不会覆盖其他内容)

  • 多态

面向对象编程的基石

1.3. JRE JDK比较

  • JVM(Java Virual Machine):是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。
    JVM + 基础类库 = JRE

  • JRE(Java Runtime Environment):是Java运行环境,所有的Java程序都要在JRE下才能运行。
    JRE + 编译工具 = JDK

  • JDK(Java Development
    Kit):是Java开发工具包,它是程序开发者用来编译、调试Java程序,它也是Java程序,需要JRE才能运行。

在这里插入图片描述

1.4. Jvm结构

在这里插入图片描述

1、类从java源代码编译后通过classloader类加载器加载到jvm中

2、类放在方法区,类的实例对象放在中,调用方法时会用到虚拟机栈、程序计数器、本地方法栈

3、类执行时,每行代码由解释器逐行执行,热点代码(执行次数多的代码)会由即时编译器进行编译(优化后执行),垃圾回收会对堆中不再被引用的对象进行回收

4、本地方法接口负责调用操作系统的方法


2. 程序计数器

2.1. 定义、作用

程序计数器的作用就是记录下一条jvm指令执行的地址

指令的执行过程:
Java源代码 -> 编译为二进制字节码(jvm指令)-> 交给解释器编译为机器码 -> cpu执行

在第一条指令执行时,下一条指令的地址会被放入程序计数器,执行完毕后从程序计数器中取得下条指令的地址。

程序计数器通过寄存器实现(读写指令地址很频繁,因此用寄存器作为程序计数器)。

2.2. 特点

(1)线程私有

每个线程都有自己的程序计数器

线程切换时,会把线程1的下一条未执行指令地址存储于计数器中,只属于线程1

多线程执行时,会根据时间片切换线程,当从线程2切换回线程1时,会从线程1的程序计数器中取得程序地址。

(2)不存在内存溢出


3. 虚拟机栈

3.1. 定义

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

3.2. 容易混淆问题

(1)垃圾回收不涉及栈内存,只回收堆内存

栈内每次方法结束后,会自动出栈,不需要垃圾回收

(2)栈内存不需要过大,否则会影响线程数量

栈内存设置大只是可以进行更多次的方法内存调用,不会增强效率,反而会减少线程数目

栈内存可以通过 -Xss参数指定大小

(3)方法内局部变量是否线程安全

  • 主要在于变量是否为方法内私有的,私有变量是线程安全的(没有逃离方法的作用范围)。

  • static变量多线程同时访问时,非线程安全,需要安全保护。

  • 如果变量作为参数或者返回值(基本类型变量除外,引用对象则会出问题),超出了方法的作用范围,则非线程安全。

3.3. 栈内存溢出原因

报错为:StackOverflowError

(1)栈帧过多导致栈内存溢出

如:递归调用中不正确的结束条件,多个类之间的循环引用

(2)栈帧过大导致栈内存溢出

不常见,栈内局部变量一般不会超过栈内存大小(1M)


4. 本地方法栈

4.1. 定义

本地方法指,不是由java编写的代码(如c/c++),带有native的方法,

本地方法栈就是本地方法运行时所需要的内存空间。

如 Object中 的clone() 方法。

protected native Object clone() throws CloneNotSupportedException;

5. 堆

5.1. 定义

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。

通过 new 关键字,创建对象时都会使用堆内存.

5.2. 特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题

  • 有垃圾回收机制

5.3. 堆内存溢出

如果对象一直有程序调用并不断实例化对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会 产生内存溢出异常。

报错信息:

OutOfMemoryError: Java heap space

可以通过 -Xmx 参数控制堆内存大小

5.4. 堆内存诊断

(1)jps 工具

查看当前系统中有哪些 java 进程

(2)jmap 工具

查看堆内存占用情况 : jmap - heap 进程id (先用jps查看进程号)

只能查询某个时刻的使用情况,不能连续监测

在这里插入图片描述

(3)jconsole 工具

图形界面的,多功能的监测工具,可以连续监测

在这里插入图片描述

6. 方法区

6.1. 定义

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

方法区在虚拟机启动时被创建。

方法区在逻辑上是堆的一部分,但没有规定方法区的位置或用于管理编译代码的策略。

hotspot在JDK 8 之前使用永久代(堆的一部分)作为方法区,1.6之后使用元空间(本地内存)作为方法区。

6.2. 组成

hotspot在JDK 8 之前使用永久代(堆的一部分)作为方法区,这样使得 HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的 工作。

字符串表在方法区的常量池中

在这里插入图片描述

JDK 8 之后使用元空间(本地内存)作为方法区。

字符串表在堆中

在这里插入图片描述

6.3. 方法区内存溢出

jdk8 以前会导致永久代内存溢出

报错: java.lang.OutOfMemoryError: PermGen space

可以通过 -XX:MaxPermSize=8m 设置内存大小

jdk 8之后会导致元空间内存溢出

报错: java.lang.OutOfMemoryError: Metaspace

可以通过 -XX:MaxMetaspaceSize=8m 设置内存大小

6.4. 运行时常量池

方法区的一部分。

Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用(要执行的类名、方法名、参数类型、字面量等信息),这部分内容将在类加载后存放到方法区的运行时常量池中,并把里面的符号地址变为真实地址。

编译的二进制字节码包括:类基本信息、常量池、类方法定义(包含虚拟机指令)

对class文件反编译后可以查看以上信息:

开头是类方法信息

在这里插入图片描述

Constant pool是常量池

在这里插入图片描述

大括号中是类的方法定义,构造函数和其他方法

在这里插入图片描述

Code中是虚拟机指令

在这里插入图片描述

Code中的#1编号对应的是常量池中的元素

6.5. StringTable

6.5.1. 定义

用于存储字符串。

StringTable底层是通过 哈希表 实现的。

注意 String的创建方式和存储位置:

String s1 = "a";
String s2 = "b";
  • 通过常量声明的字符串,会直接存入StringTable,如果StringTable中有此字符串,会直接返回

String s3 = new String("a")
  • 通过 new 创建的字符串,会存入堆中

因此 s3==s1 为false


String s4 = "a" + "b";
String s5 = "ab"
  • 字符串常量进行拼接时,java在编译期进行优化,它默认常量直接的拼接不会发生变化,结果在编译期间确定为”ab”。因此直接将“ab” 存入StringTable

因此s4==s5 为true


String s6 = s1 + s2;   // new String("ab")
  • 如果将两个String对象进行拼接,java处理中会通过StringBuilder对象进行拼接,之后返回sb.toString(),此方法会new一个String对象存放在堆中

因此s4==s6 为false


可以使用 String的intern()方法,主动将StringTable中还没有的字符串对象放入StringTable。

注意在jdk8 之后,如果StringTable中有此String则并不会放入,直接返回StringTable的string对象;如果没有则会放入StringTable, 并把StringTable中的对象返回。

String x1 = "ab"; 
String x2 = s1 + s2; //因为x1先执行,StringTable中已经有"ab",x2不会存入StringTable
String x3 = x2.intern();

此处 x1==x2为false,x1==x3为true


如果顺序变为:

String x2 = s1 + s2; //因为x1先执行,StringTable中没有"ab",x2存入StringTable
String x3 = x2.intern();
String x1 = "ab";

x1==x2为true,x1==x3为true


在jdk8 之前,如果StringTable 中 没有此String,会把此对象复制一份放入StringTable ,并返回StringTable中的对象。

因此不论顺序如何,x1==x2均为false


6.5.2. StringTable 位置

在这里插入图片描述

jdk8之前在永久代的常量池

jdk8之后在

原因:

永久代垃圾回收触发很晚,内存回收效率很低,但stringtable使用频繁,回收效率低会占用大量内存;因此转移到堆中,垃圾回收效率高

6.5.3. StringTable垃圾回收

当内存空间不足时,StringTable也会被垃圾回收。

-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

可以查看StringTable大小和垃圾回收信息

6.5.4. StringTable 性能调优

(1)调整StringTable底层 哈希表 桶的个数

通过 -XX:StringTableSize= 调整桶个数

如果系统中字符串常量比较多,可以将桶大小设置大一点,以寻求更好的哈希分布,提升性能

(2) 考虑将字符串对象放入StringTable

使用 intern 方法,将StringTable中还没有的字符串对象放入StringTable;程序运行中有很多重复的字符串对象,不放入stringtable ,只在堆中会占用很大空间,放入stringtable后相同的字符串只有一份。

8. 直接内存

8.1. 定义

是操作系统的内存,不属于jvm管理。

  • 常见于 NIO(New Input/Output) 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

直接内存进行IO操作,通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作,相比较于传统Java IO更快

传统Java IO:

复制文件时 Java IO需要从系统内存中的缓冲区中读取数据到java的缓冲区;

在这里插入图片描述

DirectByteBuffer 则会直接在系统内存中划出一个直接内存,java复制时候,系统将文件数据读取到直接内存,java也可以从直接内存中读取数据,少了一次数据读取操作。

在这里插入图片描述

8.2. 直接内存的内存溢出

报错: java.lang.OutOfMemoryError: Direct buffer memory

8.3. 直接内存的分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值