我的JVM-学习笔记

JVM原理:

Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

JVM执行程序过程:
1.加载.class文件
2.管理并分配内存
3.执行垃圾收集

完成JVM环境:

1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂载到JNIEnv(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类

JVM的生命周期:

JVM实例和JVM执行引擎实例:
JVM实例对应了一个独立运行的java程序--进程级别
JVM执行引擎实例对应了运行程序的线程--线程级别
①当启动一个Java程序时,一个JVM实例就产生了.
②main()作为该程序初始线程的起点(任何其他线程都由该线程启动)--JVM内部有两种线程:守护线程和非守护线程,main()就属于非守护线程
③当程序中所以非守护线程都终止时,JVM才退出.

类的加载机制:

过程
将java编译以后的class文件读入到内存中,然后在堆区中创建一个java.lang.Class对象,用于封装类在方法区内的数据结构.

目的
封装类在方法区的数据结构,向java程序员提供访问方法区数据的接口。


生命周期:加载——>连接——>初始化——>使用——>卸载

加载(主要完成三件事)
①通过类的全限定名来获取定义此类的二进制字节流
②将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
③在堆中生成一个代表此类(java.lang.Class)的对象,作为方法区数据结构的接口

连接(分为三个阶段)
①校验:对class文件包含的信息是否符合jvm规范。
{具体是值对文件格式,元数据,字节码,符号引用的验证来完成的}
②准备:为类变量分配内存,将其初始化为默认值
③解析:把类型中的符号引用转换为直接引用
{具体四种:类或接口的解析,字段解析,类方法解析,接口方法解析}

初始化(有5种方法)
执行类的构造器方法的过程
{ 1.调用new方法
2.使用Class类的newInstance方法(反射机制)
3.使用Constructor类的newInstance方法(反射机制)
4.使用Clone方法创建对象
5.使用(反)序列化机制创建对象
}

使用
在完成初始化后就可以对类进行实例化,在程序中使用。

卸载
当类被加载,连接,初始化后,它的生周期就开始了,当代表类的class对象不再被引用时,class对象就会结束生命周期,类在方法区内的数据就会被卸载。

 

Java中的内存分配(5个内存空间):
VM栈:存放局部变量(线程私有,生命周期和线程一致)
每调用一个方法,VM就会创建一个栈指针保护当前方法的状态,并将其压入栈中,当被调用的方法完成后,再将其出栈继续执行未完成的方法。
StackOverflowError:线程请求的栈深度大于虚拟机所允许的圣都
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

本地方法栈:主要用于VM的Native方法。本地方法栈:主要用于VM的Native方法。

Java堆:存放所有new出来的东西(对象实例和数组)

方法区:
该区域是所有线程所共享的。
主要用于存放被虚拟机加载的类信息,常量,静态常量等。
垃圾回收器对这块区域的回收主要针对常量池和类的卸载。

程序计数器:机组中所说的pc。用来存放当前指令执行的地址。(线程私有,唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域)

★ 程序计数器、虚拟机栈、本地方法栈属于线程私有
Java堆、方法区、直接内存(不受JVM GC管理)属于线程共享

JVM各区域潜在异常:
程序计数器--唯一一个不存在内存溢出的区域
Java栈,本地方法栈:
StackOverflowError:栈深度大于虚拟机所允许的深度
堆:内存溢出,堆无法扩展
方法区:内存溢出


对象的生命周期:
1.创建阶段

对象至少被一个强引用持有着
2.应用阶段

3.不可见阶段
当对象处于不可见阶段时,说明程序本不再持有该对象的任何强引用(虽然这些引用依然存在)。
简单说就是程序的执行已经超过了该对象的作用域
4.不可达阶段
对象处于不可达阶段是指该对象不被任何强引用所持有。 与不可见相比,不可见是指不再持有但依然可能被JVM等系统下某些已装载的静态变量或线程或JNI等强引用所持有着,这些特殊的强引用被称为GC root,。(存在着这些GC root会导致对象的内存泄漏无法被回收。)
5.收集阶段
当垃圾回收器发现对象已经处于不可达阶段并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了收集阶段。如果该对象重写了finalize()方法,则回去执行终端操作。
★不要重载finalize()方法。
①会影响JVM的对象分配和回收速度。
因为在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收的时候能执行重载方法;该方法执行时需要消耗CPU时间且在执行完该方法后,才会重新执行回收操作,所以需要垃圾回收器对该对象执行两次GC。
②可能造成该对象的再次"复活"
在finalize()方法中,如果有其他的强引用再次持有该对象,则导致对象的状态由收集态重新变为应用阶段,破坏了java对象的生命周期进程,不利于后续的代码管理。
6.终结阶段
当对象执行完finalize()方法后,仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象的空间进行回收。
7.对象空间重分配阶段
垃圾回收器对该对象所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为对象空间重新分配阶段。


Java四种引用的特点:
1.强引用:
如果一个对象具有强引用,那垃圾回收器不会回收它,当内存不足时,Java虚拟机宁愿抛出OOM错误,使得程序异常终止,也不会考随意回收具有强引用的对象来解决内存不足的问题。

2.软引用

软引用是来描述一些还有用但并非必须的对象。
所以当对于软引用关联着的对象,系统将在要发生内存溢出异常之前,把这些对象列进回收范围进行第二次回收,如果这次回收后还没足够的内存,才会抛出内存异常。

3.弱引用
和软引用一样也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果它只被弱引用关联,那么这个对象就会被回收。
(弱引用关联的对象是否回收取决于这个对象有没有其他强引用指向它。)
应用场景:
WeakHashMap类:
如果有一个值,对应的键已经不再使用。假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了,但由于程序中的任何部分没有再出现这个键,所以这个键/值对无法从映射中删除。为什么垃圾回收器不能够删除它呢?
因为垃圾回收器跟踪活动的对象,只要映射对象是活动的,其中的桶也是活动的,它们就不能被回收,因此,需要由程序负责从长期存活的映射表中删除那些无用的值,或者使用WeakHashMap完成这件事。当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
机制内部运行情况:
WeakHashMap使用弱引用(weak references)保存键。
WeakReference对象将引用保存到另一个对象中,在这里就是散列键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。 (通常垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中,WeakHashMap将周期性的检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来了。于是WeakHashMap将删除对应的条目)。
除了WeakHashMap使用了弱引用,ThreadLocal类中也使用了弱引用。

4.虚引用
一个对象是否由虚引用的存在,完全不会对其生存时间构成影响,也无法用过虚引用来获取一个对象的实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
虚引用和弱引用对关联对象的回收都不产生影响,如果只有虚引用或者弱引用关联着对象,那么这个对象就会被回收。它们不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
虚引用在回收前会被放入ReferenceQueue中(其他引用是被JVM回收之后才被传入ReferenceQueue中)

JVM垃圾回收(gc只发生在线程共享的区域,大部分发生在heap(堆)上):
GC的基本原理:
将内存中不再被使用的对象进行回收,按照新生代、旧生代的方式对对象进行收集,尽可能缩短GC对于应用造成的暂停。
★方法区如何判断是否需要回收:
方法区主要回收:废弃常量和无关的类
对于废弃常量可通过可达性来判断
对于无关的类则满足三个条件:
该类所有的实例都已经被回收
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反色和访问该类的方法.

垃圾回收主要算法:
1.引用计数法
通过引用计数来判断对象是否可以回收。
对于一个对象A,只要任何一个对象引用了A,A的引用计数器就加1,当引用时效时计数器就减1.所以只要引用计数器的值为0,就不再被使用——被垃圾回收器回收了。

2.标记清除算法
标记清除算法分为两个阶段:标记阶段,清除阶段
标记阶段:首先通过根节点开始标记所有较大的对象。而未被标记的对象则被认为是未被引用的垃圾对象。
清除阶段:清除所有未被标记的对象。
缺点:会存在大量的空间碎片。因为回收后的空间不是连续的。
3.复制算法
将现有的内存分为两块,每次只是用一块。当进行垃圾回收时,把正在使用的块中活跃的对象复制到另一块内存中,然后清除这一块。交替使用完成垃圾回收。
注:如果系统中垃圾对象很多,则该算法需要复制的对象不是很多,因此在需要回收时,该算法的效率很高。又因为回收时是将一个内存块清除,所以不会产生空间碎片。
缺点:将系统内存折半。
{
Java的新生代串行垃圾回收器中使用了复制算法的思想.
新生代分为eden空间、from空间、to空间。
8 : 1 : 1
from空间和to空间可以视为两份相等空间。(也被称为survivor空间,用于存放未被回收的对象)
过程:
垃圾回收时eden中的存活对象会被复制到未使用的survivor空间中,(这里假设是to空间),而正在使用的survivor空间(这里就是from空间)中的年轻的对象也会被复制到to空间中(未使用的survivor空间中)(大对象、老年对象会直接进入老年代。如果to空间满了,则对象也会进入老年代)。这时eden和from中的对象被清理。
这种改进的方法保证了空间的连续性,而且避免大量浪费空间。
}
4.标记整理算法
结合了标记-清除和复制两个优点。
第一阶段标记所有被引用对象。
第二阶段清除未标记对象,并将存活对象压缩到堆的其中一块,按顺序排放。
5.分代收集算法
对象分类:
新生代:朝荣夕灭的对象——方法的局部变量引用的对象等
老年代:存活的比较久,但还是会死的对象——缓存对象,单例对象
永久代:生成后几乎不死的对象——加载过的类信息
内存区域:
新生代和老年代在Java堆,永久代在方法区。
回收方法:采用复制算法的思想
新生代与老年代的比例1:2
垃圾回收器:Serial收集器,ParNew收集器,Paralle收集器,Paralle Old收集器,Cms收集器,G1收集器。
降低GC的调优的策略: 关键参数:-Xms、-Xmx、-Xmn、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize。
-Xms、-Xmx:通常设置相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM堆所能使用的最大值.
-Xmn:决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4 表示:Eden:S0:S1 = 4:3:3 )

-XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。

-XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。
{1.避免新生代大小设置过小
minor GC次数频繁
导致minor GC对象直接进入老年代.(当老年代内存不足时,会触发Full GC)
2.避免新生代大小设置过大
老年代变小,导致Full GC频繁执行
minor GC执行回收的时间大幅度增加
}
-Xmx3550m:设置JVM最大可使用内存为3550M
-Xms3550m:设置JVM促使内存为3550M(此值可以设置与-Xmx相同,以避免每次垃圾回收完后JVM重新分配内存)

-Xmn2g:设置年轻代大小为2G(整个堆的大小=年轻代+老年代+持久代)
这里持久代一般设为64m,推荐老年代年轻代配置3/8。

-Xss128k:设置每个线程的堆栈大小。
JDK5.0以后每个线程堆栈大小为1M,以前为256k。
操作系统对于一个进程内线程数还有有限制的,不能无限生成,经验值在3000

-XX:+UseParallelOldGC
配置老年代垃圾收集方式为并行收集

-XX:+UseAdaptiveSizePolicy
设置此选项后,并行收集器会自动选择年轻代大小和对于Survivor区比例

-XX:MaxGCPauseMillis=100
设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,来满足此值。

等等....

JVM调优:
1.初始化内存和最大内存尽量保持一致,避免内存不足继续扩充内存。最大内存不要超过物理内存。否则在加载类的时候没有空间会报错。

