- 1、JVM探究?
- 2、JVM的位置?
- 3、JVM的体系结构?
- 4、类加载器
- 5、双亲委派机制
- 6、沙箱安全机制
- 7、Native
- 8、PC寄存器(Program Counter Register)
- 9、方法区(Method Area)
- 10、栈(Stack)
- 栈、堆、方法区 关系
- 11、三种JVM
- 12、堆(Heap)
- 13、新生区、老年区、永久区
- 14、堆内存调优
- 15、GC
- 引用计数器
- (1)复制算法
- (2)标记清除算法
- (3)压缩清除算法
- 16、JMM
几道面试题:
1、请你谈谈对jvm的理解?java8的虚拟机和之前的变化更新?
2、什么是oom?什么是栈溢出(StackOverflow) ?怎么分析?
3、jvm常见的调优参数有哪些?
4、内存快照如何抓取,怎么分析Dump文件?
5、谈谈jvm中,类加载器你的认识?
可以在cmd中输入java -version ,查看自己虚拟机的版本
JVM的调优是针对于方法区和堆 ;而大部分的调优针对于堆
JVM架构图
作用:类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
package com.gd.Jvm;
public class Car {
//类是模板,对象是具体的
//进入JVM之后,先进入类加载器ClassLoader-> 然后初始化,得到Car类的Class对象
public static void main(String[] args) {
Car car1=new Car();
Car car2=new Car();
Car car3=new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
//不同的对象,对应的Class类对象是一致的
Class extends Car> aClass1 = car1.getClass();
Class extends Car> aClass2 = car2.getClass();
Class extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
类加载器:
- 虚拟机自带的加载器
- 启动类加载器(根加载器)
- 扩展类加载器
- 应用程序加载器(系统类加载器)
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader.getParent());//sun.misc.Launcher$ExtClassLoader@1540e19d 在jdk/jre/lib/ext 中
System.out.println(classLoader.getParent().getParent());//null (1)不存在 (2)java程序获取不到
应用程序加载器获取到的位于rt.jar 包下的java.lang 包下的ClassLoader 类
扩展加载器位于在jdk/jre/lib/ext 中
创建一个java.lang包,创建String类
重写toString方法,调用toString方法,出现异常
App -> Exc -> BooT(最终执行)
如果向上找,没找到对应的加载器,又会向下走,回到应用程序加载器
package java.lang;
public class String {
@Override
public String toString() {
return "toString";
}
public static void main(String[] args) {
String string =new String();
String s = string.toString();
System.out.println(s);
System.out.println(s.getClass().getClassLoader());
}
}
eg:再次创建一个包,创建一个类,类名定义为Student ,不与java定义的类重名
package com.gd.Jvm;
public class Student {
@Override
public String toString() {
return "studentString";
}
public static void main(String[] args) {
Student student =new Student();
String s = student.toString();
System.out.println(s); //studentString
System.out.println(s.getClass().getClassLoader());//null
new Thread().start();
}
}
线程类调用start方法,线程级别的java无法处理,就调用本地方法start()
作用:防止恶意代码污染java源代码
守护了被信任的类库边界
将代码归入保护域,确定了代码可以进行哪些操作
比如我们定义了一个类名为String所在的包名为Java.lang,因为这个类本来属于jdk的,如果没有沙箱安全机制,这个类将会污染到系统中的String ,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器。但是由于String就是jdk源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的String ,后面的一概不能使用,这就保证了不被恶意代码污染。
package com.gd.Jvm;
public class Demo {
public static void main(String[] args) {
Demo demo =new Demo();
demo.test1();
}
public void test1(){
test2();
}
public void test2(){
test1();
}
}
栈溢出
1、会进入本地方法栈,调用本地方法接口 JNI
2、JNI作用:扩展Java的使用,融合不同的编程语言为java所用,最初为C 、 C++
Java诞生的时候,C 、C++ 横行,想要立足,必须要有调用 C 、C++的程序,它在内存区域中专门开辟了一块标记区域:Native Method Stack 用来登记native方法
3、在最终执行的时候,加载本地方法库中的方法通过JNI
4、Java程序驱动打印机,管理系统,目前该方法使用的越来越少了,在企业级应用已经比较少见。
5、现在的异构领域间通信很发达,调用其他接口:socket 、 http…
package com.gd.Jvm;
public class Demo1 {
public static void main(String[] args) {
new Thread(()->{
},"hello").start();
}
private native void start0();
}
凡是带了native关键字的,说明java的作用范围达不到了, 回去调用底层c语言的库
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也指向要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
pc寄存机是jvm中唯一一个不会栈溢出的
方法区:static .final.Class .常量池… 运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区是所有线程共享的,所有字段和方法字节码,以及一些特殊方法
如:构造方法、接口代码;所有定义的方法的信息都保存在该区域,此区域属于共享空间。
public class Test {
private int a;
private String name="jvmstudy";
public static void main(String[] args) {
Test test =new Test();
}
}
10、栈(Stack)
1、栈是一种数据结构(后进先出),队列是先进先出的数据结构
2、为什么main方法是最先被执行的,但是最后结束?
main方法最先被压入栈,最后出栈
3、栈的别名:栈内存;主要管理程序的运行;生命周期与线程同步,线程结束,栈内存即被释放,
不存在垃圾回收的问题
4、栈里面存放的就是8大基本数据类型以及对象的引用以及实例方法
5、栈帧:是一片一片的…;压入一个方法入栈,就是栈帧
stack2:;先进来被调用
正在执行的方法,是位于栈顶的
我们普遍使用的是sun公司提供的HotSpot
BEA
IBM的J9VM
一个JVM只有一个堆内存,堆内存的大小是可以调节的
Car car =new Car(); Class extends Car> aClass =car.getClass(); //类加载器 ClassLoader classloader = aClass.getClassLoader();
类加载器读取了类文件后,将类,方法,常量 引用类的真实对象,存入堆中,堆划分为新生区,老年区,永久区
在新生区中进行垃圾回收,轻GC,经过伊甸园区随后进入到幸存0区,然后到幸存1区。
如果新生区满了,则进入老年区,重GC
如果老年区满了,则进入永久存储区
最终,内存满了,就会出现OOM,出现堆内存溢出的错误
- eg:永久循环,造成了OOM的错误
注意:一般对象都是临时变量
永久代:存放class对象,变量等,不存在垃圾回收
在jdk1.8之后,永久存储区更改为元空间:逻辑存在,物理不存在
方法区里面为常量池
程序OOM了,该怎么办?:
(1)扩大堆内存,看结果
(2)分析内存,看那个地方出现了问题(专业工具)
1、在一个项目中,突然出现了OOM故障,那么该如何排除故障,研究为什么会出错?
(1)能够看到代码第几行出错,内存快照分析工具:MAT,JProfiler
(2)Debug,一行一行的分析
MAT,JProfiler的作用:
分析Dump内存文件,
快速定位内存泄漏
2、使用JProfiler工具分析OOM原因?
(1)在idea中安装插件
…以后更新
eg:
默认: 分配的总内存是电脑内存的1/4 ; 初始化的内存 是电脑内存的1/64;
调整-Xms527m -Xmx527m -XX:+PrintGCDetails
调小看看
-Xms1m -Xmx1m -XX:+PrintGCDetails
内存效率:复制算法>标记清除>标记压缩(时间复杂度)
内存整齐度:复制算法=标记压缩>标记清除
内存利用率:标记压缩=标记清除>复制算法
GC题目:
1、JVM的内存模型和分区模型,详细到每个区放什么?
2、堆里面的分区有哪些?Eden ,from,to ,老年区 ,说说各有什么特点?
3、GC的算法有哪些?
标记清除法、标记整理、复制算法、引用计数器
4、轻GC和重GC分别在什么时候发生?
首先给每个对象都分配一个引用计数器:计数器本身也会有消耗
from->to 谁空谁是to
每次GC:都会将Eden(伊甸园区) 活的对象移到幸存区,一旦Eden区被GC后,就会是空的!
好处:没有内存碎片
坏处:浪费了内存空间:多了一半空间永远是to,假设对象100%存活【极端情况】(比较危险)
最佳使用场景:对象存活度较低的时候:新生区用复制算法
(1)扫描这些对象,对活着的对象进行标记
(2)清除:对没有标记的对象,进行清除
优点:不需要额外的空间!
缺点:两次扫描,严重浪费时间,会产生内存碎片
防止内存碎片产生,再次扫描,向一端移动存活的对象多了一个移动成本
- 难道没有最优算法吗?
- 没有,只有最合适的算法 - -> GC:分代收集算法
年轻代:存活率低,复制算法
老年代:区域大,存活率高;标记清除+标记压缩混合实现
1、什么是JMM?
Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。
2、干嘛的?
作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成
3、他该如何学习?
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置



