JVM学习笔记

今年的收获还是不小的,最为一名搞开发的新人,我会为了自己的目标继续奋斗,马年最后一篇总结。

1.java内存区域和垃圾回收机制
程序计数器,本地方法区,虚拟机栈,方法区,堆
1.程序计数器(线程私有):用来记录字节码指令的执行地址,可以看作是一个字节码指令执行时的行号指示器。
2.虚拟机栈(线程私有):描述的是java方法执行的内存模型。当一个方法被执行的时候就会创建一个栈帧用来存放局部变量表,操作数栈,动态链接和方法出口,
每个方法执行的过程就是一个栈帧在虚拟机栈中出栈入栈的过程。如果超出会报OutOfMemaryError异常
3.本地方法栈:与虚拟机栈的功能相似,只不过这里可以用其他语言来实现:抛StackOverflowError和OutOfMemoryError
4.java堆:所有的对象都会在这里分配内存。(现在随着技术的发展JIT编译器和逃逸分析技术的成熟,在栈上分配也是可能的)
分为:新生代和老年代  或者 Eden空间 form Survivor空间 to Survivor空间
5.方法区:存放被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的数据,被大家称为永久代

运行时常量词:用来存放编译存放的各种常量和符号引用  运行期间也可以放如常量 比如String的intern()方法
java对象创建过程:(不包括数组和Class对象)
执行到New指令的时候,先到常量池中检查类的符号引用,如果没有被加载,就先加载类解析和初始化,加载完后分配内存。
在堆里面分配内存可以有指针碰撞和空闲列表两种方式,指针碰撞就是将内存分为可用和不可用,分配内存的过程就是把一个指针移动一点,分配即可。
因为相当于一个长条移动一样。空闲列表:就是维护一张空闲内存表,根据对象的大小来分配够大的内存即可。使用何种方式要根据使用的垃圾回收机制来区分的。


垃圾回收的方法:
引用计数器:每次被引用的时候,计数器加一,每次失效的是减一。(对于java的循环引用没法处理)
可达性分析:根据GC ROOt来向下搜索,如果某个对象不能被搜索到,证明这个对象是不可用的。(其实在进行GC的时候会标记2次,如果在这2次期间引用有找到了gc root系统会认为它不用回收放过它)
可以做为GC ROOT的对象:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(native方法)引用的对象

引用分为:强引用(Strong referernce),软引用(soft reference),弱引用(weak reference),虚引用(phantom reference)

垃圾收集算法:
标记-清除算法
先标记,然后清除。缺点效率低,回收后的空间不连续(大量碎片),再次分配较大对象是难,容易再次触发垃圾回收,导致效率更低。
复制算法:(一般用在新生代)
把内存分为2块,使用期中的一块,GC完后,因为使用的一块根据标记清除的缺点不连续,所以把活着的复制到另一块没使用的区域中,继续使用。(这样重复)
标记-整理算法:(老年代)
先标记,清除完了以后,将所有或者的移动到一端,然后根据边界,清除另一端。

枚举根节点:
安全点:safepoint GC收集的时候正在执行的线程是找安全点
安全域:safeRegion  gc收集的时候sleep,block的线程是找安全域

1.serial收集器  单线程收集器                                          新生代(运行在client模式)
2.ParNew收集器  多线程的serial收集器                            新生代(运行在server模式域CMS(老年代)配合使用)
3.parallel scavenge收集器    多线程 吞吐量=运行用户代码的时间/(用户代码时间+GC收集时间)        新生代

4.serial old收集器  是serial的老年代版本  使用标记-整理算法,主要使用在clinet模式下
5.parallel old收集器 是parallel scavenge的老年版本   标记-整理算法
6.cms收集器(并发低停顿收集器)   初始标记,并发标记,重新标记,并发清除  初始化标记,重新标记是单线程,不过时间段,  标记-清除算法
缺点:1.对CPU使用率很敏感,增量式并发收集,效果一般(采用抢占式模式和客户线程运行)
            2.收集器无法回收浮动垃圾,可能造成GC再进行一次回收,耗费时间
            3.采用的收集算法是标记-清除,会有垃圾碎片,导致性能问题

7.g1收集器(garbage first)服务器端收集器  标记-整理   将整个GC分成区域(regin)来进行管理 jdk1.7开始正式使用
初始标记,并发标记,最终标记,筛选回收

垃圾回收新生代和老年代的组合:
1.serial-cms
2.serial-serial old
3.ParNew-cms
4.ParNew-serial old(MSC)
5.paraller scvenge -serial old
6.paraller scvenge -parallel old
7.g1(也分新生代和老年代,只不过分的每个都是按集合管理,是一个域)

