1、JVM在哪

2、JVM体系结构

3、类加载器

运行流程:

分类:

  • 虚拟机自带的加载器
  • 启动类(根)加载器
  • 扩展类加载器
  • 应用程序加载器

4、双亲委派机制

原理:

  • 一个类加载器接收到了加载类的请求,它不会自己加载这个类,而是向上委托给父类加载器进行类加载
  • 如果父类加载器还存在父类加载器,则会进一步委托给父类的父类,依次向上委托直到达到顶层的启动类加载器
  • 如果父类加载器可以完成类加载任务,就成功返回;若不能完成加载任务,则向下委托子类加载器加载
  • APP–>EXT—B0OT(最终执行)

优点:

  • 避免了类的重复加载
  • 保护了程度安全

5、Native

  1. /*
  2. 凡是带native的关键字,则说明不在java的作用范围内,需要调用底层C/C++语言的库
  3. 会进入本地方法栈,调用本地方法接口(JNI)
  4. JNI:扩展java的使用,融合不同的编程语言为Java所用
  5. 在最终执行的时候,通过JNI加载本地方法库中的方法,如Java驱动打印机
  6. */
  7. private native void hello();

6、方法区

  • 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法:如构造函数、接口代码也在此定义
  • 所有定义的方法的信息都保存在该区域,此区域属于共享区间;

静态变量、常量、类信息(构造方法、接口定义)、常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

方法区与常量池:

  • 方法区存储的是class文件的信息动态常量池;class文件的信息包括类信息和静态常量池,类信息是class文件的框架,其具体内容是存储在静态常量池中。
  • 动态常量池中不仅存储了静态常量池中的数据,还将静态常量池里的符号引用转换为直接引用,且动态常量池的内容是可以动态添加的

方法区与永久代:

  • 方法区在虚拟机启动时创建,是堆的逻辑组成部分,但与堆是分开的,通常又叫“非堆”
  • 方法区与永久代像Java中的接口和类的关系,类实现了接口,方法区是接口,永久代是类,永久代是虚拟机规范方法区的一种实现方法
  • jdk1.6及之前,常量池在方法区/永久代中,方法区与堆是独立的
  • jdk1.7时永久代退化,将常量池移到了堆中
  • jdk1.8及之后,将永久代改为元空间 ,存放到了本地内存;但没有对常量池做出改动,常量池仍然在堆中

7、对象初始化过程

  • JVM先找到指定的类的字节码文件,并加载进内存;
  • 在内存中开辟一段空间,分配内存地址;
  • 在对象的内存空间中对 对象的属性进行默认初始化;
  • 如果属性有显示初始化时,开始对属性显示初始化;
  • 调用对应的构造函数进行初始化;
  • 初始化完毕后,该对象实例化完成;
  1. public class Test2 {
  2. int num = 9;
  3. String className = new String("class A");
  4. /*
  5. 空参数构造方法,如果不显示指定,会有默认的空参构造方法
  6. */
  7. Test2() {
  8. System.out.println("Test2无参构造函数");
  9. }
  10. void show() {
  11. System.out.println("show方法调用");
  12. System.out.println(num + "----" + className);
  13. }
  14. /*
  15. 实例化过程:
  16. 1. Test2.class先加载进内存;
  17. 2. new Test2()--->开辟内存空间,分配地址(假设为0x0045);
  18. 3. num和className默认初始化
  19. num = 0;
  20. className = null;
  21. 4. 显示初始化
  22. num = 9;
  23. className = new String("class A");
  24. 5. 构造函数初始化
  25. show()方法被调用
  26. 6. 对象实例化完成
  27. c = 0x0045
  28. */
  29. public static void main(String[] args) {
  30. Test2 c = new Test2();
  31. c.show();
  32. }
  33. }

8、堆

  • 一个JVM只有一个堆内存,堆内存的大小是可以调节的
  • 类加载器读取了类文件后,一般会把类、 方法、常量、变量存放到堆中,保存我们所有引用类型的真实对象;
  • 堆内存中还要细分为三个区域:新生区(伊甸园区) Young/New 养老区old 永久区Perm
  • GC垃圾回收,主要是在伊甸园区和养老区
  • JDK8之后,将永久存储区改位了元空间

9、分析OOM原因

idea中安装JProfiler插件

安装JProfiler客户端

编写一个OOM程序:

  1. //-Xms 设置初始化内存分配大小 1/64
  2. //-Xmx 设置最大分配内存 1/4
  3. //-XX:+PrintGCDetails 打印GC垃圾回收
  4. // -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError 测试OOM Dump
  5. public class Test3 {
  6. public static void main(String[] args) {
  7. Byte[] bytes=new Byte[1024*1024];
  8. List<Object> list=new ArrayList<>();
  9. //使用一个while死循环不断地向集合中添加数组,直到内存溢出
  10. while (true){
  11. list.add(bytes);
  12. }
  13. }
  14. }

更改VM参数:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

运行程序,在src目录下打开生成的.hprof文件

可以看到是char数组内存溢出

可以在JProfiler中的Thread Dump中的main线程里清楚的看到代码的第几行内存溢出

10、GC

一旦发生GC 就是将10%的from和80%的eden中存活的对象转移到to去当中 接下来将90%除to区之外的空间全部释放

10.1 引用计数法

给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时候计数器都为0的对象就是不可能再被使用的,就会被GC回收

10.2 复制算法

GC开始时,对象只会存在于Edem区和幸存区中的From区。随着GC的进行,将Eden区存活的对象和from区中没有到阈值(默认15)的对象复制到To区中,Eden和Fron区中对象清空,且Fron区和To区交换角色;始终保持To区为空(谁空谁是To);再多次GC后,如果对象达到了阈值,则会将幸存区中存活的对象移动到养老区。

优点:没有内存碎片

缺点:浪费了内存空间,To区永远的空的

复制算法最佳使用场景:对象存活度较低,新生区

10.3 标记压缩清除算法

标记清除:

  • 先将有效的对象进行标记,在GC时只清除未做标记的对象
  • 优点:不会浪费内存空间
  • 缺点:两次扫描,浪费时间,且会产生大量的内存碎片

标记压缩:

再次扫描,将所有有效对象压缩到一块,防止内存碎片产生

总结:

  • 内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
  • 内存整齐度:复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记压缩算法=标记清除算法>复制算法