JVM学习笔记总结

jvm堆大小限制的参数配置

VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOutOfMemoryError 虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照。
分析堆快照来确定是内存溢出还是泄漏。
jvm栈大小限制的参数配置
本地方法栈:-Xoss(HotSpot不可用),-Xss参数设定。如:-Xss128k
多线程内存溢出,单线程栈溢出
在硬件条件和业务要求下,只能通过减少最大堆和减少栈容量来换取更多的线程。
-------------------------------
在java语言中可以作为GC Roots的对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(即一般说的Native方法)引用的对象。
-------------------------------
引用类型:
强引用:程序代码中普遍存在的,类似于new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。SoftReference
弱引用:描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存在下一次垃圾收集发生之前。WeakReference
虚引用:幽灵引用或者幻影引用,它是最弱的一种引用。为一个对象设置一个虚引用关联的唯一目的是在被回收的时候收到一个系统通知。PhantomReference;
finalize()方法是对象逃脱死亡命运的最后一次机会。而且任何对象的finalize()方法都只会被系统自动调用一次。
------------------------------
无用的类的判定条件:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问到该类。
------------------------------
常见垃圾收集算法:
标记-清除算法
复制算法
标记-整理算法
分代收集算法
-----------------------------
枚举跟节点通过OopMap的数据结构来达到目的。
安全点的选择:是否具有让程序长时间执行的特征,即指令序列复用。
安全区域:一个范围之内对GC来说都是安全的。
-----------------------------
垃圾收集器:内存回收的具体实现。
serial收集器:用户线程--》GC线程--》用户线程
parNew收集器:serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都和Serial完全一样。
Parallel Scavenge收集器是一个新生代收集器,也是使用复制算法的收集器,又是并行的多线程收集,看上去和ParNew一样。但是它更关注系统的吞吐量。其他收集器都是为了更快的收集,而它是达到一个可控制的吞吐量。
G1将整个Java堆划分为多个大小相等的独立区域,G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region.这种使用Region划分内存空间以及有优先级的区域回收分式,保证G1收集器在有限的时间内可以获取尽可能高的收集效率。虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set
初始标记-》并发标记-》最终标记-》筛选回收
-----------------------------
运行日志,异常堆栈,GC日志,线程快照,堆转储快照。
jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstat:JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfo:Configuration Info for Java,显示虚拟机配置信息
jmap:Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
jhat:JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个http/html服务器,让用户可以在浏览器上查看分析结果。
jstack:Stack Trace for Java 显示虚拟机的线程快照
-----------------------------
jps [options] [hostid]:java进程查看命令
options:
-q:只输出LVMID,省略主类的名称
-m:输出虚拟机进程启动时传递给主类main()函数的参数
-l:输出主类的全名,如果进程执行的是jar包,输出jar路径
-v:输出虚拟机进程启动时JVM参数
----------------------------
jstat:虚拟机统计信息监视工具,它可以显示本地后者远程虚拟机进程中的类装载,内存,垃圾收集,JIT编译等运行数据。
命令格式:
jstat [option vmid [interval[s|ms] [count]]]
参数interval和count代表查询间隔和次数,如果省略,则说明只查询一次。假设需要每一分钟查询一次进程578的垃圾收集情况,一共查询5次,那命令应该是:
jstat -gc 578 60s 5
注意:关于VMID和LVMID,如果是本地虚拟机则是一致的,如果是远程虚拟机进程,VMID的格式应该是:[protocol:][//]lvmid[@hostname[:port]/servername]
对于option选项的说明:
-class:监视类装载,卸载数量,总空间以及类装载所耗费的时间
-gc:监视java堆状况,包括Eden区。两个surivor区,老年代,永久代等的容量,已用空间,GC时间合计等信息。
-gccapacity:监视内容基本与-gc相同,但输出主要关注java堆各个区域使用到的最大和最小空间;
-gcutil:监视内容基本与-gc相同,但输出主要关注已使用空间占总空间的百分比
-gccause:与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew:监视新生代GC状况
-gcnewcapacity:监视内容基本与-gcnew相同,但输出主要关注使用到的最大和最小空间;
-gcold:监视老年代GC状况
-gcoldcapacity:监视内容基本与-gcold相同,但输出主要关注使用到的最大和最小空间;
-gcpermcapacity:输出永久代使用到的最大和最小空间;
-compiler:输出JIT编译器编译过的方法,耗时等信息;
-printcompilation:输出已经被JIT编译的方法。
举例:
localhost% jstat -gcutil 577;
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  53.31  63.03   0.05  97.18  90.91      1    0.006     0    0.000    0.006
  从中可以看出:S1和Eden区的占比分别为53.31和63.03,而元数据区(1.8没有永久代)的占用是97.18.YGC发生了一次,FGC发生了0次,GC耗时0.006秒。
-----------------------------------
jinfo:java配置信息工具,实时查看和调整虚拟机各项参数
jinfo [option] pid
----------------------------------
jmap:java内存映像工具,用于生成堆转储快照heapdump.另外的两种方式是加-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,或者通过-XX:+HeapDumpOnCtrlBreak参数则可以使用ctrl+break键让虚拟机生成dump文件。
jmap的作用不仅仅是为了获取dump文件,它还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率,当前使用的哪种手机器等。
jmap [option] vmid
option的选项
-dump:生成java堆转储快照,格式为:-dump:[live,]format=b,file=<filename>,其中live子参数说明是否只dump出存活的对象。
-finalizerinfo:显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在linux和Solaris平台可用。
-heap:显示java堆的详细信息,如使用哪种回收器,参数配置,分代状况等,只有linux和Solaris平台可用。
-histo:显示堆中对象统计信息,包括类,实例数量,合计容量
-permstat:以ClassLoader为统计口径显示永久代内存状态,只有linux和Solaris平台可用。
-F:当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照,只有linux和Solaris平台可用。
--------------------------------
jhat:虚拟机堆转储快照分析工具,与jmap搭配使用,来分析jmap生成的堆转储快照
jhat 文件名称
-------------------------------
jstack:Java堆栈跟踪工具;jstack命令用于生成虚拟机当前时刻的线程快照(threaddump或者javacore文件)。主要目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源导致的长时间等待等。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做了些什么事,或者等待什么资源。
jstack的命令格式:
jstack [option] vmid
option选项:
-F:当正常输出的请求不被响应时,强制输出线程堆栈
-l:除堆栈外,显示关于锁的附加信息
-m:如果调用到本地方法的话,可以显示c/c++的堆栈
---------------------------
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数。无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info"结尾。表用于描述有层次关系的复合结构的数据。
Class文件格式:
magic:java类标志
minor_version:子版本号
major_version:主版本号
constant_pool_count:常量池大小
constant_pool:constant_pool_count-1
access_flags:访问标志
this_class:类索引
super_class:父类索引
interfaces_count:接口索引集合
interfaces:interfaces
fields_count:字段表集合
fields:fields_count
methods_count:方法表集合
methods:methods_count
attributes_count:属性表集合
attributes:attributes_count;
----------------------------
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都使用管程(Monitor)来支持的。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。
----------------------------
类加载的时机
加载Loading ->验证Verification->准备Preparation->解析Resolution->初始化Initialization
->使用Using->卸载Unloading;
其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始。
5种情况必须立即对类进行初始化:
1)遇到new、getstatic、putstatic或者invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
---------------------------
对于任意一个类,都需要加载它的类和这个类本身一同确立其在java虚拟机中的唯一性。
对于java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,是虚拟机自身的一部分;另一种就是所有其他的类加载器,独立于虚拟机外部继承自java.lang.ClassLoader
启动类加载器:这个类加载器负责将存放在<JAVA_HOME>\Lib 目录中的并且是虚拟机识别的类库加载到虚拟机内存中。
扩展类加载器:这个加载器负责加载<JAVA_HOME>\Lib\ext目录中的。
应用程序类加载器:也称为系统类加载器,它负责加载用户类路径上所指定的类库。
双亲委派模型的类加载器之间是使用组合关系来复用父加载器的代码。
在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类委派给弗雷加载器加载
2)否则,将委派列表名单内的类委派给父类加载器加载
3)否则,将import列表中的的类委派给Export这个类的Bundle的类加载器加载
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
6)否则,查找Dynamic import列表的Bundle,委派给对应的Bundle的类加载器加载。
7)否则,类查找失败。
-------------------------
tomcat存放目录和意义
1)放置在/common目录中:类库可被tomcat和所有的web应用程序共同使用
2)放置在/server目录中:类库可被Tomcat使用,对所有的web应用程序都不可见。
3)放置在/shared目录中:类库可被所有的web应用程序共同使用,但对Tomcat自己不可见。
4)放置在/WEBApp/WEB-INF 目录中:类库仅仅可以被此Web应用程序使用,对tomcat和其他应用程序都不可见。
-------------------------
JVM内存模型与线程
每秒事物处理数(Transactions Per Second TPS)是比较重要的指标;
内存间的交互操作:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
-------------------------
由java内存模型来直接保证的原子性变量操作包括read,load,assign,use,store,write;如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反应到java代码中就是同步块-synchronized关键字。
除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final.同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store,write)这条规则获得的。
而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把”this"的引用传递出去,那么其他线程中就能看见final字段的值。
--------------------------
线程的实现:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口--轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程。
从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程。
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
java线程的实现:替换为基于操作系统原生线程模型来实现。
java线程调度:协同式线程调度和抢占式线程调度。
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完成了以后,要主动通知系统切换到另外一个线程上。
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程来决定。线程的执行时间是系统可控的。
java线程的5种状态:
新建,运行,无限期等待,限期等待,阻塞,结束;
--------------------------
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获取的正确的结果,那么这个对象是线程安全的。
不可变:java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。在javaAPI中除了String之外,常用的还有枚举,Number的部分子类,如Long,Double等数值包装类型,BigInteger和BigDecimal等大数据类型;但同为Number的子类型的原子类AtomicInteger和AtomicLong则并非不可变的。


