一、 整体结构
.java文件被javac编译成.class字节码文件。
.class文件再由JVM编译成设用于具体系统的机器码文件
下图即JVM编译.class文件的过程
JVM分为 类加载器, 内存结构, 执行引擎三部分
一、类加载器负责加载二进制字节码文件
二、方法区存储类, 具体的类对象实例存储在堆中,栈中存放堆中对象的引用地址, 当类对象调用方法 则会在栈中调用虚拟机栈, 程序技术器(找下一步要执行的JVM指令),本地方法栈中
三、解释器 逐行解释字节码文件
即时编译器 热点代码不需要重复编译直接存起来
垃圾回收 释放内存
重点介绍内存结构
二、程序计数器
右侧是Java源代码, 左侧是.class文件的二进制字节码 也就是JVM指令
字节码需要按行执行, 但执行地址不是有序的
因此, 程序计数器就负责记录下一条JVM指令的执行地址,让解释器直接找到
特点: 每个线程都有自己的程序计数器, 线程私有
不存在内存溢出
三、虚拟机栈
1、介绍
虚拟机栈和程序计数器一样, 也是线程私有的, 即线程运行需要开辟一块内存空间-->栈
栈帧:栈会在栈上为线程中每个方法的都分配一块空间, 成为栈帧
方法执行完栈帧释放, 线程执行完栈释放
每个虚拟栈默认分配1024KB大小内存, 也可以通过参数指定
1. 不涉及 线程执行完栈内存自动没了 GC主要涉及堆
2. 不是 太大线程数量就会受限了
3. 那得看变量是线程共享还是线程私有
因为方法局部变量主要存储在虚拟机栈中的栈帧上, 每个栈都属于单个线程, 理论上讲是线程安全, 比如基本数据类型的创建和销毁都是在栈帧上,没有问题
但当局部变量是类的实例对象就不一样了, 栈帧中只是存放对象的引用, 具体对象还是在堆上,这就不是线程安全的了
2、栈溢出
栈空间就那些, 创建太多栈帧导致不够用了 (无线嵌套)
3、线程诊断
案例一、 cpu占用过多
nohup 执行Java程序
top 查看后台进程对CPU的使用情况
通过top命令查出哪个进程对CPU占用较多, 但我们还需要定位到具体线程
可以通过ps打印线程信息, H 代表所有线程, -eo 代表只需要获取指定信息 | grep 32655 只需要获取这个进程的全部线程信息
(3)那为了去查看32665线程的具体信息, 还通过jstack 进一步查看
找到32665线程 看到 是第8行代码出现问题 问题解决
回到代码中第8行原来是死循环, CPU占用太多了
案例二、长时间没有结果
死锁了
四、本地方法栈
给本地方法运行提供内存空间, 本地方法都是拿C写的 很高级
五、堆
1、介绍
通过new 创建对象实例, 都会使用堆内存
特点: 因为是线程共享的, 所以要考虑线程安全问题
有垃圾回收问题
2、堆内存溢出
3、堆内存诊断
jconsole作为图形界面查看堆内存使用 , 更直观
五、 方法区
1.定义
两代JVM方法区的区别:
1.6代 : 方法区还在JVM内存中分配, 1.8方法区交给本地内存
1.6代 由永久代实现, 1.8由元空间实现
1.6 StringTable 被放在常量池中 1.8转移到 堆内存中存储
2、方法区内存空间溢出
加载类太多了
3. 常量池
首先需要说明JVM内存结构中的常量池指的是运行时常量池, 因为在.class文件中包含了一张常量池的表, 常量池表中存放Java源代码的各种信息 已加载的类型(每一个class文件中),都维护着一个常量池(不同于方法区的运行时常量池),里面存放着编译时期前就存在的字面值(基本数据类型String、对应的值hello world、及final修饰的变量)和符号引用(对类型、域、方法的引用);这个常量池的内容在类加载的时候,被复制到方法区的运行时常量池;池中的数据项类似数组项一样,是通过索引访问的。
4. class信息
对每个加载的类型,jvm必须在方法区中存储以下类型信息:
1.类的完整有效名;
2.直接父类的完整有效名(除非当前类型是Interface 或 java.lang.Object,两者都没有父类);
3.类型的修饰符(public,abstract, final等);
总结来说就是一个类上户口,需要知道这个类的名字叫什么、父亲是谁、有没有实现接口、 权限是什么。
5. 类加载器
jvm必须知道当前.class文件是由启动加载器加载的还是由用户类加载器加载的,如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
jvm在动态链接的时候需要这个信息,当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的,这对jvm区分名字空间的方式是至关重要的。
6、 类变量
由static修饰的变量, 这个不多说, 静态变量随着类加载而放在方法区中