JVM 学习笔记(内存结构部分)

JVM、JRE和JDK的区别?

JVM:Java虚拟机

JRE:Java运行时环境 = JVM+基础类库

JDK:Java开发工具包 = JVM+基础类库+编译工具

1. 内存结构

1.1 程序计数器(寄存器的抽象)

  • 实质:操作系统的寄存器

  • 作用:记住下一条 JVM 指令的执行地址

JVM 指令 到 CPU 上运行的流程:

jvm指令->放入程序计数器->解释器从程序计数器中获取 jvm 指令 -> 解释器将其转变为机器码->CPU 运行机器码

  • 特点:

    • 程序计数器是线程是私有的

    体现在进行线程切换的时候:线程1 切换为线程2,程序计数器需要记住线程1目前已经执行到了哪一条指令,以便于线程1下次在 CPU 上运行时要从哪里开始执行。

    • 不会存在内存溢出

1.2 虚拟机栈

  • 定义:线程运行需要的内存空间

一个栈由多个栈帧组成

栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)

垃圾回收是否涉及栈内存?

不涉及,栈内存的分配直接通过入栈来完成,栈内存的清理直接通过出栈来进行。

栈内存分配是否越大越好?

不是。运行代码时,可以通过 -Xss 指定栈内存大小。默认大小为1024 KB(Windows 根据虚拟内存来定)

栈内存越大,最大运行的线程数反而会更少,因此运行速度不一定快。只是能够进行更多次的方法递归调用。

方法内的局部变量是否是线程安全的?

需要看局部变量是否逃离方法的作用范围

  • 如果方法内的局部变量没有逃离方法的作用范围(比如没有作为返回值),则是线程安全的。

    因为一个线程对应一个栈,线程内的每次方法调用都会产生一个新的栈帧,栈帧内的局部变量是私有的,所以线程安全。

    如果是 static 关键字修饰的则不是线程安全。因为static 变量会被存放在方法区,而非栈内存,属于共享内存区域,因此不是线程安全的。

  • 如果逃离了方法的作用范围(比如作为参数/作为返回值),需要考虑线程安全问题。

1.2.2 栈内存溢出

  • 原因:

  1. 栈帧过多(方法递归太多次)

  2. 栈帧过大

1.2.3 线程诊断常见问题:

CPU 占用过高怎么排查?

首先通过 top 命令找到 CPU占用很高的那个进程ID

然后通过 ps H -eo pid,tid,%CPU |grep + 端口号 查看某个进程的进程id、线程id、CPU占用率,看到底是哪个线程占用CPU很高,记录下这个线程ID。

然后通过 jstack + 进程ID 打印这个进程的所有线程。(注意:打印出来的线程ID为十六进制,需要进行换算!),找到对应的线程,再定位到对应的代码行数进行 debug。

jstack + 进程ID 也能够排查死锁问题!

1.3 本地方法栈

Java虚拟机调用本地方法的时候需要为这些方法分配内存空间。

本地方法:那些不是由Java代码编写的,由C/C++编写的本地方法(有时需要和操作系统打交道)。

1.4 堆

  • 定义:通过 new 关键字创建的对象都会使用堆内存。

  • 特点:

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

    2. 有垃圾回收机制

1.4.1 堆内存溢出

(OutOfMemoryError)

可以通过 -Xmx 修改堆内存大小(默认为 4G)

1.4.2 堆内存诊断

  1. jmap

首先通过 jps 命令查看当前运行的进程

然后通过 jmap -heap + 进程 ID 打印出来当前 Java 进程的堆内存使用情况

  1. jconsole

可以直观地查看某个进程的堆内存使用情况

1.5 方法区

  • 定义:在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存(由操作系统管理的内存空间,不由 JVM 管理内存了)。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。

1.5.1 StringTable

  • 本质:哈希表

String s1= "a" 过程:

会先从 StringTable 中查找是否有 "a",如果有,拿来直接用,没有的话则将 "a" 变为 String 对象,并且放入 StringTable 中。每个字符串在 StringTable 中是唯一的。

判断两个字符串对象是否相等:
public static void main(String[] args) {
    String s1 = "a"; // 懒惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2; // new StringBuilder()
    System.out.println( s3 == s4 );
}

答案:false

s3 存储在 StringTable (串池)当中

s4 是通过StringBuilder 拼接 s1 和 s2 得到的对象,存储在堆里面

public static void main(String[] args) {
    String s1 = "a"; // 懒惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2; // new StringBuilder()
                                           String s5="a"+"b";                   
    System.out.println( s3 == s5 );
}

答案: true

"a"+"b" 直接在编译期间确定为 “ab” 因此 s5 是直接从 StringTable 中找到的,所以 s3==s5

  • JDK1.8 后可以通过字符串对象的 intern( ) 方法尝试将某个字符串对象放入 StringTable 中,如果有则不会放入,但仍然会返回 StringTable 中的字符串

  • JDK1.6之前则是复制一个新的字符串对象再把新的对象放入 StringTable 。

JDK 1.8后:

public static void main(String[] args) {
    // StringTable: ["ab","a","b"]
    String s1=new String("a")+new String("b"); // new String("ab") 堆中
    String s2=s1.intern();
    System.out.println(s2=="ab");
    System.out.println(s1=="ab");
}

运行结果:

true

true

s1.intern() 将 s1 放入了 StringTable中

因此 s1=="ab" s2=="ab"

如果是 JDK 1.6 则运行结果为:

true

false

public static void main(String[] args) {
    // StringTable: ["ab","a","b"]
    String x="ab";
    String s1=new String("a")+new String("b"); // new String("ab") 堆中
    String s2=s1.intern();
    System.out.println(s2==x);
    System.out.println(s1==x);
}

运行结果:

true

false

因为 "ab" 在一开始已经被放入 StringTable 中了

s1.intern() 的时候发现 StringTable 中已经存在 "ab" ,所以不会将 s1 放入 StringTable,s1 仍然是放在堆内存中的。

虽然 StringTable 中已经存在 "ab" ,但 s1.intern() 仍然会返回 StringTable 中的字符串对象,所以 s2==x 返回为 true

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值