这是关于java虚拟机相关的学习笔记
1. 程序计数器
程序计数器(program counter 也称作pc寄存器)就是记录当前线程所执行的字节码行号,字节码解释器工作就是通过改变这个计数器的数值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java多线程是通过线程切换来实现的,所以每个线程必须存储自己当前执行的字节码行号,以保障切换之后再回到当前线程的时候需要恢复到正确的位置,该区域线程私有,各个线程不互相影响。执行本地方法时这个计数器为空(Undefined)。
注意点:这里的pc寄存器是JVM层面上的,并非是cpu上原生的pc寄存器,所以native方法执行虽然在JVM层面上的pc寄存器没有定义值,线程切换也不会导致程序混乱。
2. java虚拟机栈
java虚拟机栈是java方法运行过程的内存模型,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。每个方法执行的时候都会创建一个栈帧,栈帧中用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表:储存方法中的局部变量,包括方法的形参、方法非静态变量、基本数据类型、
3. 本地方法栈
本地方法栈与java虚拟机栈非常类似,唯一的区别是java虚拟机栈执行的是java方法(字节码),本地方法栈执行的是本地方法,用c、c++编写的本地方法。
4. 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
jdk7使用的是永久代来实现方法区,但是jdk8以后采用metaSpace(元空间)来实现,元空间是直接采用的直接内存(非堆),之前的永久代则是java堆中的一部分。而且采用元空间实现以后将字符串常量池保留在了堆中,其他的则信息留在了metaSpace中。
5. 堆
所以线程共享的一块区域,几乎所以有java对象都在堆里面进行分配,这里要注意以下几个问题
1.并不是所有的对象都在堆中分配
这是一个很容易被忽略的点,jdk1.8之后,虚拟机默认开启子逃逸分析,如果变量A只在本方法中使用,则可以不在堆中分其分配,可以在栈中为其分配. 这样随着方法调用结束,栈帧销毁,对角也跟着销毁,就不用调用GC了。
2.虚拟机规范并没有对堆进行分代划分
如我们现在常说的年轻代,老年代等是HotSpot的实现, JVM规范只是制定了堆,没有制定分代的标准。
6. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
java对象在堆内存中的存储布局可以划分为三个部分,对象头、实例数据、对其填充,如果是数组对象还会有一个数据长度存储。
对象头
对象头包含两部分信息,第一部分是自身运行时数据,哈希码(HashCode)、GC分代年龄信息、锁状态标志位、线程持有的锁、偏向线程ID、偏向时间戳等。第二部分是指向对象类型数据指针,也就是指向class的数据指针。开启了压缩指针占据12bytes,未开启压缩指针占据16bytes。
实例数据
对齐填充
对齐填充并没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
java对象访问的方式主流的有两种,一是句柄方式,这种方式需要在堆中单独维护一个句柄池,这种好处就是对象被移动的时候引用的值不用改变,只需要改变句柄池中的指针就可以了,但是同时带来的性能消耗是多一次寻址。二是直接指针方式,这种方式是引用直接存储的实例数据的地址,直接可以访问,但是当对象移动的时候需要去更新引用的地址,但是性能上会更加快。
- 标记清除算法
最简单的垃圾回收算法,直接标记已经废弃的对象,然后直接进行对象回收,但是标记清除有两个问题,第一个是效率在需要回收对象较多的时候,这时需要进行大量的标记和清除动作,到时过程耗时很长。第二个是产生内存碎片。
- 标记复制算法
标记复制算法,将内存分为多个区域,一个区域用完以后,就将这个区域还存活的对象复制到另外一个区域,jvm大多数虚拟机都采用这种方式去回收新生代,HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略,将新生代分为Eden、Survivor,Survivor有两个,每次垃圾回收的时候都会讲Eden区域以及当前使用的一个Survivor区域存活的对象移动到另外一个Survivor区域中,如果Survivor内存不足以存放存活的对象,那么将会使用内存担保机制直接将该对象放入老年代。
- 标记整理算法
标记整理算法,该算法第一阶段也是标记,但是后续不会直接进行内存释放,而是将标记过后的存活对象向内存空间的另一端移动,然后直接清理掉边界以外的内存,标记-整理相对于标记-清除不会产生内存碎片,并且在内存存活对象较多的时候比标记-复制算法效率更高一些。但是标记-整理也存在不足的地方,一是标记-整理在移动对象的时候需要stop-the-world,需要暂停工作线程,因为对象移动过程中需要更改用到这个对象的引用地址。二是大量存活对象移动操作也是一个极为负重的操作。
- 新生代收集器
Serial收集器
Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。
ParNew收集器
ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。
- 老年代收集器
Serial Old收集器
Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
Parallel GC(并行gc) -XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的,所以对于延迟性有比较高的要求可以使用cms gc,cms和并行gc不同,cms关注的是地延迟性,而并行gc更关注的是吞吐量。CMS标记清除会产生内存碎片,可通过配置来进行碎片整理,默认是每次FULL GC都会进行内存整理,可以配置N次GC之后进行一次内存整理,不过JDK9以后参数被废弃掉了,因为内存整理可以与用户线程并发进行就无需这个参数了。
CMS默认启动的回收线程数是(处理器核心数量+3)/4。
CMS GC四个阶段
- G1收集器
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1配置参数相关
| 选项 | 作用 |
|---|---|
| -XX:+UseG1GC | 启用 G1 GC |
| -XX:G1NewSizePercent | 初始年轻代占整个 Java Heap 的大小,默认值为 5% |
| -XX:G1MaxNewSizePercent | 最大年轻代占整个 Java Heap 的大小,默认值为 60% |
| -XX:G1HeapRegionSize | 设置每个 Region 的大小,单位 MB,需要为 1,2,4,8,16,32 中的某个值,默 认是堆内存的 1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了。 |
| -XX:ConcGCThreads | 与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可 能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回 收机制耗时加长。 |
| -XX:+InitiatingHeapOccupancyPercent | G1 内部并行回收循环启动的阈值,默认为 Java Heap 的 45%。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在 什么时间启动老年代的并行回收。 |
| -XX:G1HeapWastePercent | G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中 的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以 遗留少量的下次处理,这样也降低了单次消耗的时间。 |
| -XX:MaxGCPauseMills | 预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在 这个范围内。 |
- 正在执行的方法中的局部变量和输入参数
- 活动线程
- 所有类的静态变量
- JNI引用
点击这里可以查看官方文档:jvm虚拟机规范。
java启动不指定堆大小的时候,默认最大堆大小为机器内存的1/4,分配给年轻代的最大空间为堆大小的1/3,Eden与Survivor的比例为8:2, yong与old比例为1:2,默认的堆外内存为堆的大小。
jps(JVM Process Status Toll)
jps是查看当前系统运行java进程状态的工具
| 选项 | 作用 |
|---|---|
| -q | 只输出进程id,省略主类名称 |
| -m | 输出进程启动时传给主类main()函数的参数 |
| -l | 输出主类全名,如果进程执行的是JAR包,则输出JAR路径 |
| -v | 输出虚拟机进程启动时的JVM参数 |
jstat(虚拟机统计信息监视工具)
| 选项 | 作用 |
|---|---|
| -class | 查看类加载情况、卸载情况以及类装载所耗费的时间 |
| -gc | 查看JVM堆情况,包括Survivor、Eden、老年代以及元空间的容量以及使用情况,垃圾收集数据等信息。 |
| -gccapacity | 与gc输出内容基本相同,但是gccapacity更关注各个区域使用最大、最小空间数据。 |
| -gcutil | 与gc输出内容基本相同,但输出主要是各个区域的百分比数据。 |
| -gccause | 与gcutil输出内容基本相同,但是会输出导致上一次垃圾收集产生的原因。 |
jinfo(java配置信息工具)
| 选项 | 作用 |
|---|---|
| -falg | 查询某一个name配置的值 |
jmap(java内存映像工具)
| 选项 | 作用 |
|---|---|
| -dump | 生成java堆快照,格式为-dump:[live],format=b,file=[filename],其中live表示是否只dump出存活的对象。 |
| -histo | 显示对象统计信息,包括类、实例数量、合计容量等。 |
jmap -dump:format=b,file=xxxxxx.dump pid
jstack(Java堆栈跟踪工具)
jstack打印的堆栈信息过的话可以配合 grep使用 列如 jstack -l pid | grep -30 xxx,直接查找有问题线程的上下文信息,或者使用jstack -l pid >> xxx.txt 输出内容到文件。
- 魔数:每个class文件头4个字节为魔数,它唯一的作用就是标识这个文件是否为一个能被虚拟机接收机的class文件。
- 版本号:随后4个字节为版本号,第5、6个字节为次版本号,第7、8为主版本号。
- 常量池:常量池的内容是class文件空间最大的数据项目之一,常量池的开头都会有一个u2类型的数据类存储常量池的数量。常量池中主要存放两大类常量:字面量(文本字符串、被声明为final的常量值等)和符号引用(类和接口的全限定名、方法名和描述符、方法句柄和方法类型等)。
- 访问标志:接着常量池的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。例如这个class是类还是接口,是否是public,是否是abstract,是否是枚举类、注解类等等。
- 类索引、父类索引以及接口索引信息。
- 字段表:描述类中声明的变量,包括类级变量、实例级变量,但是不包括方法内部声明的局部变量。
- 方法表:描述类中声明的方法,静态方法、实例方法以及方法的访问标志等。
- 属性表:class文件、字段表、方法表都可以携带自己的属性表集合,例如某些方法解析出来的代码就存在属性表中,对应的属性名称为code。
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。
类加载的时机完全由虚拟机自己把握,但是对于初始化阶段虚拟机有强制性的规范,而对类进行初始化(类加载、验证、准备)的过程必须在此之前开始。
何时进行类初始化
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载必须要由类加载器去加载,但是jvm有启动类加载器、扩展类加载器、应用程序加载器、以及自定义加载器,那么最终由哪一个类加载器去加载呢?双亲委派原则就是解决这个问题的,类加载的过程:先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
启动类加载器负责加载《JAVA_HOME》\lib目录下的类。
扩展类加载器负责加载《JAVA_HOME》\lib\ext目录下的类。
应用程序类加载器负责加载用户类路径(ClassPath)上所有的类库。
双亲委派的好处
1.使得类加载具有层次性,避免的基础类重复加载的情况。
2.避免核心类被篡改。
违背双亲委派的例子
1.jdbc驱动加载,java提供的spi接口在核心类库,由启动类去加载,但是spi的实现类在classpath下,是一个第三方jar包,这个时候就需要通过ServiceLoader类,ServiceLoader通过获取当前上下文加载器,并将上下文加载器传递下去然后去实现类的加载。
2.tomcat服务webapp下的应用类文件加载,因为tomcat要保证不同的webapp目录要进行隔离。
3.代码热替换。
