类的加载指的是将类的**.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class
对象**, Class
对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
加载-》验证-》准备-》解析-》初始化-》使用-》-》卸载
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
java.lang.Class
对象,作为对方法区中这些数据的访问入口。JVM初始化步骤
类的主动使用包括以下六种 :
Class.forName(“com.shengsiyuan.Test”)
)JavaTest
),直接使用 java.exe
命令来运行某个主类BootstrapLoader
(引导类加载器)是用C语言实现的Bootstrap ClassLoader-》ExtclassLoader-》AppClassLoader-》User ClassLoader
启动类加载器-》扩展类加载器-》应用类加载器-》自定义类加载器
注意:父类加载器并不是通过继承关系来实现的,而是采用组合实现的
Java虚拟机的角度来讲,只存在两种不同的类加载器 :
启动类加载器 : 负责加载存放在 JDK\jre\lib
扩展类加载器 : 负责加载 JDK\jre\lib\ext
应用程序类加载器 : 负责加载用户类路径(ClassPath)所指定的类
getContextClassLoader()
loader.getParent()
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
1、命令行启动应用时候由JVM初始化加载
2、通过**Class.forName()**方法动态加载
3、通过**ClassLoader.loadClass()**方法动态加载
使用ClassLoader.loadClass()来加载类,不会执行初始化块
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。Class.forName(name,initialize,loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。ClassLoader loader = HelloWorld.class.getClassLoader();
类名.class.getClassLoader()
Class.forName(类名)
loader.loadClass(类名)
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
AppClassLoader
加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。ExtClassLoader
加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。BootStrapClassLoader
加载失败(例如在 $JAVA_HOME/jre/lib
里未查找到该class),会使用 ExtClassLoader
来尝试加载;AppClassLoader
来加载,如果 AppClassLoader
也加载失败,则会报出异常 ClassNotFoundException
。JVM内存结构主要有三大块:堆内存、方法区和栈。
堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);
栈又分为java虚拟机栈和本地方法栈主要用于方法的执行
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。
老年代空间大小=堆空间大小-年轻代大空间大小
方法区和堆是所有线程共享的内存区域;
而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
是Java虚拟机所管理的内存中最大的一块。
Java堆是被所有线程共享的一块内存区域
唯一目的就是存放对象实例
几乎所有的对象实例都在这里分配内存
收集器基本都是采用的分代收集算法
所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
线程私有的
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
GC Roots包括:
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
主要缺点有两个:
一个是效率问题,标记和清除过程的效率都不高;
另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
参数控制:
-XX:+UseParNewGC
ParNew收集器
-XX:ParallelGCThreads
限制线程数量
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
参数控制: -XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
参数控制: -XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
参数控制:
-XX:+UseConcMarkSweepGC
使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量(一般情况约等于可用CPU数量)
新生代GC策略 | 老年老代GC策略 | 说明 | |
---|---|---|---|
组合1 | Serial | Serial Old | Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。 |
组合2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 |
组合3 | ParNew | CMS | 使用 -XX:+UseParNewGC 选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项 -XX:+UseConcMarkSweepGC 选项,则新生代默认使用ParNew GC策略。 |
组合4 | ParNew | Serial Old | 使用 -XX:+UseParNewGC 选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 |
组合5 | Parallel Scavenge | Serial Old | Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 |
组合6 | Parallel Scavenge | Parallel Old | Parallel Old是Serial Old的并行版本 |
组合7 | G1GC | G1GC | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例 |
lel Old是Serial Old的并行版本 |
| 组合7 | G1GC | G1GC | -XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC
#开启; -XX:MaxGCPauseMillis=50
#暂停时间目标; -XX:GCPauseIntervalMillis=200
#暂停间隔目标; -XX:+G1YoungGenSize=512m
#年轻代大小; -XX:SurvivorRatio=6
#幸存区比例 |
https://zhuanlan.zhihu.com/p/34426768