提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
jvm内存结构
1 、jvm简介
Java的虚拟机 Java的运行环境(Java二进制字节码的运行环境)
它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序
java编译器面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
1.1 好处
一次编码,到处运行
内存自动管理,垃圾回收功能
数组下标越界检查
多态
1.2 运行过程
1.java编写的.java文件;
2.javac 编译器编译后的.class文件;
3.jvm的类加载器加载.class文件,初始化JVM的堆、栈、程序计数器等;
4.java虚拟机开始执行加载的.class文件,读取JVM堆、栈的方法指令、变量、参数到JVM的程序计数器准备执行解释;
5.java虚拟机里面的解释器将程序计数器里面的指令、数据解释成操作系统能执行的机器码;
6.操作系统执行机器码,并将结果返回给JVM;
7.程序运行。
一个.class文件加载到JVM中要经过三个步骤。
1.由ClassLoader(类加载器)将.class字节码文件加载到JVM中去,等待后续过程。(此时的Class对象还不可用)
2.链接。(该步骤分为三个小步骤 (1)检查.class文件的正确性.。(2)给静态变量分配存储空间。(3)解析:将符号引用转换成直接引用)
3.初始化:对静态变量和静态代码块进行初始化工作。
补充:jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载
我们自己在idea中编写的各种类是如何被加载到jvm(java虚拟机)中去的?
2 、堆
2.1定义
· 线程共享,放对象实例和数组的地方
· 堆也是垃圾收集器管理的主要区域
2.2 堆内存溢出
· OutOfMemoryError:java heap space
·首先jsp查看下进程id
· jmap -heap 进程id(这个进程id的堆内存占用情况)
检查这个进程的堆内存的占用
· jconcle 命令打开图形界面的工具
案例
public static void main(String[] args) throws InterruptedException {
System.out.println("1....");
//等待十分钟,方便使用jconsole命令
Thread.sleep(10000);
byte[] array = new byte[1024 * 1024 * 10];
System.out.println("2......");
Thread.sleep(5000);
array = null;
System.gc();
System.out.println("3.......");
Thread.sleep(50000);
}
【jconcle在bin目录下找】
· jvisualvm 可视化监测堆内存的占用
3 、虚拟机栈
每个线程运行时所需要的虚拟机内存,成为虚拟机栈
每个线程都有自己的栈,栈中的数据都是以栈帧(stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(stack Frame) 。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
3.1 数据结构的特点
先进后出
每个线程只有对应的一个活动栈帧,对应正在执行的那个方法
3.2 一些问题
. 垃圾回收是否占用栈内存?
不会,因为栈内存时每一个方法调用时一个栈帧,当方法执行完,栈帧自动弹出,不需要垃圾回收管理
·栈内存不是越大越好
3.3 线程内局部变量的安全问题
· 静态变量:线程非安全
(重点是是否共享)
静态变量也称为类变量,属于类对象所有,位于方法区,为所有对象共享,共享一份内存,一旦值被修改,则其他对象均对修改可见,故线程非安全。
· 实例变量:单例时线程非安全,非单例时线程安全
-
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
-
单例模式有两种类型:
懒汉式:在真正需要使用对象时才去创建该单例类对象
饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
· 局部变量:线程安全
什么时候可能会得到正确的结果:
不使用共享内存,每个线程内存空间相互独立;
多线程共享一块内存区域,但是对这块共享区域加锁访问。对调用static变量的方法使用lock或synchronized
总结
怎么判断是否安全?
是否逃离方法的作用范围就是安全的,有没有可能被别的线程访问到
3.4 栈内存溢出(stackflowover)
·栈帧过多 (递归方法没有终止条件)
·栈帧过大
3.5线程运行诊断
1)cpu占用
· 用top命令定位哪个进城对cpu占用过高
· 查看线程的cpu占用最高
·jstack 进程id
把这个进程的 线程 列出来判断哪个线程cpu占的高 根据ps命令判断哪个线程占用高 然后把线程编号换算成16进制的 再进行排查 因为 jstack列出的是十六进制 ps排查到的是10进制
2)线程迟迟没有结果(也可以用jstack来排查)
可能是遇到死锁 deadlock
4.方法区
4.1 定义
·线程共享区,存储跟类相关数据。
· 方法区在虚拟机启动时候被创建
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
·静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池存在方法区中
·每个class文件都有一个class常量池
4.2 方法区内存溢出
4.3 常量池
要理解常量池是什么,先看看.class文件类的二进制字节码包含哪些信息
(类的基本信息,常量池,类方法定义,包含虚拟机指令)
4.3.1 常量池是什么
1)常量池是class文件的资源仓库(#数字 对应的都在常量池中)
就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
4.3.2 常量池作用
当解释器解释执行main方法的时候,读取到下面的11行JVM指令码0: getstatic #2
getstatic指令表示获取一个静态变量,#2表示该静态变量的符号地址,解释器通过#2符号地址去常量池中查找#2对应的静态变量
然后解释器继续向下运行,执行第13行的3: ldc #3指令,该指令的含义是:从常量池中加载符号地址为 #3 的常量
然后解释器继续向下运行,执行第15行的5: invokevirtual #4指令,该指令的含义是:执行方法,那么要执行哪个方法呢?执行常量池中符号地址为 #4 的方法。
// main方法JVM指令码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
// main方法访问修饰符描述
flags: ACC_PUBLIC, ACC_STATIC
// main方法中的代码执行部分
// ===============================解释器读取下面的JVM指令解释并执行===================================
Code:
stack=2, locals=1, args_size=1
// 从常量池中符号地址为 #2 的地方,先获取静态变量System.out
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 从常量池中符号地址为 #3 的地方加载常量 hello world
3: ldc #3 // String hello world
// 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// main方法返回
8: return
// ==================================解释器读取上面的JVM指令解释并执行================================
4.3.3 运行时常量池是什么
常量池是.class文件中的。当该类被加载它的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实的地址。即真正运行的时候把常量池中的编号1,2,3变成真正的内存地址
4.3.4 javap反编译.class字节码
查看二进制字节码文件,看看里面有什么
1)测试代码
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
2)命令
先将示例代码编译为 *.class 文件,然后将class文件反编译为JVM指令码。然后观察 *.class字节码中到底包含了哪些部分。
4.3.5 常量池与串池的关系
描述: 常量池在在运行之前只是一堆编号,当代码执行时才给这些编号分配真实的内存地址,并且时懒惰式的,执行到哪一行哪一行开始创建对象,当创建对象时会先到串池中去找,看可有,没有再放进去,有就不放了(有就直接用串池中的对象就可以)串池是哈希表的结构
最后有真实的地址是在串池中
到JDK1.8时代,方法区被移到了本地内存,而串池留在了堆中。发生这一变化的原因是,只有在full gc的时候才会触发永久代的垃圾回收,而full gc发生在老年代空间不足时,触发时间晚,但是StringTable用的非常频繁,这就间接导致StringTable回收效率不高,这样在minor gc时就可以触发StringTable的垃圾回收,减轻了字符串对内存的占用
原文链接:https://blog.youkuaiyun.com/qq_38557194/article/details/103001143
· 常量字符串拼接的底层原理
【验证一下】
public class HelloWorld {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s5 == s3);
System.out.println(s5 == s4);
System.out.println(s4 == s3);
}
}
5.本地方法栈
native方法,没有方法实现的,natve的方法实现都是用C和C++ JAVA 代码是间接的调用
给本地方法提供内存的空间
6.程序计数器
- 作用:记住下一条jvm指令的执行地址,如果没有程序计数器不知道下一条该执行哪行命令
是一行一行去解释的第一行通过解释器先解释成机器码交给cpu执行,然后第二行再去同样的操作,同时把下一条指令的地址也就是3放到计数器中,2执行完再去找3去执行,依次重复。
物理层面上是通过寄存器来实现的,寄存器是整个cpu组件中读取速度最快的单元,由于读取指令地址是频繁的,所以jvm在设定时,将cpu中的寄存器当作程序计数器,用它来存储地址,将来读取地址。
- 程序计数器特点
· 线程私有的
cpu 时间片 多线程
每个线程都有它自己的程序计数器,是线程私有的
线程多的时候cpu会分配时间片给每个线程,比如a线程执行到第8行,时间片用完了,开始去执行线程b,与此同时记住a线程只执行到哪了,等b执行结束再到a,就能知道从第9行开始,每个线程计数器都是私有的。
· 不会存在溢出问题
7、垃圾回收
7.1新生代
当创建一个对象,首先存放在eden空间,当eden满了之后,触发minor GC垃圾回收,采用可达性算法去找看这些对象是有用还是垃圾,进行一次标记的动作,标记成功采用复制算法,把存活的对象放到幸存区TO,同时让幸存的对象寿命+1,from和to交换位置(实际变得不是两块物理地址,而是指针的引用,局部变量的引用地址和物理地址俩映射关系发生了变化)
第一次回收后如果eden又满了,触发第二次minor GC ,找到eden中存活的对象,同时找到from中存活的放到to中寿命+1,再清理eden和from区,最后交换from和to的位置。(总之每次都保证最后to是空的,每次触发minor GC eden和from 都会清干净,存存活的对象年龄+1放到to里,然后交换to和from)
BUt 幸存区的对象不会永远呆着,当年龄达到一定的阈值就会被移到老年代
7.2老年代
1、新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。
2、幸存区的对象不会永远呆着,当年龄达到一定的阈值就会被移到老年代
3、如果老年代也满了,会触发full GC
整个的清理,从新生到老年代,相当于种族灭绝
8、类加载器
类加载器把字节码加载到虚拟机中就可以执行里面的字节码指令
执行的时候需要执行引擎其中的解释器来进行解释执行,解释过程中对热点代码进行运行期的编译处理,jvm是 解释+编译来运行的
8.1字节码文件
8.2图解运行流程
类加载就是把class文件中的数据读取到内存里
常量池中的数据放到运行时常量池中
方法中的字节码指令放到方法区
方法开始运行,启动main主线程,给main方法分配一个栈帧内存
然后执行引擎去读取方法区的字节码指令开始运行
左边是局部变量表 右边是操作数据栈
进行加法运算是在操作数据栈中,iload就是把数据从左边放到右边
iadd是把两个数据弹出去 结果留下
getstatic 是对象的引用
fieldref 成员变量的引用