2.gc/full gc频率不要太高,每次gc时间不要太长,根据系统应用来定。

jps:查看所有的jvm进程,包括进程ID,进程启动的路径等等。
jstack:观察jvm中当前所有线程的运行情况和线程当前状态。
jstat:利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控(包括了进程的classloader,compiler,gc情况)
jmap:监视进程运行中的jvm物理内存的占用情况/
jinfo:观察进程运行环境参数,包括Java System属性和jvm命令行参数。


Java IO/NIO
阻塞IO模型
在读写数据过程中发生组赛现象。当用户线程发出IO请求后,内核会去查看数据是否就绪,如果没有就绪就会等待就绪,而用户线程就会处于组赛状态,用户现场交出CPU后。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才会解除block状态。
非阻塞IO模型
当用户线程发起一个read操作时,不需要等待,而是马上就能得到一个结果。当结果error时,它知道数据还未准备好,于是再次发送read操作。一旦内核中的数据准备好了,并且再次收到请求,它就马上将数据拷贝到用户线程。
(因为这时候用户线程需要不停的向内核请求数据,所以它不会释放CPU,导致CPU的占用率非常高。)
多路复用IO模型
会有一个线程不断轮询多个socket的状态,只要当socket真正有读写时,才真正调用实际的IO读写操作。
因为在多路复用IO模型中,只需要一个线程就可以管理多个socket,不需要系统建立新的进程或线程。
因此多路复用IO比较适合连接数较多的情况。
(多路复用IO中每个socket状态是在内核进行的,而非阻塞IO是通过用户线程进行的。所以多路复用IO快)
(多路复用IO对到达的事件逐一进行响应,但事件响应体很大时,会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询)
异步IO模型
最理想的IO模型。但用户发起read后,立刻就可以去做其他事。另一方面,从内核角度,当收到一个异步read时,它会立刻返回,说明已经成功了。不会对用户线程产生任何阻塞。用户线程和内核互不干预。
但异步IO需要操作系统的底层支持。Java7有提供。

 2020-10-20 12:51:57

 

posted @ 2020-10-20 12:53  菠菜面筋  阅读(91)  评论(0编辑  收藏  举报