JVM(一)——内存结构

一. 前言

1、什么是 JVM?

1)定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
2)好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

3)比较:

jvm jre jdk的关系如下图

2、学习 JVM 有什么用 ?

  • 面试
  • 理解底层的实现原理
  • 中高级程序员的必备技能

3、常见的JVM

4、学习路线

二、内存结构

1、程序计数器

1)定义

Program Counter Register 程序计数器(寄存器)

作用,是记住下一条jvm指令的执行地址

特点

  • 是线程私有的
  • 不会存在内存溢出

2)作用

java 复制代码
0: getstatic #20 // PrintStream out = System.out; 
3: astore_1 // -- 
4: aload_1 // out.println(1); 
5: iconst_1 // -- 
6: invokevirtual #26 // -- 
9: aload_1 // out.println(2); 
10: iconst_2 // -- 
11: invokevirtual #26 // -- 
14: aload_1 // out.println(3); 
15: iconst_3 // -- 
16: invokevirtual #26 // -- 
19: aload_1 // out.println(4); 
20: iconst_4 // -- 
21: invokevirtual #26 // -- 
24: aload_1 // out.println(5); 
25: iconst_5 // -- 
26: invokevirtual #26 // -- 
29: return
  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

2、虚拟机栈

1)定义

Java Virtual Machine Stacks (Java 虚拟机栈)

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

问题辨析:

  1. 垃圾回收是否涉及栈内存?
    不涉及栈内存,栈内存是在方法调用时产生的,栈帧在每次弹出栈后,会被自动回收掉。
  2. 栈内存分配越大越好吗?
    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
  3. 方法内的局部变量是否线程安全
    • 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的-
    • 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

2)栈内存溢出

报错: java.lang.StackOverFlowError

  • 栈帧过多会导致栈内存溢出。常发生在递归调用过多,或循环引用问题
  • 栈帧过大会导致栈内存溢出
    -Xss指令可以为虚拟机栈分配内存大小。

3)线程运行诊断

案例:cpu 占用过多

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

3、本地方法栈

java 语言调用其他语言的方法或接口,实现更底层的应用和操作,本地方法栈就是用来存储的。例如 java 中的 native 关键字所引用的就是 c 或 c++ 的方法。

4、堆

1)定义

Heap 堆

  • 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所
    有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java
    世界里"几乎"所有的对象实例都在这里分配内存。

特点

  • 它是线程共享的堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

2)堆内存溢出

报错:java.lang.OutOfMemoryError: Java heap space

-Xmx 指令可以指定堆内存大小。

3)堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具

5、方法区

1)定义

方法区和堆一样,是各个线程共享的内存区域,存储了每个类的结构,例如成员变量、静态变量、方法数据、成员方法、构造器和运行时常量池。

虽然方法区逻辑上是堆的一部分,但是简单的实现可以不同,方法区只是一个规范,例如jdk8之前hotspot虚拟机的"永久代"就是方法区的实现方式之一。jdk8后,使用hotspot虚拟机使用"元空间"的方式实现方法区,也就是在本地内存来实现元空间。

2)组成

3)方法区内存溢出

jdk1.8之前 报错:java.lang.OutOfMemoryError: PermGen space

jdk1.8之后 报错:java.lang.OutOfMemoryError: Meta space

使用 -XX:MaxPermSize=8m 指定永久代内存大小

使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

场景:

  • spring
  • mybatis cglib

4)运行时常量池
运行时常量池 (Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字

段、方法、接口等描述信息外,还有一项信息是常量池表 (Constant Pool Table),用于存放编译期生

成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

*.class 文件目录下使用javap -v ./HelloWorld.class查看二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
    等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
    池,并把里面的符号地址变为真实地址

5)StringTable

StringTable 底层是 HashTable ,存储的字符串是唯一的,不能扩容。

常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象

