diff --git "a/main/JAVA/JVM/JVM\345\206\205\345\255\230\345\210\206\345\214\272.md" "b/main/JAVA/JVM/JVM\345\206\205\345\255\230\345\210\206\345\214\272.md" new file mode 100644 index 0000000000000000000000000000000000000000..c2610b0f6f3e83fcbfdeaa6613f39a45162b5205 --- /dev/null +++ "b/main/JAVA/JVM/JVM\345\206\205\345\255\230\345\210\206\345\214\272.md" @@ -0,0 +1,88 @@ +#JVM的内存分区 +jvm自己的内存分区逻辑,是对操作系统的基础之上按照自己的使用,做了一定的划分,是为了JVM更好的管理。 +##几个划分 +不同的jvm可能会有不同的实现方法,但基本上是一致的,一些细微的地方可能会不一样。 + +* 堆 +* 栈(线程栈,方法栈) +* 静态区 + * 常量区 + * 方法区 +* 直接内存 + +###静态区 +java中要new一个对象,就像是按照一个模板去生成一个。前提是必须先将类加载到jvm中。类加载步骤是读取class文件,并对这个文件校验(是否安全,有没有被非法篡改),检查这个类的依赖并先加载依赖的类,最后生成一种类(类本身也是一种对象)。如果有一些特别的操作,比如静态的代码块,也会被执行。类加载是原子操作,一个类不可能被同一个classloader加载两次。类对象就被保存在方法区,一旦一个类载入后,就能确定存储这个类定义需要空间(类中的方法被编译后可以明确知道需要多大存储空间,属性也是。) + +在编译期间就能确定出一些常量,比如定义为final的类型(这么做应该是为了节约空间,相同的东西保存同一份就好)。还有是运行时的常量,比如`String.intern()`,直接将会放入常量区,在生成字符串的时候也会先从常量区查找,有就使用,普通的String是存放在堆中,会被回收掉。 + +---------- +#### 如何模拟出静态区的OOM +模拟OOM,首先需要将jvm使用的内存配置减少,减少模拟的时间。然后不断的往静态区增加东西,增加常量,或者增加类定义。 + +1. 不断的利用String.intern()方式。 +2. 不断的载入类,可以使用字节码生成技术不断生成。 + +在框架中大量使用字节码生成来产生增强类的技术中,必须要回收已经不再使用的类定义,否则静态区会越来越大,最后直到OOM。() + +### 栈 +栈分为线程栈,方法栈。栈可以理解为一种执行权的抽象,里面记录了执行时候需要用的一些数据。`new Thread()`生成一个线程的代价是很大的,需要调用操作系统的方法来创建,jvm需要一定的抽象来使用由操作系统创建的线程,那就是`Thread`类。每个线程单独维护一个栈,用来表示每个线程需要的信息,比如监视器锁,状态等(猜测每个线程栈的长度应该是一样的,因为是同一种类型)。而方法栈维护每个方法的信息,包括对象引用,原始类型的变量,程序指针等,不同的方法需要栈长度不同,但是一旦编译完成后,长度就可知。 + +#### 如何模拟出OOM/SOF +StackOverFlow 和 OutOfMemory,在现在的jvm中越来越像,因为栈不足往往会向堆扩展,直到堆也没有足够的空间。模拟出这个异常,也有几种方式,不断得创建线程,无限递归。 + +1. 一般不会无限得创建线程,线程越多,也就意味着更多的线程切换(上下文切换需要变化比较多的信息,比较耗时),一般使用线程池技术控制线程的数量 +2. 递归,递归的深度决定着方法栈需要的个数,比较深的递归可能有SOF的风险。而迭代就不会有这样的问题,因为迭代只需要一个方法栈就ok了。 + + +### 堆 +堆是java中存储对象最主要的区域,也是GC最主要的区域。为了提高堆的应用,jvm相应的做堆做了划分,初略可以分为新生代和老生代。老生代的大小往往会比新生代的大很多。 + +#### 堆内存分区 +* 新生代 + + 新生代是专门用来存放新创建出来的对象,这块区域的特点是对象很快就会被回收,所谓朝生夕死。也是GC发生最频繁的区域。有些还在新生代再做细分(比如Eden:Su啥的:Su啥的 8:1:1),具体看虚拟机实现。新生代的GC称为Minor-GC。一般比较快,因为空间少 +* 老生代 + + 老生代中存储的对象一般都是长久会用到的,不太容易会经常被销毁。当某些对象在新生代的岁数够久了(经历了GC后,还存在),就会进入到老生代中。有些大对象也会直接在老生代中创建,因为新生代的空间不够,一个大对象就直接沾满了。新生代在GC时,如果空间不够(比如标记-复制策略)也会占用老生代的空间。老生代的GC称为full-GC,这个时间会很长。 +#### 堆内对象定位方式 +JVM在堆上创建具体的对象,在方法栈或者线程栈上存储对象的引用。真正要使用的时候,必须通过栈上的引用定位到堆上的对象。这里存在一个问题,**因为GC的存在,对象在堆上的位置会可能会发生变化**。针对这个问题,有两个解决方式: + +1. 在GC的时候,同步更新栈上对于堆中被移动过对象的引用 +2. 在堆中的固定位置,维护一个固定的表供栈使用(栈-->表-->实际对象) + +在方法2中,将GC的变化全部封在堆上,对象移动只要更新表的地址,对于栈无感知。但是每次定位到一个对象都需要两次(先从栈到表,再从表到实际堆地址),因为查值是非常基本的操作,这个代价是非常高的。所以一般jvm都会使用方法1实现。更新栈上引用和GC上对象移动需要是一个原子操作(在没有STW的情况下) + +#### GC回收 +GC的垃圾收集器有多重实现方式,比如串行收集器,CMS(同步标识清除),G1(garbage-first)等。 + +所有的垃圾收集器实现的功能都是一样的,找出哪些对象已经不再被引用(可以消除),回收不再被引用的空间。 + +GC发生时,需要各个线程在安全区,java中没有抢占式中断,是协商式中断。设置一个gc标志位,在安全区轮训gc标志位。 + +##### 找出哪些对象已经可以回收 +jvm应该会对创建出来的对象都有登记,有类似于表的方式管理所有存活的对象。找出哪些对象还需要,然后在所有的对象中取反,就是可以回收的对象集合。 + +一般有两种方式确定对象是否还被需要 + +1. 引用计数 +2. 可达性分析 + +单一的引用计数无法解决循环引用的问题,基本上所有的GC都采用可达性分析。可达性分析的关键是要确定出开始分析的根,术语为GC-root。GC-root为栈中引用的对象,常量区的常量引用。 + +* 串行收集器: 这个是最原始的垃圾收集器。 + + 在执行回收的时候,所有的工作线程都会暂停工作。串行收集器先确定GC-root,然后遍历完(当然也会出现循环引用的情况,做一次去重就ok了),将所有没有被引用的空间回收掉。 这一系列的工作都是串行的,并且要一致完成,所以叫做串行收集器。在这基础上衍生出一种收集器,确定GC-root的时候是单线程,在遍历和回收的时候采用多线程。 +* CMS: 垃圾收集器和工作线程可以并发工作。 + + 首先是确定GC-root(阶段一),在这个阶段,也必须是STW;然后和工作线程并发的执行可达性分析(阶段二),可以多线程一起;再次STW(阶段三),弥补在阶段二中新增的对象,然后得到可清除的对象;最后和工作线程并发的清除老对象。CMS的初衷是低延时,尽量少的STW时间,但是负载很高的情况下,工程线程和垃圾收集器线程一同工作,反而会降低想要,原本10ms可以处理完的计算,现在可能就不止10ms了,20ms都有可能。 + +* G1: 目的是高吞吐量 + + 这种垃圾收集器,对内存空间做了分段,执行的过程和CMS很相似。G1把分段的内存存在优先队列中,每次在有限的时间内腾出最多的空间。 + +### 直接内存 +不受jvm管理的内存,直接由操作系统管理的内存,主要应用于Nio中传文件 + + + + diff --git "a/main/JAVA/JVM/\350\231\232\346\213\237\346\234\272\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" "b/main/JAVA/JVM/\350\231\232\346\213\237\346\234\272\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" new file mode 100644 index 0000000000000000000000000000000000000000..0ee1df213b6a6e3476f3af0712156eabeecab14d --- /dev/null +++ "b/main/JAVA/JVM/\350\231\232\346\213\237\346\234\272\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" @@ -0,0 +1,74 @@ +# 虚拟机类规范 +class文件实际上是一个进制的流文件,jvm的class规范,只是约定这个流文件应该遵守怎样的格式去描述类的信息。并没有明确规定从哪里获取。可以是从java的源文件编译出来,也可以由其他语言也可以编译成class文件。只要符合jvm制定的类规范就可以使用。所以jvm在设计之初就是跨语言的。 + +##什么时候加载类 +jvm加载类是一种懒加载模式,要用到的时候才会加载。相对全部启动时候全部加载而言,启动的时间会比较短。在以下几种场景会加载类,一个classloader只会加载一次,如果加载过了就不会重复加载。猜测是用set的结构去存储已经加载过类。 + +* 第一次使用该类,即遇到`new`,或者调用类中`static`的属性,非`final`修饰(`final`修饰的属性会被优化到常量区,但如果不加载又怎么能知道是不是final呢??) +* 利用反射方式调用。 +* 初始化子类时,父类未被初始化;则初始化父类 + +## 类加载过程 +先定位类文件,然后转换成jvm中的一种类型。需要加载类文件,验证,准备,解析和初始。因为jvm可用的class文件可以有各种各样的产生方式,不能避免恶意的修改字节码做破坏,所以jvm在类加载的时候必须做一定的验证功能工作保证安全。 + +1. 加载 + + * 从类的全限定名称获取一个二进制流(有多钟方式,比如文件,网络) + * 将流数据转换成一定的数据结构 + * 生成一个`java.lang.Class`的对象来映射刚生成的数据结构。 + +2. 验证 + * 文件格式的验证,比如魔数为oxCAFFBABE,编译版本号等 + * 元数据验证, 是否符合java的语法规范,比如父类验证(除了Object外,有且只有一个,父类不能为fianl),是否实现了接口和抽奖类的方法等 + * 字节码验证,分析数据流和控制流,做语义分析。 + * 符合引用验证(与其他类的引用关系) +3. 准备 + * 在方法区分配内存,只有类变量,对象的变量不在这个里分配. +4. 初始化 + * 执行静态块中代码 +需要所有类的准备阶段完成只有才会进入初始化的状态。 + + + public class Father{ + public static int A = 2; + { + A = 3; + } + } + + // 这里B的值为2 + public class Child extends Father{ + public static int B=A; + + } +## 类加载器 +类加载需要由类加载器完成工作。类加载器需要指定一个类加载的路径,不然茫茫人海,亲你在哪里呢。 +## 具体实现(代码) +需补充 +## 双亲委派模型 +非强制性的规定,是一种推荐实现。通过组合来实现。在加载类的时候首先委派给父类,如果父类加载不了才尝试自己去加载。 +### 这么做的好处 +1. 顶级类的统一性,是由同一个类加载器加载得到的。 +2. 安全性,由统一性来保证,比如rt.jar中class,只会有Boostrap classLoader加载得到(这一方面jvm本身就会有保护) +3. 节约内存,顶级类只会存在一个class,比如Tomcat中有sharedLoader。每个class用不同的classloader加载保存多份,是比较浪费内存的,但是为了安全,有些业务类就必须不得不这么做。 + + +## 双亲委派模型 +classloader的双亲委派模型并不是一种强制性的行为,完全由程序员自己实现。双亲委派模型靠**聚合**,而不是继承来实现的。内部含有一个`parent`的属性,在类加载的时候首先委派给`parent`去执行。**但是也完全可以自定义一个classloader不遵守这个模型** + +### jvm中内置的3中类加载器 +* Bootstrap ClassLoader 由c++代码完成,用来加载核心的jar包,比如rt.jar。 +* ExtClassLoader java代码写的,在sum的包下面,还没开始找源码的实现。主要加载jdk/ext下面的jar包 +* AppClassLoader 也是java写的。用来加载应用classloader。即程序的入口 + +### java.lang下的classloader模型 +* `abstract class ClassLoader` 定义了双亲委派模型的基础 +* `class SecurityClassLoader extends ClassLoader` 做了安全限制 +* `class UrlClassLoader extends SecurityClassLoader` 定义从哪里去加载类文件 + +ClassLoader中构造方法中决定了一定会有一个parent,如果parent为null的话,也会使用系统的classLoader作为parent,及AppClassLoader。为了使类只能加载一次,在类加载的加载类的时候必须上锁(这里可能会出现死锁的情况,A->B->A)。内部对这个优化,用一个ConcurrentHashMap来保存可以并行加载的类。可以并行加载的类就会只会获取需要加载类的锁,而不是整个classloader的锁。 + +### 为什么要有这个模型 +详见这么做的好处。 +### 什么时候需要打破这个模型 +暂时没想到,如果顶层的类也需要使用同一个类加载器,比如多项目集成中的logger? \ No newline at end of file