楔子
在生产开发中,20%的 JAVA 知识,就能完成 80%的工作。但是区分高手与新手的差异往往就在那 20%,JVM 就属于这 20%的知识,本篇文章准备和大家分享 JVM 入门的相关知识,可以理解为简单的导论。
从 hello world 的运行开讲
学习一门新语言一般都是从简单的 hello world 开始,所以,咱们先准备一个 HelloWorld.java 的文件。
class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
打开终端 执行命令:
会发现目录下多了个 HelloWorld.class 文件,然后继续执行命令
输出结果
总结起来,程序的运行步骤如下:
1、首先把“.java”代码文件编译成“.class”字节码文件;
2、然后类加载器把“.class”字节码文件中的类给加载到 JVM 中 ;
3、接着是 JVM 执行类中的代码。
流程图如下:
JVM 类加载机制
提到类的加载,就离不开一个问题——JVM什么情况下会加载一个类呢?答案是使用这个类的时候。那什么时候会使用到这个类呢?一个是程序的入口main函数,一个是实例化这个对象的时候。从下面这一段代码,我们分析一下这个过程:
public class Test {
public static void main(String[] args){
UpDownloaderFactory factory = new UpDownloaderFactory();
}
}
三个步骤
(1)步骤一
JVM 进程启动之后,会加载 Test.class 到内存里;
(2)步骤二
如果有“main()”方法,则从此入口开始执行程序;
(3)步骤三
执行程序时,发现需要使用 UpDownloaderFactory 去实例化对象,所以得把 UpDownloaderFactory.class 加载到内存里面来。
四个环节
类的加载大概就是这样一个情况,当然在进行加载的过程中,我们有 4 个概念需要大概了解下。
(1)验证
这个环节主要是确定 JVM 加载的.class 文件是符合规范的,就好比男人去男厕所,女人去女厕所,符合 JVM 规范的才能进入到 JVM。
(2)准备
这个环节主要是给类以及类变量分配内存空间。
(3)解析
这个环节主要是把符号引用替换为直接引用。
(4)初始化
这个环节主要主要做的是赋值。
注:针对初始化有一个重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类。
流程图如下:
双亲委派机制
在介绍双亲委派机制的时候,不得不提 ClassLoader(类加载器)。我们可以从 java.lang 包下,找到源码入口。
ClassLoader.loadClass
/**
* 加载具有指定二进制名称的类。 此方法的默认实现按以下顺序搜索类:
* 调用 findLoadedClass(String)来检查类是否已经加载。
* 在父类加载器上调用 loadClass 方法。 如果 parent 为 null,则使用虚拟机内置的类加载器。
* 调用 findClass(String)方法来查找类。
* 如果使用上述步骤找到了该类,并且解析标志为真,则此方法将在生成的 Class 对象上调用 resolveClass(Class)方法。
* 鼓励 ClassLoader 的子类覆盖 findClass(String) ,而不是这个方法。
* 除非被覆盖,否则在整个类加载过程中,此方法会同步 getClassLoadingLock 方法的结果。
* 参数:
* name - 类的二进制名称
* 解决 - 如果为真,则解决类
* 返回:
* 生成的 Class 对象
* 抛出:
* ClassNotFoundException – 如果找不到类
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// ①首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// ②存在父加载器,递归的交由父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// ③直到最上面的Bootstrap类加载器或者Null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过源码我们可以很好的分析出什么是双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
小结:双亲委派就是向上委派,向下查找
JVM 内存区域划分
上一章我们讲了 JVM 的类加载机制,知道了类会被 JVM 加载到内存中以便使用,但是这些类被加载到内存以后,放在哪里去了呢?这个就要涉及到 JVM 的内存区域划分了。
我们以以下代码为例
class Application{
public static void main(String[] args) {
User user = new User();
user.getName();
}
}
首先我们加载了一个类,所以我们需要一块空间来存放类信息,这块空间在 JVM 中叫做元数据区(方法区)。
接着需要将.class 文件翻译成字节码,所以 JVM 使用字节码引擎,执行字节码指令,使用程序计数器,记录指令位置。
然后每一个程序计数器会对应一个线程,如果线程执行了一个方法,就会对这个方法创建一个栈帧。栈帧里包含了方法的局部变量。以上所有数据统称为 JAVA 虚拟机栈。
JAVA 虚拟机栈中的局部变量,会指向另一片区域中的对象地址,这一片存放对象的区域叫做 JAVA 虚拟机堆。
以上过程的流程图,总结如下:
JVM 分代模型
上一章我们讲了 JVM 的内存区域划分,这一章,咱们继续学习 JVM 的分代模式。先说结论,JAVA 的 JVM 一般分为:新生代(年轻代)、老年代、永久代。
通过以下代码和相关流程图,咱们先简单的认识下这 3 个分代模型。
代码:
class Application{
// 静态变量属于类
private static Man man = new Man();
public static void main(String[] args) {
getName();
// 因为在 while 死循环中,Man 的对象一直被 Application 的 man 引用着
// 所以他会存在于老年代中不会被 GC 回收
while (true){
man.createMan();
}
}
private static void createMan(){
man.createMan();
}
private static void getName(){
// 局部变量
User user = new User();
user.getName();
}
}
流程图:
小结:
(1)新生代一般是短期存在的对象。
(2)存在时间长的会进入老年代。
(3)一般情况下,新产生的对象,会存在于新生代中。
常见的 JVM 参数
说到 JVM 参数,大家的第一印象一般就是 JVM 调优,作为入门导论,该章不具体阐述如何调优,仅仅列出 JVM 最常见的基础参数,如下图所示:
注:如果您想获取更多关于 JVM 调优的知识,小七提供了一组 demo (如果能顺手给个 start 就更好啦,小七会不定时更新的~)
什么情况下对象会被垃圾回收掉
JVM 通过一种可达性分析算法来判断对象是否可以被回收,也就是说看看对象都是谁在引用他,是否有一个 GCRoots。总而言之,只要你的对象被方法的局部变量或者类的静态变量引用着,那么他就不会被回收。
新生代复制算法及其优化
通过学习前面的章节,咱们了解了 JVM 分代模型有哪些,但是大家肯定会有个疑问,JVM 为什么要这么分代呢?实际上这跟 GC 回收的算法有关,这一章,咱们就来看看新生代的垃圾回收算法。
新生代的垃圾回收算法是基于复制算法思想的。说人话就是,首先把新生代的内存分为两块,一块放存活对象,一块备用,两块空间交替使用。但是这样有个问题,比如 1G 的空间,等分成两块,那么就会有大概 500M 的空间处于空闲状态。为了减少空间的浪费,根据内存的使用频率,Java 做了优化,将空间分为了两块 Survivor 区,和一块 Eden 区。
如果 Eden 区快满了,此时就会触发垃圾回收,把 Eden 区中的存活对象都一次性转移到一块空着的 Survivor 区。接着 Eden 区就会被清空,然后再次分配新对象到 Eden 区里。如果下次 Eden 区满了,那么再次触发 GC,就会把 Eden 区和放着上一次 GC 后存活对象的 Survivor 区内的存活对象,转移到另外一块 Survivor 区去。这样 3 块内存循环使用,按 8:1:1 分配的话,90%的空间都得到了有效的利用。
以上内容,总结的流程图如下:
新生代进入老年代的条件
上一章,咱们简单的介绍了新生代垃圾回收的算法,这一章咱们讨论一下,什么情况下,新生代会进入老年代。
(1)躲过 15 次垃圾回收,就进入老年代
(2)动态年龄判断
年龄1+年龄2+年龄3+年龄N的对象加起来的空间,大于survivor区域的一半,就会让年龄N和年龄N以上的对象进入老年代。
注:动态年龄判断是年龄从小到大对象的占据空间的累加和,而不是某一个特定年龄对象占据的空间。
(3)大对象直接进入老年代
这个也很好理解,你的对象都那么大了,在新生代倒来倒去不是浪费空间吗,不如早点去老年代待着。
(4)YuangGC 后,对象太多,无法放入 survivor 区
(5)空间担保原则
如果老年代的可用内存大小是小 于新生代的所有对象的总大小的,看是否设置了参数 “-XX:-HandlePromotionFailure”( 设置空间担保)。
有这个参数,就会继续尝试下一步判断,就是看看老年代的可用内存是否大于之前每一次Young GC后进入老年代的对象总和的平均大小。剩余的存活对象的值大于survivor区的内存大小,并且小于老年代可用内存大小,那么就会进入老年代。
以上内容,小七绘制了一个流程图,方便大家记忆理解:
stop the world
如果说"hello world"代表是一个程序的开始,那么"stop the world",就代表着程序的暂停。
新生代垃圾回收器
老年代垃圾回收器
触发 FullGC 的条件
G1 垃圾回收器
本质上来说,G1垃圾回收器依然是一个分代垃圾回收器。但是它与一般的回收器所不同的是,它引入了额外的概念,Region。G1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆被划分成2048左右个Region。每个Region的大小在1-32MB之间,具体多大取决于堆的大小。
后记
这篇文章作为小七JVM学习的第一篇文章,主要整理的是一些基本概念,对于实战调优并没有涉及。一方面是JVM调优需要针对具体项目具体分析,没有一套万能配置;另一方面是小七的实战经验也的确不足,暂时就没在本篇呈现相关内容了。