JVM基础概论
JVM是什么?
是一个设备的规范,是虚拟出来的计算机 通过实际的计算机上模拟实际的计算机 jvm有一组自己的一套指令 让编码最终转换为JVM指令而不是操作系统指令 所以说只要可以装JVM的地方都是可以使用的 让java可以在不同的平台上实现一次编译处处运行。
多种平台上运行只需要通过java语言编译程序为字节码就可以实现不加修改的运行。JDK --> JRE --> JVM
程序在JVM中执行的整个过程主要会经过下图的各个区域。
当一个字节码文件进入JVM被类加载器加载之后就会进行重新分配去到不同的各个功能区之中。
内存占比始终以8的倍数进行存储
如果一个对象仅有6字节 但是内存依然会分配16字节空间给该对象
JVM内存结构
被加载的类 —> 方法区
实例对象(new出来的东西都是实例对象) —> 堆
实例对象调用方法时 —> 虚拟机栈,程序计数器,本地方法栈
JVM执行引擎
类在执行的时候的每行代码 —> 解释器
频繁调用的代码 —> 编译器,系统会进行优化和识别为频繁操作的代码,然后执行
堆(实例对象)中不再被引用的对象进行回收 —> GC垃圾回收机制
调用底层操作系统的功能,与其打交道的地方 —> 本地方法接口
1.程序计数器 (寄存器)
作用:在执行指令的同时,将下一条的执行号码存入程序计数器,每次执行语句的时候,再不断重复这个过程。如果没有这个计数器,将不会知道下面应该执行什么。cpu中的程序计数器是所有组件中读取速度最快的,这个是属于系统的,但是java在设计的时候就将程序计数器当做了java的寄存器
流程:每条语句都会通过jvm指令进行编译—>被解释器变为二进制字节码 ---->再变为机器码 ---->再由cpu运行
程序计数器的特点
-
线程私有
由于java是支持多线程运行的,所以每个线程都有一个程序计数器
在cpu快速不断切换时,各自存储自己的执行号码
-
不会存在内存溢出
在java虚拟机中唯一一个不会存在内存溢出的区,所以每个厂商设计时就不会设计内存溢出的问题
2.栈内存
每个用户访问一次就是一个线程,每个线程运行时所需要的内存,叫做栈,分为虚拟机栈和本地方发栈。
问:栈的数据结果是什么,为什么?
答:栈数据结构的特点 先进后出 。因为在程序执行的时候,都是一个方法(最开始是main方法)调用另一个方法来执行的。所以当方法被执行的时候就会压栈运行,但是当最后面的方法执行结束的时候就会进行弹栈,然后再执行倒数第二个方法,层层弹栈,按照此逻辑,所以最先进的反而是最后出,才会形成先进后出的数据结构。
本地方法栈
本地方法所使用的内存叫做本地方法栈。
native关键字修饰的方法叫做本地方法(用C语言编写的方法),是直接与操作系统底层进行交互。使java代码可以通过本地方法栈来间接的调用操作系统底层的功能。同样对这些方法进行压栈或弹栈。
虚拟机栈
问:虚拟机栈和本地方法栈的区别?
答:虚拟机栈又叫java栈或者栈,均属于顶层意义的栈,而本地方发栈和虚拟机栈功能是一样的,都是指在执行方法时需要使用内存,对方法进行压栈或者弹栈。
虚拟机栈(java栈)是用户线程所使用的方法进行压栈或者弹栈时的概念
本地方法栈,是JVM用C语言编写的那些方法所使用的栈,同样对这些方法进行压栈或弹栈的概念
每个虚拟机栈(frame)由多个栈帧组成。一个栈帧是由方法的参数,局部变量,返回值组成,每个方法运行时需要的内存就是一 个栈帧,每一个栈帧就表示一次方法的调用,被执行时压栈运行,自动释放。可以简单理解为一个方法。
活动栈帧,意思是对应正在运行执行的那个方法,在方法栈最顶端的栈帧就是活动栈帧
栈内存溢出
-
栈帧过多导致内存溢出(递归)
-
栈帧过大(不太容易出现)
异常名:java.lang.StackOverFlowError
可以进行栈内存的大小设置,默认是1M
问题:
-
垃圾回收机制是否会对栈内存进行回收?
每次生成的栈帧在被执行时,都压栈,每当结束时就会自动的弹栈,然后被释放掉,所以垃圾回收机制不会对栈进行回收也没有必须
-
占内存是分配越大越好嘛?
栈内存划分越大,线程数就会变得越小。
占内存划分大,只是可以进行更多次的递归划分调用。不会增强运行效率,且线程数量随之会变小,所以采用默认大小即可。
-
方法内的局部变量是否是线程安全的?
看一个线程是不是安全的,主要是看这个线程对一个变量的操作是共享的还是私有的
比如 两个线程同时有局部基本类型变量a循环5000次的 a++, 是不会产生线程安全问题的,因为普通变量在底层是线程安全的,并且在方法区中多个线程各自有各自的栈,自己独立操作,即使是传递或者被返回那么都是安全的。
但是如果这个a变量是一个Static的a 那么两个线程会同时对这个变量进行a++ 然后在重新写回公共区,此时就会出现不安全的情况
再比如 如果这个两个线程同时有局部引用数据类型变量StringBuilder循环5000次的 +1, 是不会产生线程安全问题的,因为在方法区中多个线程各自有各自的栈,自己独立操作,但是如果当作返回值或者参数值进行传递到其他方法体当中,则在途中就会变得不安全
结论:如果变量只是在方法内部进行使用,那么该局部变量是安全的。如果该局部变量逃离了方法作用域,基本数据类型的变量仍然安全,引用数据类型变得不安全。
方法局部变量在方法内使用,一定是安全的。逃离出作用域,基本类型是否还安全有待进一步核实。
3.堆内存
组成结构
堆分为 新生代ende、survivor1、survivor1、老年代
垃圾回收机制每执行一次,对象未被回收到 则进阶一次 到达老年代后会被gc二十次后才会被回收
新数据是在新生代 大数据直接到老年代
堆和方法区,是线程共享的区,堆中的对象都需要考虑线程安全问题
new 出来的 东西进堆内存
有垃圾回收机制
新生代、老年代、方法区 常用的GC
默认4G,可进行修改
堆内存溢出
什么情况下会出现堆内存溢出?
当垃圾回收机制没有被激活的时候,而新new的对象又不断的出现就会出现堆内存溢出。而垃圾回收机制何时进行回收的先决条件是该对象是否还在被使用,当前面的对象一直在被使用,后面又一直在new的时候就会出现堆内存溢出。
异常名:java.lang.OutOfMemoryError:java heap space
4.方法区
属于共享区间
定义:在虚拟机启动时被创建。所有虚拟机线程都可以进行访问的区称之为方法区,仅仅是概念上的单独的一个区,逻辑上是堆的一部分,由不同的JVM厂商(如:oracle)进行实现。
作用:存储与类相关的信息,如:成员方法,构造器,成员变量,运行时常量池等
若方法区内存不足,也会抛异常名:java.lang.OutOfMemoryError的异常
方法区在1.6和1.8版本的区别
JVM1.6版本,单独有方法区地方存储,里面是一个永久代(PermGen),存储着类相关信息。
JVM1.6后的版本,将该区域去除放到了本地内存中,不再与JVM相关且名字更改为元空间(MetaSpace)。默认情况下运行时使用的是系统内存,且无上限
补充:类加载器classloader的作用是可以加载类的二进制字节码
运行时常量池
常量池
类字节码中的一张常量表,存放着用于解析代码的符号集合,虚拟机指令通过这张表来对应找到要执行的类的各项信息。如:类名,方法名,参数,字面量信息(用户输入的信息,整数,字符串等)等等等等。每个类都有一个常量池,里面存储的是该类的符号集合(#number…)。
运行时常量池
运行时常量池的意思就是,当某类被加载的时候,会将该类的常量池加载入内存,而放在内存中的位置就叫做运行时常量池,并且会把该类的符号集合(#number…)变为真实的内存地址用来进行查找。
StringTable(串池)
运行时常量池中有一个区域叫做StringTable又叫串池,数据结构是哈希表不能扩容,主要用于加载字符串对象。当代码执行到的时候才会生成对应的字符串对象存入串池,未被加载时仅仅是运行时常量池中的符号,还未变为字符串对象。
常量池与串池的区别
常量池最初存在于类字节码当中,当该类被加载的时候才会被加载到运行时常量池中(内存)
public void TestStringTable(){
//前提是该类被调用加载入内存中的串池
//StringTable["a","b","ab"]
String s1 = "a"; //当读取到此行代码时,最开始串池中不存在a 进行加载
String s2 = "b"; //当读取到此行代码时,最开始串池中不存在b 进行加载
String s3 = "ab"; //当读取到此行代码时,最开始串池中不存在ab 进行加载
// new StringBuilder().append("a").append("b").toString(); toString()底层--->new String("ab")
String s4 = s1+s2; //注意此处是变量
// s5="ab"
String s5 = "a"+"b"; //注意此处是常量
//问题1:此时s3==s4吗?
System.out.println(s3==s4);
//问题2:此时s3==s5吗?
System.out.println(s3==s5);
//答案是false
//问题1解析:s3虽然在被执行的时候已经被串池所加载,通过反编译可以知道s4在执行的时候是创建了StringBuilder对象,调用两次append,将串池中的a,b取出进行拼接,再进行toString(),关键在toString,经过看toString的方法可以知道,底层是new了一个String对象,将拼接的结果传入,由于是new出来的东西就一定会进入堆内存当中重新分配一个地址值,所以与串池当中的地址值就不会相同,故false
//答案是true
//问题2解析:在执行s3的时候,已经把ab加载入串池当中。通过反编译可以知道在执行到s5的时候编译器直接编好了一个a+b的结果ab,而不是在执行的时候对a+b=ab进行字符串拼接,所以会去池子中找到已经存在的ab取出给s5赋值。故答案时true。
//可是为什么在执行s4的时候是使用的StringBuilder拼接,s5就是直接拼接好了呢?原因是因为虚拟机会判定该常量已经不会有任何的变动,其结果必定为ab(javac命令在编译期的优化),而s4是通过变量进行赋值的,变量是有可能会变化的,所以s4和s5的执行过程会不一致。
//问题3:假若串池中为ab,此处的结果是什么,此时串池中有什么?
//答案:false,["a","b","ab"] 解析:new出来的东西进堆内存
String s = new String("a")+new String("b"); // new String("ab")
System.out.println(s=="ab");
}
StringTable(串池)特性
- 常量池中的字符串仅是符号,当第一次被加载到的时候,字符串才会池化
- 池化机制,可保证每个字符串在串池中是唯一的。避免重复创建对象。
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译器优化,会提前识别最终拼接结果。
- 字符串intern()方法,将池中没有的字符串进行池化
intern特性
1.8和之前是不一样的。
1.8特性:放入成功–>调用者同化, 不成功—>调用者不同化,依然是堆内存的地址值
1.7特性:调用者不管放入成功与否都不会进行同化。
intern方法定义:将调用对象尝试放入池中,判定池中是否存在
若池中没有,返回放入后该值在池中对应的地址值,同化调用者的地址值
有, 返回池中的原有值的地址值,不同化调用者的地址值
String a ="ab";
String s = new String ("a")+ new String("b");
String s1 = s.intern(); //intern方法的定义:将字符串尝试放入池中,池中有则返回池中对象,池中没有则添加
System.out.println(s==a); //f
System.out.println(s1==a); //t
//问题4:下面两种情况,结果分别是什么?
//情况1:池中没有ab
String s = new String ("a")+ new String("b"); // new String (ab)
String s1 = s.intern(); //将new String尝试放入池中
System.out.println(s=="ab");
System.out.println(s1=="ab");
//情况2:池中有ab
String a = "ab"
String s = new String ("a")+ new String("b"); // new String (ab)
String s1 = s.intern(); //将new String尝试放入池中
System.out.println(s==a);
System.out.println(s1==a);
//解析,关键在于看调用者会不会被同化。
//情况1:开始的时候池中是没有值的[],由intern特性,可以成功放入池中,返回ab在池中的地址值给s1且同化自身s的地址值,所以s=="ab"
//由于返回的s1就是池中的地址值,所以也s1=="ab"
//情况2:开始的时候池中值["ab"],,由intern特性,不会成功放入池中,但是依旧返回ab在池中的地址值给s1,但是不会对s进行同化,所以s的地址值依旧是堆内存中的地址值,所以s!=a
面试题
//[a,b,ab]
String s1="a";
String s2="b";
String s3="a"+"b"; //ab
String s4=s1+s2; // new String (ab)
String s5="ab"; //ab
String s6=s4.intern(); //放不进,不同化
System.out.println(s3==s4); //f
System.out.println(s3==s5); //t
System.out.println(s3==s6); // t
String x2 =new String("c")+new String("d"); // new String (cd)
String x1 ="cd"; //[cd]
x2.intern();
System.out.println(x1==x2); //f
StringTable位置
从JDK1.7开始,从永久代,改到了堆内存中,因为永久代在使用StringTable的时候效率太低
StringTable垃圾回收机制
当池中存在很多未被引用的字符串,导致内存紧张的时候会触发垃圾回收机制对未被引用的对象进行回收,但是如果在内存不紧张的时候,垃圾回收是不会被触发的。
StringTable性能调优
StringTable是按照哈希表进行排列的,哈希表是由数组+链表组成的,哈希表的性能高低是根据其大小(即桶的多少)来确定的,越大越快,就相当于元素更加分散,链表短。哈希冲突的几率就越小,查找速度就会变快。反之越小就会导致链表的长度越长,根据其特性查找就会越慢。
其他知识点
-
CPU占用过高
由于后期集成是在Linux中进行,故对Linux进行操作
-
使用top定位哪个进程对CPU占用过高
-
ps H ep pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪一个线程导致CPU使用过高)
-
jstack 进程id (根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
注:thread开头是用户的线程。
线程编号是十六进制,所以要将Linux的线程编号进行转换再查找用户线程中的nid查看是否一致。找到一致的那个线程,他的下方就是状态为runnable,再下方就是出错的代码行数
-
视频地址
https://www.bilibili.com/video/BV1yE411Z7AP?p=15
至此结束看到P14章节
-
使用top定位哪个进程对CPU占用过高
-
ps H ep pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪一个线程导致CPU使用过高)
-
jstack 进程id (根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
注:thread开头是用户的线程。
线程编号是十六进制,所以要将Linux的线程编号进行转换再查找用户线程中的nid查看是否一致。找到一致的那个线程,他的下方就是状态为runnable,再下方就是出错的代码行数
视频地址
https://www.bilibili.com/video/BV1yE411Z7AP?p=15
至此结束看到P14章节