绝对线程安全:不管允许的环境如何,调用者都不需要任何额外的同步措施。javaAPI中标注自己是线程安全的类,大多数都不是绝对线程安全的。


相对线程安全:就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保证措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
线程兼容:是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。
线程对立:是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。一个线程对立的例子就是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用是否进行了同步,目标线程都是存在死锁的风险的。
---------------------------
线程安全的实现方法
互斥同步:是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section),互斥量(Mutex)和信号量(Semaphore)都是主要的互斥现实方式。因此在这四个字里面,互斥是因,同步是果,互斥是方法,同步是目的。
最基本的互斥同步手段就是synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果java程序中的synchronized参数明确指定了对象参数,那就是这个对象的reference;如果没有指定,那就看修饰的是实例方法还是类方法。
--------------------------
synchronized可重入,于ReentrantLock相比,synchronized是原生语法层面的的互斥锁,而ReentrantLock是一个API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成)。不过ReentrantLock更灵活一些,支持等待可中断,可实现公平锁,以及锁可以绑定多个条件。
--------------------------
非阻塞同步:乐观锁,首先操作数据,然后检测是否有冲突,如果没有则直接保存,如果有则重试,直到成功为止。
必须要现代机器指令来做支持:比较并交换:Compare-and-swap cas;加载链接/条件存储(Load-Linked/Store-Conditional);其中CAS操作的三个操作数,分别是内存位置,旧的预期值和新值。在操作时当且仅当内存位置上的值是旧的预期值,才去更新新的值。而且这是一个原子操作。
在jdk1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里的compareAndSwapInt()和compareAndLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了。
但是CAS没有办法解决ABA的问题,通过标记版本号来使用。
--------------------------
无同步方案:1)可重入代码:这种代码也叫做纯代码,只要输入相同,结果永远是相同的。2)线程本地存储来保证只在单个线程中操作,不会涉及到多个线程的安全性问题。ThreadLocalMap对象;
--------------------------
锁优化:
自旋锁与自适应锁,线程并不释放处理器,而是通过等待试着获取锁。而等待的时候会根据历史等待时间来调整的锁就是自适应锁。
锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进件消除。主要依据来自逃逸分析的数据支持。如果判断在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到,那就可以把它们当做栈上的数据对待,加锁就是不合适的。
锁粗化:对同一对象的多次加锁和去锁改为一次加锁和去锁,减少获得锁和释放锁的代价。
轻量级锁:在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。(1.6加入)
HotSpot虚拟机的对象头分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等这部分数据可能是32或者64位,官方称它为“Mark Word",它是实现轻量级锁和偏向锁的关键,另外一部分用于存储指向方法区对象类型数据的指针。
对象哈希,对象分代年龄, 标志位01 未锁定
指向锁记录指针         标志位00 轻量级锁定
指向重量级锁的指针      标志位10 膨胀(重量级锁定)
空,不需要记录信息      标志位11 GC标记
偏向线程ID,偏向时间戳,对象分代年龄。 标志位01 可偏向
轻量级锁执行过程:在代码进入同步代码块的时候,如果此同步对象没有被锁定(锁标志位01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的MarKWord的拷贝 Displaced Mark Word,然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功了,那么这个线程就拥有了该对象的锁。并且对象MarkWord的锁标志位将转为”00“,即表示此对象处于轻量级锁定状态。如果失败了,虚拟机首先会检查对象的MarkWord是否指向了当前线程的栈帧,如果是,则说明已经获取到锁,可以直接进入同步块继续执行,否则这个锁对象已经被其他线程抢占了。如果有两个以上的线程争用同一个锁,那轻量级锁就会变成重量级锁,锁标记位10,Mark Word中存储的就是指向重量级锁的指针。后面等待锁的线程也要进入阻塞状态。减锁过程也是通过CAS操作来进行的。如果对象的Mark Word仍然指向着线程的锁记录,那就用cas操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,则说明有其他线程尝试过获取该锁,那就要在释放锁的同时,换醒被挂起的线程。
偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序运行的性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程永远不需要再进行同步。
----------------------------

猜你喜欢

转载自blog.csdn.net/wu1226419614/article/details/80376127
今日推荐