新生代分为:Eden和2*Survivor空间(当然使用复制算法的时候才会有)

并行:多条垃圾收集线程并行工作,但是客户线程仍然需要等待
并发:多条垃圾手机线程同时执行,用户线程在继续执行


2.虚拟机执行子系统
class文件结构:
魔数(class版本等)+常量池(各种类型的信息和字符串索引地址)+访问标志(该类的相关信息,public?等等)+类索引+父类索引+接口索引结合
+字段表集合(用来描述字段的)+方法集合+属性集合(重点)

类加载的过程:

加载(loading)->验证(verification)->准备(preparation)->解析(resolution)
->初始化(initaliztion)->使用(Using)->卸载(unloading)
解析和初始化有可能顺序发生变化。
验证,准备,解析三个过程被称为连接(linking)

<一>类加载的条件:有且只有这5种情况才会进行类加载
1.调用new,getstatic,putstatic或invokestatic这4条字节码指令,主要体现在使用new关键字,
读取或者设置静态字段(final修饰,已经在编译期间把结果放入常量池的除外)的时候,
以及调用一个类的静态方法的时候
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有进行过初始化需要初始化。
3.当初始化一个类的时候,如果发现其父类还没有初始化,首先初始化父类。
4.当虚拟机启动的时候,用户指定一个要执行的主类(包含main方法的类)
5.当使用jdk1.7的动态语言的支持时,一个java.lang.invoke,MethodHandle实例的最后解析结果是
Ref_getstatic,Ref_putStatic,Ref_invokeStatic的方法句柄

<二>类加载的过程
1.加载阶段需要完成3件事
(1)通过全限定名获取定义此类的二进制文件
(2)将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
关于获取二进制文件流的形式有很多种,如下:
(1)从zip包获取
(2)从网络中获取
(3)运行时,计算生成,动态代理技术
(4)其他文件生成,如jsp
(5)从数据库中获取

数组类的创建规则:对于类数组SuperClass [] suClassArr=new SuperClass[10];这样的定义,不会加载SuperClass类

2.验证(这个阶段可以在虚拟机执行的时候通过命令不执行)
(1)文件格式验证:这个阶段的验证主要是校验class文件二进制流,是不是按照当前虚拟机规范来定义的,验证通过就可以正常存放在方法区。
(2)元数据验证:主要是对类的元数据经行校验,保证不存在不符合java语言规范的元数据信息。类的字段,类的方法,父类什么的校验
(3)字节码验证:跳转指令是否正常跳出,类型转换是否转换正确,主要体现在具体逻辑代码的校验
(4)符号引用验证:根据全限定名是否能找到类,类中是否存在合法的字段和方法,符号引用的类的字段和方法是否可以被当前类引用
符号引用的验证主要是为了保证解析动作能够正常执行。

3.准备(为类变量分配内存,设置类变量初始值的阶段)
这里说的是类变量不是实例变量,而且这里所说的设置初始值是默认的初始值不是开发人员定义的例如
public static int value=123;
在这个阶段的初始值是value=0;而不是123.因为这个阶段还没有执行java方法,存在的类结构器<clinit>()方法在初始化的时候才会被执行。
如果类字段的字段属性表中存在ConstantValue属性,在编译生产class二进制文件的时候就会赋值。
public static final int value=123;
这样的话,在javac的时候就已经赋值了,所以是在准备阶段就已经是123了。

4.解析
解析的主要目的就是把符号引用转换直接引用,符号引用是jvm定义的一套,直接引用是根据具体的机器分配的内存的一个指针什么的。

5.初始化
对java代码的类属性和类方法的初始化。调用<clinit>()方法。


类与类加载器(双亲委派模型)
启动类加载器 bootstart classLoader
扩展类加载器 extendsion classLoader
应用程序类加载器 application classLoader
自定义类加载器 User ClassLoader
破坏双亲委派模型的例子:
jndi服务需要调用第三方服务jar包,所以在使用rt.jar加载的时候,需要加载到第三方的jar,但是默认是使用启动类加载器先加载rt.jar然后最后可能使用
应用程序类加载器加载第三方或者扩展类加载器,但是实际这样是不行的。需要在加载rt.jar的时候就加载jndi需要的第三方jar。所以使用一种线程上下文类
加载器,及在加载rt.jar的时候,如果检测出来需要第三方jar的支持的时候,启动一个子线程,调用线程上下文加载器加载第三方的jar.
所以引出了osgi热插拔的发展。

