jvm学习笔记-原理篇

参考文档:
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://blog.csdn.net/hellozhxy/article/details/80649342

一、jvm内存结构

1. heap堆

堆是内存管理里最大的一块,是线程共享的数据。在jvm启动时创建。
目的是存放对象实例。java的引用传递依靠的就是堆内存,例如引用类型的变量是在栈区保存一个指向堆区的指针,通过这个指针可以找到实例在堆区对应的对象。同一块堆内存可以被不同的栈内存所指向。
堆内部细分为新生代和老年代。

新生代

新生代是用于存储新生对象,分为Eden,From survior,TO survior,hotspot虚拟机中默认比例是8:1:1,当新生代存储满是会发生MinorGC。

MinorGC

对象从Young generation区域消失的过程我们称之为MinorGC。

MinorGC的过程:MinorGC采用复制算法。首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。

老年代

主要存放应用程序中生命周期长的内存对象。

MajorGC

对象从old generation区域消失的过程我们称之为MajorGC。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
MajorGC也会触发STW,STW的时间取决于老年代垃圾收集器的种类。

FullGC

full gc清理整个堆空间

2. 方法区(永久代)

方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。方法区又称为永久代,线程共享,目的是存放已被jvm加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,垃圾回收在这个区域比较少,主要目的是对常量池的回收和类的卸载。

运行时常量池

运行时常量池是方法区的一部分,用于存储编译器产生的字面量和符号引用,这类内容类加载后被存储到方法区的rcp。
在JDK8中废弃了永久代,替换为Metaspace(本地内存中)

3. JVM Stack

JVM栈是线程私有的,它的生命周期与线程相同。JVM栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表中存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象的引用类型(reference类型,不等同于对象本身,根据不同的虚拟机实现,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或者其他与对象相关的位置)。局部变量表中需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

4. 本地方法栈

与VM Strack相似,VM Strack为JVM提供执行JAVA方法的服务,Native Method Stack则为JVM提供使用native 方法的服务。

5. 程序计数器

程序计数器是一块较小的内存区域,作用可以看做是当前线程执行的字节码的位置指示器。分支、循环、跳转、异常处理和线程恢复等基础功能都需要依赖这个计算器来完成

二、Java 类加载机制

1
2
3
4
5
6
7
8
9
加载
⬇️
验证➡️准备➡️解析
⬇️
初始化
⬇️
使用
⬇️
卸载

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。

  1. 隐式加载
    程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
  2. 显式加载
    通过class.forname()等方法,显式加载需要的类

1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

类加载器

jvm通过类加载器把数据加载到内存

1.启动类加载器Bootstrp loader

Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。

2.扩展类加载器ExtClassLoader

Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader。
主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。

3.应用程序类加载器AppClassLoader

Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。
ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。

示例:

1
2
3
4
5
6
7
8
9
10
11
public class JVMTest {
public static void main(String[] args) {
ClassLoader loader = JVMTest.class.getClassLoader();
System.out.println(loader);
ClassLoader loderp =loader.getParent();
System.out.println(loderp);
ClassLoader loaderpp =loderp.getParent();
System.out.println(loaderpp);

}
}

运行结果:

1
2
3
sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null

由于Bootstrap Loader是用C++语言写的,并不存在类实体,所以打印为null。

类加载模型:双亲委派

这个模型要求除了Bootstrap ClassLoader外,其余的类加载器都要有自己的父加载器。子加载器通过组合来复用父加载器的代码,而不是使用继承。在某个类加载器加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器(Bootstrap)。如果顶层加载不了(它的搜索范围中找不到此类),子加载器才会尝试加载这个类。

双亲委派模型解决的问题:
  • 每一个类都只会被加载一次,避免了重复加载
  • 每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
  • 有效避免了某些恶意类的加载(比如自定义了Java。lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)

三、方法执行过程

四、java大版本新特性