# 目录

  * [JVM介绍和源码](#jvm介绍和源码)
  * [JVM内存模型](#jvm内存模型)
  * [JVM OOM和内存泄漏](#jvm-oom和内存泄漏)
  * [常见调试工具](#常见调试工具)
  * [class文件结构](#class文件结构)
  * [JVM的类加载机制](#jvm的类加载机制)
  * [defineclass findclass和loadclass](#defineclass-findclass和loadclass)
  * [JVM虚拟机字节码执行引擎](#jvm虚拟机字节码执行引擎)
  * [编译期优化和运行期优化](#编译期优化和运行期优化)
  * [JVM的垃圾回收](#jvm的垃圾回收)
  * [JVM的锁优化](#jvm的锁优化)



---
title: JVM原理学习总结
date: 2018-07-08 22:09:47
tags:
	- JVM
categories:
	- 后端
	- 技术总结
---

这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍，可能会有一些错误，还望见谅和指点。谢谢

更多详细内容可以查看我的专栏文章：深入理解JVM虚拟机

https://blog.csdn.net/column/details/21960.html
<!-- more -->

## JVM介绍和源码

首先JVM是一个虚拟机，当你安装了jre，它就包含了jvm环境。JVM有自己的内存结构，字节码执行引擎，因此class字节码才能在jvm上运行，除了Java以外，Scala，groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。

## JVM内存模型

内存模型老生常谈了，主要就是线程共享的堆区，方法区，本地方法栈。还有线程私有的虚拟机栈和程序计数器。

堆区存放所有对象，每个对象有一个地址，Java类jvm初始化时加载到方法区，而后会在堆区中生成一个Class对象，来负责这个类所有实例的实例化。



栈区存放的是栈帧结构，栈帧是一段内存空间，包括参数列表，返回地址，局部变量表等，局部变量表由一堆slot组成，slot的大小固定，根据变量的数据类型决定需要用到几个slot。

方法区存放类的元数据，将原来的字面量转换成引用，当然，方法区也提供常量池，常量池存放-128到127的数字类型的包装类。
字符串常量池则会存放使用intern的字符串变量。
## JVM OOM和内存泄漏

这里指的是oom和内存泄漏这类错误。

oom一般分为三种，堆区内存溢出，栈区内存溢出以及方法区内存溢出。

堆内存溢出主要原因是创建了太多对象，比如一个集合类死循环添加一个数，此时设置jvm参数使堆内存最大值为10m，一会就会报oom异常。

栈内存溢出主要与栈空间和线程有关，因为栈是线程私有的，如果创建太多线程，内存值超过栈空间上限，也会报oom。

方法区内存溢出主要是由于动态加载类的数量太多，或者是不断创建一个动态代理，用不了多久方法区内存也会溢出，会报oom，这里在1.7之前会报permgem oom，1.8则会报meta space oom，这是因为1.8中删除了堆中的永久代，转而使用元数据区。

内存泄漏一般是因为对象被引用无法回收，比如一个集合中存着很多对象，可能你在外部代码把对象的引用置空了，但是由于对象还被集合给引用着，所以无法被回收，导致内存泄漏。测试也很简单，就在集合里添加对象，添加完以后把引用置空，循环操作，一会就会出现oom异常，原因是内存泄漏太多了，导致没有空间分配新的对象。

## 常见调试工具

命令行工具有jstack jstat jmap 等，jstack可以跟踪线程的调用堆栈，以便追踪错误原因。

jstat可以检查jvm的内存使用情况，gc情况以及线程状态等。

jmap用于把堆栈快照转储到文件系统，然后可以用其他工具去排查。

visualvm是一款很不错的gui调试工具，可以远程登录主机以便访问其jvm的状态并进行监控。

## class文件结构

class文件结构比较复杂，首先jvm定义了一个class文件的规则，并且让jvm按照这个规则去验证与读取。

开头是一串魔数，然后接下来会有各种不同长度的数据，通过class的规则去读取这些数据，jvm就可以识别其内容，最后将其加载到方法区。

## JVM的类加载机制

jvm的类加载顺序是bootstrap类加载器，extclassloader加载器，最后是appclassloader用户加载器，分别加载的是jdk/bin ，jdk/ext以及用户定义的类目录下的类（一般通过ide指定），一般核心类都由bootstrap和ext加载器来加载，appclassloader用于加载自己写的类。

双亲委派模型，加载一个类时，首先获取当前类加载器，先找到最高层的类加载器bootstrap让他尝试加载，他如果加载不了再让ext加载器去加载，如果他也加载不了再让appclassloader去加载。这样的话，确保一个类型只会被加载一次，并且以高层类加载器为准，防止某些类与核心类重复，产生错误。

## defineclass findclass和loadclass

类加载classloader中有两个方法loadclass和findclass，loadclass遵从双亲委派模型，先调用父类加载的loadclass，如果父类和自己都无法加载该类，则会去调用findclass方法，而findclass默认实现为空，如果要自定义类加载方式，则可以重写findclass方法。

常见使用defineclass的情况是从网络或者文件读取字节码，然后通过defineclass将其定义成一个类，并且返回一个Class对象，说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代，1.8以后则是元空间了。

## JVM虚拟机字节码执行引擎

jvm通过字节码执行引擎来执行class代码，他是一个栈式执行引擎。这部分内容比较高深，在这里就不献丑了。

## 编译期优化和运行期优化

编译期优化主要有几种

1 泛型的擦除，使得泛型在编译时变成了实际类型，也叫伪泛型。

2 自动拆箱装箱，foreach循环自动变成迭代器实现的for循环。

3 条件编译，比如if(true)直接可得。

运行期优化主要有几种

1 JIT即时编译

Java既是编译语言也是解释语言，因为需要编译代码生成字节码，而后通过解释器解释执行。

但是，有些代码由于经常被使用而成为热点代码，每次都编译太过费时费力，干脆直接把他编译成本地代码，这种方式叫做JIT即时编译处理，所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。

2 公共表达式擦除，就是一个式子在后面如果没有被修改，在后面调用时就会被直接替换成数值。

3 数组边界擦除，方法内联，比较偏，意义不大。

4 逃逸分析，用于分析一个对象的作用范围，如果只局限在方法中被访问，则说明不会逃逸出方法，这样的话他就是线程安全的，不需要进行并发加锁。

1

## JVM的垃圾回收

1 GC算法：停止复制，存活对象少时适用，缺点是需要两倍空间。标记清除，存活对象多时适用，但是容易产生随便。标记整理，存活对象少时适用，需要移动对象较多。

2 GC分区，一般GC发生在堆区，堆区可分为年轻代，老年代，以前有永久代，现在没有了。

年轻代分为eden和survior，新对象分配在eden，当年轻代满时触发minor gc，存活对象移至survivor区，然后两个区互换，等待下一场gc，
当对象存活的阈值达到设定值时进入老年代，大对象也会直接进入老年代。

老年代空间较大，当老年代空间不足以存放年轻代过来的对象时，开始进行full gc。同时整理年轻代和老年代。
一般年轻代使用停止复制，老年代使用标记清除。

3 垃圾收集器

serial串行

parallel并行

它们都有年轻代与老年代的不同实现。

然后是scanvage收集器，注重吞吐量，可以自己设置，不过不注重延迟。

cms垃圾收集器，注重延迟的缩短和控制，并且收集线程和系统线程可以并发。

cms收集步骤主要是，初次标记gc root，然后停顿进行并发标记，而后处理改变后的标记，最后停顿进行并发清除。

g1收集器和cms的收集方式类似，但是g1将堆内存划分成了大小相同的小块区域，并且将垃圾集中到一个区域，存活对象集中到另一个区域，然后进行收集，防止产生碎片，同时使分配方式更灵活，它还支持根据对象变化预测停顿时间，从而更好地帮用户解决延迟等问题。

## JVM的锁优化

在Java并发中讲述了synchronized重量级锁以及锁优化的方法，包括轻量级锁，偏向锁，自旋锁等。详细内容可以参考我的专栏：Java并发技术指南