栈帧:用来支持虚拟机进行方法调用和方法执行的数据结构。(局部变量表,操作数栈,动态连接,方法返回地址)
局部变量表:用来存放方法的参数和局部变量
操作数栈:运算是基于操作数栈进行的,或者调用其他方法的时候是通过操作数栈进行参数传递的。

动态类型语言:Java c++ 需要在编译期就进行类型检查
静态类型语言:php,python,ruby等  在运行期间进行类型检查。

虚拟机执行引擎是如何工作的?(虚拟机是如何执行字节码文件的)

基于栈的指令集和基于寄存器的指令集
tomcat应该具备的特点:
1.一个web应用服务器应对使用的java类库相互隔离。
2.一个web应用服务器应对使用的java类库相互共享。
3.尽可能保证服务器的安全不受应用程序的影响。
4.支持jsp的热替换(weblogic默认不支持)

tomcat目录分析:
/commnon:类库可以被tomcat和应用程序加载   common类加载器  commonClassLoader
/server:对tomcat可以使用,其他应用程序不可见。  catalina类加载器  CatalinaClassLoader
/share:可以被web应用程序使用,但是不能被tomcat使用  shared类加载器  sharedClassLoader
程序/webapp/web-inf/下面的只能被本web应用程序使用,其他的无法使用。 webAppClassLoader   每个应用一个实例
                                                                                                                                        jasperLoader 每个Jsp文件一个实例
                                                                                                                                        
                                                                                                                                        
osgi热部署的缺点:死锁:两个加载器可能存在相互引用的情况,会出现死锁  内存泄漏:?

编译过程大致可以分为:
1.解析和填充符号表过程。 解析:词法分析和语法分析   抽象语法树
2.插入式注解处理器的注解处理过程。
3.分析与字节码生成过程。

语法糖、
JIT编译器:just in time compiler
sun公司推出的是hotspot虚拟机

java程序最初是通过解释器来进行解释执行的(就是把编译完的字节码解释掉)(解释器),但是虚拟机发现有些方法和代码运行特别频繁
“热代码”,为了提高热代码的执行效率,在运行的时候虚拟机将会把这些代码编译成与本地机器相关的机器码(JIT编译器)及即使编译器。
为了提高他们两之间的性能,在jdk1.6出现分层编译(1.7正式开启)。
分层编译:
0层:解释执行,不启动性能监控,可触发1层
1层:c1编译器(client compiler)将字节码编译成本地代码,进行简单可靠的优化,必要的情况下加入性能监控     提高编译速度
2层:c2编译器(server compiler)将字节码编译成本地代码,启动编译耗时长的优化,根据性能分析进行不可靠的激进优化。  提交编译质量

热代码:被多次调用的方法和被多次执行的循环体(这种被编译的对象,因为编译的不是一个方法,被称为是OSR编译,栈上替换)
热点探测:用来发现热代码
基于采样的热点探测:检测每个线程的栈顶,如果某个方法经常出现在栈顶就认为是热点方法  有点:简单,高效 缺点:容易收到线程阻塞的影响。
基于计数器的热点探测:为每个方法或者代码块创建一个计数器,通过计数的方式来判断一个方法和代码块是不是热点  hotspot使用
基于“跟踪”的热点探测:firefox里面用到hotspot没有用

hotspot使用的是基于计数器的热点探测,有方法调用计数器和回边计数器,当计数器超过了规定的值就执行JIT编译。


及时编译优化手段:
公共子表达式消除:对于重复出现的部分(可能是在一个局部中或者一个全局中),可以直接用同一个值取代(前提是这些表达式在计算过程中值没有发生改变)
数组边界检查消除:对于数组边界的检查放松,当然是保证没问题的情况下,提高访问速度。
内联方法:将一个方法直接变成代码放到使用它的方法里面

逃逸分析:
1.栈上分配    一个方法在没有被其他外部引用的时候,只被本地局部变量引用,使用完及可以释放,就可以将内存分配到栈里,使用完及消失。减少GC工作时间  hotspot暂时没有这项优化
2.同步消除  一个方法没有逃逸出线程,那就可以消除线程同步的措施,提高效率
3.标量替换  就是拆分对象,将对象拆分成标量(最小单位,如int String),然后在使用到对象的属性的地方直接使用标量

TPS:事务每秒处理数
指令重排序   线程内观察是串行的有序的,线程外看线程是无序的
线程调度:协同式线程调度和抢占式线程调度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值