JDK JRE JVM
-
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
-
JRE(Java Runtime Environment)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
-
JVM(Java Virtual Machine)即Java虚拟机,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
Java类文件加载机制
所谓类加载机制就是虚拟机把class文件加载到内存,并对数据进行校验,转换解析和初始化,形成虚拟机可以直接使用的java类型,即java.lang.class
装载(Load)
查找和导入class文件
(1)通过一个类的全限定名获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在java堆中生成一个代表这个类的java.lang.class对象,作为方法区中这些数据的访问入口
链接(Link)
验证
保证被加载类的正确性
文件格式验证
元数据验证字
节码验证
符号引用验证
准备(Prepare)
为类的静态变量分配内存,并将其初始化为默认值
解析(Resolve)
把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
初始化(Initialize)
对类的静态变量,静态代码块执行初始化操作
类加载器(ClassLoader)
在装载(load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类加载器完成
双亲委派模型
在某个类加载器加载class文件时,它首先委托父亲加载器去加载这个类,依次传递到顶层加载器。如果如果顶层加载不了(它的搜索范围中找不到此类),⼦加载器才会尝试加载这个类。
双亲委派的好处:
-
每个类都只会被加载一次,避免重复加载
-
每一个类都会被尽可能的加载
-
有效避免了某些恶意类的加载(比如自定义了Java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类)
运行时数据区
Method Area(方法区)
(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建
(2)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
(3)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
Heap(堆)
(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
(2)Java对象实例以及数组都在堆上分配。
Java Virtual Machine Stacks(虚拟机栈)
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
栈帧
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
1,局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中
2,局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
3,操作数栈:以压栈和出栈的方式存储操作数的
4,动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
5,方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
The pc Register(程序计数器)
我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU
调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得
CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。
Native Method Stacks(本地方法栈)
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
折腾一下
1,栈指向堆
如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。
2,方法区指向堆
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
3,堆指向方法区
思考 :一个对象怎么知道它是由哪个类创建出来的?怎么记录?这就需要了解一个Java对象的具体信息咯。
JVM内存模型
与运行时数据区
上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从这两方面展开(注意这两块区域都是线程共享的)。对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。
图形展示
一块是非堆区,一块是堆区堆区分为两大块,一个是Old区,一个是Young区
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区
S0和S1一样大,也可以叫From和To
对象创建过程
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor
区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。
常见问题
Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代
为什么需要Survivor区?只有Eden不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
为什么需要两个Survivor区?
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置
Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor
区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,
也就导致了内存碎片化。永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
垃圾回收
如何确定一个对象时垃圾
引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
弊端 :如果AB相互持有引用,导致永远不能被回收。
可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达
什么时候会垃圾回收
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
(1)当Eden区或者S区不够用了(2)老年代空间不够用了(3)方法区空间不够用了(4)System.gc()l
垃圾回收算法
标记-清除(Mark-Sweep)
找出内存中需要回收的对象,并且把它们标记出来
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
缺点:
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制(Mark-Copying)
缺点: 空间利用率降低。
标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
垃圾收集器
Serial
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
优点:简单高效,拥有很高的单线程收集效率缺点:收集过程需要暂停所有线程算法:复制算法适用范围:新生代应用:Client模式下的默认新生代收集器
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
采用的是"标记-清除算法",只对老年代进行收集
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,
CMS收集器的内存回收过程是与用户线程一起并发地执行的。
优点:并发收集、低停顿缺点:产生大量空间碎片、并发阶段会降低吞吐量
G1(Garbage-First)
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
(1)分代收集(仍然保留了分代的概念)(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
描述一下CMS和G1的异同?
CMS只对老年代进行收集,采用“标记-清除”算法,会出现内存碎片,但是可以设置;而G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片尤其是当 Java 堆非常大的时候,G1 的优势更加明显,并且G1建立了可预测的停顿时间模型, 可以直观的设定停顿时间的目标,减少每一次的垃圾收集时间,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。