引言
随着Java行业竞争越来越激烈,面试的难度也在不断的提升
初级程序员也是很有必要了解掌握JVM,简单说是为了应对面试,换句话说也是为了长远
学习JVM可以更好的理解一些底层的实现
- 自动拆箱、装箱
- foreach怎么实现的
- 动态代理相关
- …
目录
- 1JVM简单介绍
- 2JVM学习路线
- 3内存结构
- 3.1程序计数器
- 3.2虚拟机栈
- 3.3本地方法栈
- 3.4堆
- 3.5方法区
- 4StringTable字符串常量池
开始
1简单介绍
2学习路线
- JVM内存结构
- GC垃圾回收
- 字节码
- 类加载器
- 解释器、即时编译器
3JVM内存结构
3.1程序计数器(物理实现为寄存器)
作用:
Program Counter Register 记录下一条jvm指令的执行地址
原理:
Java源代码经编译之后,变成二进制字节码(对应为jvm指令),然后jvm将指令 经过 解释器解释为 机器码,最后CPU执行
特点:
- 线程私有:各个线程在时间片结束的时候,每个线程都有一个私有的程序计数器 记录当前线程的 执行位置
- 不会存在内存溢出
3.2虚拟机栈
-
栈-线程运行需要的内存空间(每个线程都会对应一个虚拟机栈)
-
栈帧-一个栈可以看做是多个栈帧组成,一个栈帧对应着一次方法的调用(每个方法运行时需要划分一个栈帧内存,形参、局部变量、返回地址,栈帧的大小由上述三者决定)
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
定义
Java Virtual Machine Stacks
演示栈帧
问题辨析
- 垃圾回收是否涉及栈内存—》不涉及,方法结束会自动弹栈
- 栈内存分配越大越好吗—》不是,栈内存越大,线程数越少,因为每个线程都有一个虚拟机栈。可以通过虚拟机参数来指定
-Xss yoursize
,Windows下根据虚拟内存来给定,其他操作系统下为1024kb - 方法内的局部变量是否线程安全—》安全,变量是私有的。每个线程的虚拟机栈是私有的
- 判断一个变量是不是线程安全的,不仅需要判断是不是方法内的局部变量,还需要判断 是否逃离方法的作用范围(形参、return,即是否别的线程能拿到该变量)
3.3栈内存溢出
原因
- 栈帧过多:栈帧(每一次方法的调用)过多,栈空间不足,如无终结递归
- 栈帧过大
案例一:找出CPU占用过多的代码
top
:查看JVM虚拟机下的所有进程,找对对应线程的编号ps H -eo pid,tid,%cpu | grep 待查进程id
:查看进程和该进程对应的多个线程,找出哪个线程占用的CPU过高jstack 进程id
:查看当前进程对应的全部线程的详细信息(需要注意将10进制转化为16进制进行比对查看),找出对应的代码位置
案例二:程序运行很长时间没有结果(可能多个线程发生死锁)
jstack 进程id
:查看两线程之间是否发生死锁,找出对应代码的位置
3.4本地方法栈
在JVM调用本地方法的时候,会自动开辟的空间栈,Native Method Stacks
- 本地方法:由其他语言像c、c++编写的操作系统底层打交道的方法,像Object类的clone()方法,就是本地方法
3.5堆
Heap,前面的栈都是线程私有的,堆是线程共享的
- 通过new关键字,创建对象都会使用堆内存
- 它是线程共享的,堆中对象都要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出问题
- 虽然有垃圾回收机制,但是对象在使用中不被回收累计的情况下,也是会发生堆内存溢出的问题
堆内存诊断
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存的占用情况,只能看某一时刻的
- jconsole工具:图形化界面的,多功能的监测工具,可以连续监测,线程、cpu等
直接在控制台输入工具代码即可使用
案例:垃圾回收后,程序仍然占用很高的内存
- jvisualvm工具:可视化虚拟机工具
3.6方法区
方法区特点
- 所有JVM线程共享的区域(类似堆是线程共享的)
- 存储跟类结构相关的一些信息,像运行时常量池
- 像类的成员变量、成员方法、构造器方法的代码
- 方法数据、特殊方法(类的构造器)
- 方法区在JVM虚拟机启动的时候创建
- 方法区逻辑上是堆得概念(可以被看做JVM假的堆),但是JVM厂商在实现的时候不一定,
- 在JVM堆内物理实现—》JDK1.8之前,永久代 PermGen space
- 在操作系统也就是JVM之外物理实现—》JDK1.8之后,元空间 Metaspace
- 内存也是会溢出的
方法区内存溢出的现象
运行时常量池
- 二进制字节码
.Class
(包括类基本信息、常量池、类方法定义、虚拟机指令) - 可以通过
javap -v 字节码文件
反编译为我们能读懂的信息,JVM主要根据#数字
去常量池中找一些静态变量等的相关信息 - 常量池:就是一张表存在
.class
字节码文件中,给JVM指令提供一些常量符号,便于查找,要执行的类名、方法名、参数类型、字面量等 - 运行时常量池:常量池是
.class
文件中的,当该类被加载时,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的地址(就是放进运行时真实的内存,将真实地址对应起来)
3.7StringTable字符串常量池
往期博客相关:
Java课堂篇3_对象类型下的“==“和equals()、字符串常量池、(-128-127)int类型装箱Integer大整型常量池、关于static关键字
Java课堂篇9_String、StringBuilder、StringBuffer简单理解
字符串常量池StringTable 和 常量池、运行时常量池的联系
- StringTable的数据结构为HashTable结构,JVM刚启动的时候为空状态
- JVM启动之后,方法区常量池的信息,都会被加载到运行时常量池中,这时 代码中的
String str = "a"
中的a还是常量池中的符号,还没变成java对象 - JVM执行完 方法区中的JVM创建该变量的虚拟机指令后,会把
符号a
变为 字符串对象a,然后去找StringTable中是否存在 对象a- 不存在:直接将字符串对象 a对象 放入字符串常量池
- 存在:直接指向字符串常量池中的对象
举栗1
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //----> new StringBuilder().append("a").append("b").toString()
//而StringBuilder的toString() 方法----》new String("ab");
举栗2
String s3 = "ab";
String s5 = "a" + "b";
// s3 和 s5 引用的为一个字符串常量池中的对象 "ab"
原理是javac
在编译器的优化,结果已经在编译器间确定 "a" + "b"为 "ab"
StringTable字符串常量池的特性
- 常量池中的字符串仅仅是符号,第一次用到才变为对象
- 利用串池的机制,来会面重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译器优化
- 可以使用
intern方法
,主动将串池中还没有的字符串对象放入串池
JDK1.8之后,直接入池,本身直接从堆指向池子
对比 (注意这是1.8之后的环境)
JDK1.8之前,intern()
方法会创建一个 副本 入池,本身还是指向堆区,
对比
面试题
3.8StringTable的位置
JDK1.6的时候,StringTable字符串常量池在 永久代方法区的常量池中,但是大量的字符串常量会导致永久代的内存不足
JDK1.7及以后,StringTable字符串常量池 挪到了堆中,便于垃圾回收
3.9StringTable的垃圾回收机制
当内存空间不足的时候,字符串常量池中的常量也会被垃圾回收
3.10StringTable的性能调优
数据结构为hash表,数组挂链表
- 当hash桶的个数较多时,冲突少,查找速度较快
- 当hash桶的个数较少时,冲突多,查找速度较慢
现实场景:可以将内存中的字符串对象考虑是否入池,如果内存中有大量的重复的字符串对象,可以入池减轻内部的压力