一、基本概念
定义:
Java Virtual Machine -java程序的运行环境(java二进制字节码的运行环境)
好处:
1.一次编写,到处运行。JVM屏蔽了java字节码文件和底层操作系统之间的差异。对外提供了
一个一致的运行环境。
2.有自动管理机制,提供了垃圾回收的功能。
3.数组下标越界检查。之前c语言的话,如果越界了数组的新元素会覆盖程序的部分,而jvm会
抛出异常,不会造成上述情况。
4.多态。
比较:
JVM:java的运行环境
jre:JVM+基础类库
JDK:JVM+基础类库+编译工具
学习这个有啥用:
1.面试
2.理解底层的实现原理
3.中高级程序员的必备技能
常见的JVM:
JVM是一个规范,所以很多公司对这个规范进行了一系列的是实现。比如Oracle的HotSpot,
Eclipse OpenJ9
二、内存结构
1.程序计数器
定义:Program Counter Register程序计数器(寄存器)
作用:保存下一条JVM指令的地址。就是下图中 0,3,4,5。根据下图的描述,在解释
器将地址是0的指令解释为机器码的时候,程序计数器中会存入3。在上条指令
getstatic执行结束后,解释器去程序计数器中找地址为3的指令,并解释为机器
码,程序计数器中存4。在物理中,JVM使用寄存器来实现程序计数器。
特点:(1).是线程私有的。java是支持多线程的,CPU的调度器对不同线程提供不同的时
间片。如果在一个时间片内线程1的程序没有执行完,线程1的状态会被保存下来,
然后,时间片会分配给线程2的程序执行。每个线程都会有自己的程序计数器,在
切换线程时候,各自的程序计数器会保存下来各自线程的下一条命令。
(2).这个区域不会造成内存溢出。
2.虚拟机栈
定义:
栈是一种先进后出的存储结构,这里的虚拟机栈,就是线程运行需要的内存空间。如果
有多个线程,就会有多个虚拟机栈。
栈内的每个元素,我们称之为栈帧。每个栈帧都对应着 一次 方法的调用,也就是说栈帧
就是方法运行需要的内存空间。包括,参数,局部变量和返回地址所需要的内存空间。每个
线程只有一个活动栈帧,对应着档期啊正在执行的那个方法。
调用改方法时会将该栈帧压入虚拟机栈中,调用完毕后该栈帧出栈。如果方法一调用了
方法二,那虚拟机栈会先把方法一的栈帧压栈,然后再把方法二的栈帧压进栈。
问题辨析:
1.垃圾回收会涉及占内存吗?不会因为随着方法调用的结束,方法所占用的内存会随着
方法的结束被释放掉,不需要垃圾回收。
2.栈内存分配越大越好嘛?不好,因为栈内存过大可能会使该机器可支持的最大线程数
量变少,因为内存就那么大,每个栈的内存大了,最大线程就少了。一般1M。
3.方法内的局部变量是否是线程安全的?其实这个问题可以改为,这个局部变量对应不
同的线程来说是不是共享的?还是说线程私有的?是安全的。但是如果两个线程共同访问一
个类中的static变量,就会不安全。
4.内存溢出问题:(1)当一直调用方法,但是却不释放方法的话,虚拟机栈的栈帧就会越
来越多,最终导致内存溢出。比如递归调用。(2)栈帧过大。
线程运行诊断:
1.CPU占用过高。解决:top命令可以检测到后端进程的进行对CPU的占用情况。ps H
-eo pid,tid,%cpu | grep 进程id 命令可以查看线程对CPU的占用情况。jstack 进程id 命令就能
找到特定某个线程,和与之相关的代码。
2.迟迟得不到结果,有可能是线程死锁导致了这个问题。解决:使用jstack 进程id 命令,在打印信息中找到“Found one Java-level deadlock”信息......
3.本地方法栈
java代码不太能和计算机底层打交道,所以他需要调用位于我们计算机上的一些c语言,c++语言编写的本地代码,而这些本地代码就被存入到我们的本地方法栈中。
4.堆
定义:
通过new关键字,创建的对象都会使用堆内存。
特点:
1.上面的程序计数器,虚拟机栈,本地方法栈都是线程私有的。堆和下面的方法区可以
看成是线程共享的,在堆中的对象都需要考虑线程安全的问题。
2.有垃圾回收机制。
堆内存溢出问题:
1. 当一直创建新的对象,这些对象又一直在被使用,就会导致对象越来越多,然后溢
出。-Xmx可以设置对空间的大小。
堆内存问题的诊断:
工具:jps:查看当前系统中有哪些java进程。jmap:查看堆内存占用情况。jconsole:图形
界面的,多功能的检测工具,可以连续监测。新创建的对象占用的堆内存都会保存在Heap
Usage中的Eden Space中。调用的命令分别是:(1) jps (2)jmap -heap 进程id 。(3)jconsole
案例:
垃圾回收之后,内存占用依然很高怎么办?使用jvisualvm。可能是顶一个一个较大的数
据,然后他又是一直活动的,导致垃圾回收机制回收不掉。
5.方法区
定义:
方法区是所有java虚拟机线程共享的区域。他存储了跟类结构相关的信息,比如说运行
时常量池,类的成员变量,方法数据以及成员方法和构造器方法的代码。方法去虚拟机启动
时创建,在逻辑上是属于堆的一部分。 但是不同的实现对于方法区域的位置上有所不同。方
法区是有可能会内存溢出的。
内存溢出:
java1.8后方法区的实现使用的是系统内存。内存溢出不好演示,但是我么你可以用
-XX:MaxMetaspaceSize=8m来减少方法区元空间大小。在1.6的时候,如果我们要设置永久
代的大小,命令是-XX:MaxPermSize=8m。如果一直不停的加载类,那么内存就会溢出。在
生产环境中我们可能由于框架使用不规范,导致我们生成了太多的代理类啥的,就会溢出。
运行时常量池:
常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类
型,字面量等信息。运行时常量池,就是当该类被加载时,他的常量池信息就会放入运行时
的常量池,并把里面的符号地址变成真实的地址。
将一个类编译成二进制字节码文件,该文件一般包含三大部分——类基本信息(把一个
class文件反编译后我们可以看到这些基本信息,比如类文件路径,最后修改时间,编译之前
的文件,类的访问权限,父类是什么,继承了那些接口,MD5值),常量池,类方法定义(包含
了虚拟机指令,方法的 )。
在下图中我们可以看到,在虚拟机指令的命令后面有#数字,这个#数字是这个虚拟机指
令运行需要操作或查找的某个常量的地址。而这些常量被存储在方法区中。
串池(StringTable):
1. 常量池中的字符串仅仅是符号,在第一次用到的时候才会创建出一个对象。
2.利用串池的原理,可以避免重复创建字符串对象。
3.字符串变量拼接的原理是StringBuilder,在底层他会创建一个StringBuilder对象,然后把这个
独享转变成String对象,使用new的方式,所以这个新创建的字符串占用的是堆空间。
4.字符串常量拼接的原理是编译期优化。比如如果是把"A"和“B”进行拼接,他会直接在串池中
找有没有“AB”,如果有就使用串池中的这个对象。
5.可以使用intern方法,主动将串池中还没有的字符串对象放进串池。
该方法:
1.8:将某个字符串对象s尝试放进串池中,如果有就不会放入,如果没有就会放入,会
把串池中的对象s2返回。如果放入成功那么这里的s==s2,如果放入失败s!=s2。
1.6:将某个字符串对象s尝试放进串池中,如果有就不会放入,如果没有就会把此对象
复制一份s2放入,会把串池中的对象s2返回。如果放入成功那么这里的s!=s2,如果放入
失败s!=s2。
串池位置:
现象:在1.6的时候,串池是在永久代里面的,但是在1.8之后串池的位置转移到了堆中。方法
区的实现也从原来的永久代变成了原空间(占用本地内存)。
原因:因为永久代里面的垃圾回收的效率很低,而串池又是一个经常用的内存。
串池的垃圾回收:
串池中的数据长期不使用也是会触发垃圾回收机制的。
串池调优:
1.因为串池的底层数据结构是hashMap,桶的个数会影响hash冲突的次数,所以其实串池的
调优可以修改桶个数。-XX:StringTableSize=桶个数。
2.考虑将字符串对象放进串池中。原理就是,如果要存储一堆字符串,但是这写字符串很多都是重复的,我们就不必把所有的字符串都存进内存中。只要存储字符串时候使用intern方法,把想通的字符串存进串池中就好。