java 复制代码
public static void main(String[] args) {
	String s1 = "a";	// 默认懒加载
	String s2 = "b";
	String s3 = "ab";		// 在 StringTable 字符串常量池中创建
	String s4 = s1 + s2;	// new StringBuilder().append("a").append("b").toString(); => new String("ab");	在堆中创建的对象
	String s5 = "a" + "b";	// javac 在编译期间的优化,结果已经在编译期间确定为 ab,即 s5 == s3 -> true
}

6)StringTable 的特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量 拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则直接返回串池中的对象,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则直接返回串池中的对象,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

例1:

java 复制代码
// 在 jdk1.8中
public class Main{
	public static void main(String[] args) {
		// 串池StringTable["a", "b"]
		// 堆 new String("a"), new String("b"), new StringBuilder() * 3, new String("ab")
		String s = new String("a") + new String("b");	//一共创建了 6 个对象
		// TODO: 字符串对象s调用intern()方法, 由于串池中没有"ab",将 s 放入串池中,直接返回串池中的对象
		String s2 = s.intern();		
		String x = "ab";	// 取出串池中的 "ab"
		
		System.out.println( s2 == x);	// true
	    System.out.println( s == x );	// true
	}
}

例2:

java 复制代码
// 在 jdk1.6中
public class Main{
	public static void main(String[] args) {
		// 串池StringTable["a", "b"]
		// 堆 new String("a"), new String("b"), new StringBuilder() * 3, new String("ab")
		String s = new String("a") + new String("b");	//一共创建了 6 个对象
		// TODO: 字符串对象s调用intern()方法, 由于串池中没有"ab", 字符串对象s会被复制一份放到串池中,返回串池中的对象
		String s2 = s.intern();		
		String x = "ab";		// 取出串池中的 "ab"
		
		System.out.println( s2 == x);	// true
	    System.out.println( s == x );	// false
	}
}

7)StringTable的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

8)StringTable垃圾回收

添加虚拟机参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

java 复制代码
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 10000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}
shell 复制代码
[GC (Allocation Failure) [PSYoungGen: 2048K->496K(2560K)] 2048K->720K(9728K), 0.0015984 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
10000 	# 10000个字符被GC回收了一部分
...
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      5671 =    136104 bytes, avg  24.000
Number of literals      :      5671 =    346048 bytes, avg  61.021	# 字符串常量池中的字符串
Total footprint         :           =    962256 bytes

9)StringTable性能调优

如果需要添加的字符串常量的数量很多,可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间。

java 复制代码
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
  • 考虑将字符串对象是否入池
    • 可以通过 intern 方法减少重复入池

6、直接内存

1)定义

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

2)分配和回收原理

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

在进行JVM调优时,尝试用-XX:+DisableExplicitGC指令,可以防止我们自己手动的进行垃圾回收 (System.gc())。

但是在直接内存中,JVM无法自动对直接内存进行垃圾回收,我们可以通过 Unsafe 中的 freeMemory 方法手动释放内存

相关推荐
小火球2.027 分钟前
STM32简介
jvm
Y编程小白11 小时前
JVM组成
jvm
陈老师还在写代码12 小时前
在服务器部署JVM后,如何评估JVM的工作能力,比如吞吐量
运维·服务器·jvm
鲨鱼 Fish15 小时前
JVM春招快速学习指南
java·jvm
兀行者(做个有情怀的java程序员)1 天前
什么是Java虚拟机(JVM)?它的作用是什么?
java·开发语言·jvm
北执南念1 天前
Synchronized使用
android·java·jvm
java小罗_江西南昌2 天前
17.垃圾回收器
java·jvm
计算机小白一个2 天前
蓝桥杯 Java B 组 - 第 1 周复习总结
java·开发语言·jvm·算法·蓝桥杯
众乐乐_20082 天前
JVM栈帧中|局部变量表、操作数栈、动态链接各自的任务是什么?
java·开发语言·jvm
阿乾之铭2 天前
JVM(Java 虚拟机)
jvm