蚩尤后裔 2020-04-19
文章首发我的博客,欢迎访问:https://blog.itzhouq.cn/jvm
首先基本的面试题都是下面的夺命连环问,感受一下。
这篇文章先大体梳理一下相关的知识点,后面再整理一篇基本面试题相关的,先挖个坑。要说明的是,文章中很多地方关于概念是一带而过的,难免有部分内容没有说明白。对于不明白的点,建议自己动手查查相关资料,决不能指望一篇笔记就能把 JVM 搞明白,这显然也是不可能的。
可以看到 JVM 是 JRE 的一部分。主要工作是解释自己的字节码并映射到本地的 CPU 指令集和 OS 的系统调用。Java 语言是跨平台的,不同的操作系统会有不同的 JVM 映射规则,这就使得 Java 语言与操作系统无关。
作用:加载 Class 文件,比如我们 new Student()
的时候,Student
是类,是抽象的,使用 new
关键词创建对象实例,实例的引用是在栈中,而具体的人是放在堆中。
public class Car { public static void main(String[] args) { // 类是模板,对象是具体的 Car car1 = new Car(); Car car2 = new Car(); Car car3 = new Car(); System.out.println(car1.hashCode()); // 460141958 System.out.println(car2.hashCode()); // 1163157884 System.out.println(car3.hashCode()); // 1956725890 Class<? extends Car> aClass1 = car1.getClass(); Class<? extends Car> aClass2 = car2.getClass(); Class<? extends Car> aClass3 = car3.getClass(); System.out.println(aClass1.hashCode()); // 685325104 System.out.println(aClass2.hashCode()); // 685325104 System.out.println(aClass3.hashCode()); // 685325104 } }
类加载器:
试验:自己定义一个String类,看是否能执行
package java.lang; public class String { // 双亲委派机制:安全 // BOOT --> EXT --> APP (最终执行) // BOOT // EXT // APP public String toString () { return "Hello"; } public static void main(String[] args) { String s = new String(); System.out.println(s.toString()); } // 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: // public static void main(String[] args) //否则 JavaFX 应用程序类必须扩展javafx.application.Application /** * 类加载的流程 * 1. 类加载器收到类加载的请求 * 2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器 * 3. 启动节加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器, 否则,抛出异常通知子加载器进行加载 * 4. 重复步骤3 */ }
百度:双亲委派机制
Java安全的模型的核心就是 Java 沙箱 (sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限制在虚拟机特定的运行环境中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统操作破坏。沙箱主要限制系统资源访问。
开启一个多线程启动类:
public static void main(String[] args) { new Thread(() -> { }, "my thread name").start(); }
点进去查看start()
方法的源码:
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group‘s list of threads * and the group‘s unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); // 调用start0()方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0();
可以看到源码中使用了特殊的方法 start0()
,使用了 native
关键词。
native :凡是带了 native 关键词,说明 java 的作用范围达不到了,会调用底层 C 语言的库! native 的方法会进入本地方法栈,调用本地方法接口 JNI(Java Native Interface 本地方法接口),其他的就是 Java 栈。 JNI的作用:扩展 java 的使用,融合不同的编程语言为 java 所用!最初的时候需要融合 C 和 C++。 Java 诞生的时候, C 和 C++ 横行,想要立足,必须要有调用 C 和 C++的程序。 它在内存区域中专门开辟了一块标记区域: Native Method Stack ,登记 native 方法, 在最终执行的时候,加载本地方法库中的方法通过 JNI。 比如: Java程序驱动打印机,管理系统。这部分掌握即可,在企业级应用中较为少见。 现在调用第三方语言接口的方式很多,比如:Socket、WebService、HTTP等。
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎下读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
Method Area 方法区
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。
**静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关 ** 。
static、final、Class、常量池。
栈是一种数据结构,可以形象地理解为一个水桶或水杯,其特点是先进后出。 比如,依次将乒乓球放入杯子中,先放进去的球,最后才能拿出来。
栈中存放 8 大基本数据类型 + 对象引用 + 实例的方法。
栈内存主管程序的运行,生命周期和线程同步。Java 中执行方法的过程就是调用栈的过程。为什么 main() 方法,最先执行,最后结束呢?因为main() 方法是程序的入口,执行时 main() 会最先被压到栈底, 在 main() 方法中调用其他方法时,依次将其他方法压入栈中。
线程结束,栈内存就释放了,对于栈来说,不存在垃圾回收问题。
栈的运行原理:
画一个对象实例化的过程在内存中:百度、看视频。
HotSpot
: Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
JRockit
J9VM
我们学习的都是HotSpot
。
Heap,一个 JVM 只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取类文件后,一般会把什么东西放入堆中?类、方法、常量、变量,保存所有引用类型的真实对象。
堆内存中还要细分为三个区域:
新生区中没有被垃圾收集器干掉的对象会进入幸存区0区
,幸存区0区
中没有被干掉的对象会进入幸存区1区
。幸存区0区
和幸存区1区
会不停的交换位置。经过一定次数后的垃圾回收后还没有被干掉的对象会进入养老区
,这个区域的对象一般不会被干掉,但不是绝对的。假设养老区满了,对象会进入永久存储区
。
针对新生区的垃圾回收称为轻量级的垃圾收集,也称轻 GC。针对养老区的垃圾回收称为重量级的垃圾收集,也称重 GC。
GC 垃圾回收,主要是在伊甸园区和养老区。
假设内存满了,会报错 OOM,OutOfMemeroy,对内存不够!
public static void main(String[] args) { String str = "hello world!"; while (true) { str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999); } // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space // at java.util.Arrays.copyOf(Arrays.java:3332) // at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) // at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674) // at java.lang.StringBuilder.append(StringBuilder.java:208) // at Hello.main(Hello.java:8) }
在 JDK8 以后,永久存储区改名为元空间。
新生区:
这个区域常驻内存的。用来存放 JDK 自身携带的 Class 对象。Interface 元数据,存储的是 Java 运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭 VM 虚拟机就会释放这个区域的内存。
一个启动类,加载了大量的第三方 jar 包。Tomcat 部署了太多的应用,大量动态生成的反射类。不断的被加载,知道内存满,就会出现 OOM。
public static void main(String[] args) { // 返回 JVM 试图使用的最大内存 long max = Runtime.getRuntime().maxMemory(); // 字节 // 返回 JVM 的初始化总内存 long total = Runtime.getRuntime().totalMemory(); System.out.println("max=" + max + "字节\t" + (max / (double)1024/1024) + "MB"); // max=2831679488字节 2700.5MB System.out.println("total=" + max + "字节\t" + (total / (double)1024/1024) + "MB"); // total=2831679488字节 182.5MB // 默认情况下:分配的从内存是电脑内存的 1/4, 而初始化内存是电脑内存的 1/64。 }
元空间在逻辑上存在,物理上不存在。
OOM 解决方案:
在一个项目中,突然出现了 OOM 故障,那么该如何排除,研究为什么出错?
MAT 、JProfiler的作用:
JProfiler 插件和Windows客户端安装百度。
public class Demo03 { byte[] array = new byte[1 * 1024 * 1024]; // 1MB public static void main(String[] args) { ArrayList<Demo03> list = new ArrayList<>(); int count = 0; try { while (true) { list.add(new Demo03()); count ++; } } catch (Exception e) { // 错误写法 e.printStackTrace(); } } // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space // at Demo03.<init>(Demo03.java:4) // at Demo03.main(Demo03.java:12) }
这个程序出现了 OOM,但是从报错信息无法看出哪里的问题。
此时需要添加一些配置,打印一些信息。
通过 JProfiler 工具打开文件:
总结: // -Xms 设置初始化内存分配的大小 默认1/64 // -Xmx 设置最大分配内存 默认 1/4 // -XX:+PrintDCDetails // 打印GC垃圾回收信息 // -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError OOM Dump(转储文件)
GC 的作用区域:
JVM 在进行 GC 时,并不是对这三个区域统一回收,大部分时候,回收都是新生区。
GC 的分类:
轻 GC(普通的 GC):主要针对新生区,偶尔对幸存区进行 GC。
重 GC(全局 GC):把上面所有的区域都进行 GC,也就是释放内存。
堆内存中的幸存区是可以交换位置的。
GC 的题目:
GC 复制算法:
好处:没有内存碎片;
坏处:浪费了空间内存,多了一半空间(to)永远都是空的。极端情况下,比如对象 100% 存活,这个缺点就很明显。
复制算法最佳使用场景:对象存活度较低,比如新生区。
优点:不需要额外的空间!
缺点:两次扫描,严重浪费时间,会产生内存碎片。
对标清除进行再优化。
再次优化上述算法,可以多次进行标记清除,进行一次标记压缩。
思考一个问题:难道没有一个最优的算法吗?
答案:没有,没有最好的算法,只有最合适的算法---> GC:分代收集算法
年轻代:
老年代:
参考书籍:《深入理解 JVM》
JMM:Java Memory Model 的缩写。
作用:缓存一致性协议,用于定义数据读写的规则。
JMM 定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(Local Memory)。
解决共享对象可见性这个问题: volilate
官方、其他人的博客、对应的视频。。。
关于内存图:可以去思维导图 : processon 网站搜索 JVM 可以看到别人画的相关思维导图。