如有问题欢迎大家指正,非常感谢!
带着问题学习!
JVM题目:
- 请你谈谈你对JVM的理解? java8虚拟机和之前的变化?
- 什么是00M,什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,你对类加载器的认识?
- …
一、JVM是什么?
JVM是Java Virtual Machine(Java虚拟机)的缩写,引入Java语言虚拟机后,Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
二、JVM的体系结构
三、类装载器
public class Car {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
//类是模板,对象是具体的实例
Car car1 = new Car();
Class<? extends Car> car1Class = car1.getClass();
Car car2 = car1Class.newInstance();
Class<? extends Car> car2Class = car2.getClass();
Car car3 = new Car();
Class<? extends Car> car3Class = car3.getClass();
System.out.println(car1Class);//class Car
System.out.println(car1.hashCode());//1163157884
System.out.println(car2Class);//class Car
System.out.println(car2.hashCode());//1956725890
System.out.println(car3Class);//class Car
System.out.println(car3.hashCode());//356573597
ClassLoader classLoader = car1Class.getClassLoader();
System.out.println(classLoader);//AppClassLoader
System.out.println(classLoader.getParent());//ExtClassLoader
System.out.println(classLoader.getParent().getParent());//null,这里属于下面第二种可
/**
* 返回值为null一般有两种可能
* 一、对象不存在
* 二、java程序获取不到(比如底层代码是c/c++写的,java就获取不到)
*/
}
}
双亲委派机制:为了保证安全 .class文件调用类加载器时,会进行以下三步(自我理解的)
1.类加载器接收到该类的请求
2.将这个请求向上委托给父类加载器,一直向上委托,直到启动类(根)加载器
3.启动类加载器检查是否能够加载这个类,能就加在加载结束,使用当前加载器, 否则,抛出异常,通知子加载器进行加载,重复步骤3 。如果都不能加载就会抛出ClassNotFound异常
用户自定义的Class Loader,继承ClassLoader—>AppClassLoader —>ExtClassLoader—>BootstrapClassLoader
四、Native
凡是带了native关键字的方法。说明java 的作用范得达不到了,会去调用起层C/C++的库
首先进入木地方法栈-------->调用本地方法本地接口(JNI)---------->调用本地方法库
JNI作用:为了扩展Java的使用,融合不同的编程语言为Java所用。
java刚诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序,所以它在内存区域中专门开辟了块标记区域本地方法栈:Native Method Stack,它的作用是登记native方法 ,它是线程私有的
常见的java调用底层代码的例子
Java程序驱动打印机,管理系统
五、PC寄存器
程序计数器: Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址, 也是将要执行的指令代码),是一个非常小的内存空间,几乎可以忽略不计。
六、 JVM栈
线程私有,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack
Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
线程结束,栈内存也就释放了,所以对于栈来说,不存在垃圾回收问题
栈里存放的一般就是: 8大基本类型+对象引用+实例的方法
栈满了: StackOverflowError(错误,很严重)
七、方法区
方法区:Method Area
方法区是被所有线程共享,所有字段和方法的字节码,以及一些特殊方法如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存放方法区中,但是实例变量存在堆内存中,和方法区无关(static final, Class, 常量池)
八、三种JVM
●Sun公司HotSpot Java HotSpot™ 64-Bit Server VM (build 25.181-b13, mixed mode)
●BEA JRockit
●IBM J9VM
一般是用的是HotSpot虚拟机,在cmd中可以用 java -version查看
九、堆(Heap)
一个JVM只有一个堆内存,堆内存的大小是可以调节的。 类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量~,保存我们所有引用类型的真实对象;
GC垃圾回收,主要是在伊甸园区和养老区~
永久区用来存放JDK自身携带的Class对象,接口元数据,也就是存储的是ava运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存。
怎么会发生OOM(OutOfMemoryError)?
一个启动类,加载了大量的第三方jar包;Tomcat部署了太多的应用;大量动态生成的反射类,不断的被加载。直到内存满,就会出现OOM;
对于永久区来说几个版本的变化
●jdk1.6及之前:永久代,常量池是在方法区;
●jdk1.7:永久代,但是慢慢的退化了,成为去永久代,常量池在堆中;
●jdk1.8及之后:无永久代,常量池在元空间。
永久区/元空间实际不存在验证
在测试的时候-Xmx最大内存设置小一点!
p
ublic class HeapTest {
public static void main(String[] args) {
long max = Runtime.getRuntime().maxMemory();//最大内存
long total = Runtime.getRuntime().totalMemory();//初始化内存
System.out.println(max + "字节\t" + (double)(max / (1024 * 1024)) + "MB");
System.out.println(total + "字节\t" + (double)(total / (1024 * 1024)) + "MB");
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails 调完之后
//1029177344字节 981.0MB
//1029177344字节 981.0MB
/**
* Heap
* PSYoungGen total 305664K
* ParOldGen total 699392K
* 新生区+老年区基本上占满了虚拟机的最大内存 981.0MB
* 所以元空间逻辑上存在,物理上不存在
* Metaspace
*/
}
}
在实际业务中出现OOM
我们就可以先扩大内存试一下如果不行就分心内存,看具体是哪个地方出现了问题
我这里使用的是Jprofiler(内存分析快照工具)
再次增加参数**-Xms1m -Xmx8m -XX:+heapDumpOnOutOfMemoryError**
public class JProfilerTest {
byte[] array = new byte[1024 * 1024];//1MB
public static void main(String[] args) {
ArrayList<JProfilerTest> list = new ArrayList<>();
int count = 0;
try {
while(true){
list.add(new JProfilerTest());
count++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
找到对应文件使用Jprofiler打开就可以查看了。
十、GC回收
GC两种类:轻GC (普通的GC),重GC (全局GC)
GC题目:
●JVM的内存模型和分区?
●堆里面的分区有哪些? Eden, form, to, 老年区,说说他们的特点!
●GC的算法有哪些?标记清除法,标记整理,复制算法怎么用?
●轻GC和重GC分别在什么时候发生?
…
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~
复制算法
对象是从Eden区new出来的,如果满了就会触发轻GC活下来的进入幸存区,然后Eden区就空了,form区和to区它不是固定的,哪个区域是空的那个区域就是to区,当一个对象存活时间超过MaxTenuringThreshold设定值就会进入老年代。
●好处:没有内存的碎片~
●坏处:浪费了内存空间-:多了一半空间永远是空的to区. 假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候:新生代
标记清除法
●优点:不需要额外的空间!
●缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩
总结:
**内存效率:**复制算法>标记清除算法>标记压缩算法(时间复杂度)
**内存整齐度:**复制算法=标记压缩算法>标记清除算法
**内存利用率:**标记压缩算法=标记清除算法>复制算法
对于GC垃圾回收没有最好的算法,只有最合适的算法。所以 GC 也叫分代回收算法
年轻代:
●存活率低
●复制算法!
老年代:
●区域大:存活率
●标记清除+标记压缩混合实现
十一、JMM
1.什么是JMM?
JMM: Uava Memory Model的缩写),Java内存模型,不存在的东西,就是一种约定。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
2.作用?
缓存一致性协议,用于定义数据读写的规则,保证了安全。
关于JMM的一些约定
线程解锁前,必须把共享变量立刻刷回主存;必须读取主内存中的最新值到工作内存中,加锁和解锁是同一把锁。
8种操作
在高并发的情况下我们需要解决可见性这个问题,因为每个线程操作完本地内存的值后会刷新(flush)回主内存,但是可能别的线程还没有读取到,就会产生一些问题。可以用volatile关键字来处理
public class JMMDemo {
private volatile static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
//结果等了1秒输出1,
//分析:线程A先执行(为了保证它先执行,所以让主线程睡一秒),程序一直在while循环里,直到一秒后,主线程执行,num=1;刷回主存,线程A立马读到了这个数据,结束while循环。
学到这里大体了解了JVM,但是需要继续深究!