jvm的位置
jvm是运行在操作系统之上的,与硬件没有直接的交互,但是却可以调用本地方法(native)与硬件交流。
jvm的架构
类加载器
类加载器负责加载字节码文件到内存中,字节码文件在文件的开头有特定的文件标识。这些文件的内容将会被转换成方法区中的运行时数据结构,ClassLoader只负责.class文件的加载,至于能否运行,则是由执行引擎决定的。
-
类加载器的种类
- 启动类加载器:Bootstrap(C++语言编写)
- 扩展类加载器:Extension(Java语言编写)
- 系统类加载器:System(也叫做AppClassLoader,加载当前application的classpath的所有类文件)
public class UserObject { public static void main(String[] args) { Object object = new Object(); // null (C++语言编写,Java中作为顶级超类) System.out.println(object.getClass().getClassLoader()); UserObject userObject = new UserObject(); // sun.misc.Launcher$AppClassLoader@18b4aac2(此时获得的是系统类加载器) System.out.println(userObject.getClass().getClassLoader()); // sun.misc.Launcher$ExtClassLoader@74a14482(系统类加载器的父类是扩展类加载器) System.out.println(userObject.getClass().getClassLoader().getParent()); } }
-
双亲委派机制
当一个类加载器收到了类加载请求,它不会去尝试自己加载这个类,而是把这个加载请求委托给父类完成,而每一个层次的类加载器的做法都是如此,因此所有的加载请求都会被发送到Bootstrap类加载器中。只有当父类加载器反馈无法完成这个请求时,子类加载器才会去尝试加载。
这样做的好处是很明显的,例如,用户自定义一个java.lang.String类,系统类加载器不会直接加载该类,而是将加载请求向上传递直到启动类加载器,启动类加载器会发现在rt.jar中已经存在java.lang.String,则会将该类加载进来。最终这样就保证了尽管使用不同的类加载器,结果得到的都是同一个Java原生的String对象(Java语言特性中的安全性体现)。如果的确需要我们自定义的String,那么我们应该避免使用jvm自带的三个类加载器,继承ClassLoader类实现我们自己的类加载器。
本地方法接口和栈
本地接口的作用是融合不同的编程语言为Java所利用,具体做法就是在内存中开辟一块区域处理标记为native的方法,在本地方法栈中进行登记,执行引擎执行时加载本地类库。
程序计数器
每个线程都有一个程序计数器,是线程私有的,可以简单理解为一个指针指向方法区中的方法字节码(指向即将要执行的指令代码),是一个非常小的内存空间,由执行引擎读取下一条指令。若执行的是一个native方法,则程序计数器是空的。程序计数器主要用来完成分支、循环、跳转以及异常处理等基础功能。
方法区
各线程共享的运行时内存区域,方法区中存储了每一个类的结构信息Class(字段、常量池、方法数据、构造函数、普通方法的字节码… …)方法区是一个规范,不同的jvm的实现是不一样的。最典型的是永久代和元空间。方法区是一个常驻内存的区域,存放JDK自身所携带的类和接口的元数据,也就是说存储的是运行环境所必需的信息(例如连接数据库的驱动… …),垃圾回收器不会回收方法区,关闭jvm才会释放该区域所占用的内存空间。
注意:实例变量是存储在堆中的,和方法区无关!
- 永久代:JDK8以前
- 元空间:JDK8及以后
栈
Java栈主管Java程序的运行,其生命周期跟随线程的生命周期,栈是线程私有的。栈不存在垃圾回收的问题。栈中存储三类数据:
- 本地变量:输入参数(形参)和输出参数(返回值)以及方法内的变量
- 栈操作:记录出栈和入栈的操作
- 栈帧数据:包括类文件,方法等
注意:栈溢出 Exception in thread “main” java.lang.StackOverflowError是一个ERROR!
栈 堆 方法区的关系
堆
参数调优
- 元空间
JDK8及以后的元空间并不在虚拟机中,而存在于本机物理内存。因此,默认情况下,元空间的大小受到本地内存的限制。类的元数据放入本地内存中,字符串池和类的静态变量放入堆中。 - 堆空间
-
-Xms:设置初始分配大小,默认为物理内存的1/64
-
-Xmx:最大分配内存,默认为物理内存的1/4
(以上两个参数强烈建议设置一致,避免出现内存峰值波动)
-
-XX:+PrintGCDetails:输出详细的GC处理日志
e.g
-Xms10m -Xmx10m -XX:+PrintGCDetails
-
GC日志信息分析
[GC (Allocation Failure) [PSYoungGen: 2028K->499K(2560K)] 2028K->771K(9728K), 0.0011613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1998K->510K(2560K)] 2270K->1485K(9728K), 0.0007512 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2392K->304K(2560K)] 4579K->2793K(9728K), 0.0005962 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1596K->336K(2560K)] 7721K->7067K(9728K), 0.0008696 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 336K->0K(2560K)] [ParOldGen: 6731K->3637K(7168K)] 7067K->3637K(9728K), [Metaspace: 3196K->3196K(1056768K)], 0.0061910 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1259K->160K(2560K)] 6109K->6221K(9728K), 0.0007565 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 160K->0K(2560K)] [ParOldGen: 6061K->3028K(7168K)] 6221K->3028K(9728K), [Metaspace: 3207K->3207K(1056768K)], 0.0054982 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 45K->96K(1536K)] 5497K->5548K(8704K), 0.0009081 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 96K->128K(2048K)] 5548K->5580K(9216K), 0.0004201 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2048K)] [ParOldGen: 5452K->4241K(7168K)] 5580K->4241K(9216K), [Metaspace: 3215K->3215K(1056768K)], 0.0081965 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4241K->4241K(9216K), 0.0004658 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4241K->4221K(7168K)] 4241K->4221K(9216K), [Metaspace: 3215K->3215K(1056768K)], 0.0064194 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 2048K, used 85K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 8% used [0x00000000ffd00000,0x00000000ffd154f8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 7168K, used 4221K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 58% used [0x00000000ff600000,0x00000000ffa1f4e8,0x00000000ffd00000)
Metaspace used 3261K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at se.Test.main(Test.java:14)
[GC (Allocation Failure) [PSYoungGen: 854K->587K(2048K)]
GC类型:PSYoungGen;GC前新生代内存占用:854K;GC后新生代内存占用:587K;新生代总内存大小:2048K(大约1/3堆内存)
2874K->3164K(9216K), 0.0008812 secs]
GC前jvm堆内存占用:2874K;GC后jvm堆内存占用:3164K;jvm堆总内存占用:9216K;GC耗时:0.0008812 secs
[Times: user=0.00 sys=0.00, real=0.00 secs]
用户耗时:user=0.00;系统耗时:sys=0.00;实际耗时:real=0.00 secs;
注意:java.lang.OutOfMemoryError是ERROR!
GC算法
- 引用计数法(jvm的实现一般不采用此种方式)
- 复制算法
HotSpot JVM把年轻代分为了三个部分:一个Eden区和两个Survivor区,默认比例为8:1:1。一般情况下,新创建的对象都会被分配到Eden区(特殊的大对象特殊处理),这些对象经过第一次minor GC后,存活的将会被转移到Survivor区,在Survivor区中每经历一次minor GC,年龄就会增加1,当年龄增加到一定程度时,就会被移动到年老代中。由于年轻代的对象回收率特别高,所以年轻代的GC算法采用的是复制算法,它的基本思想是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另一块上面。复制算法不会产生内存碎片。缺点是耗费空间(S0的容量有多大则S1的容量也必须和S0匹配。并且需要双倍空间)
-
标记清除算法
算法分为标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。当程序运行时,若可用内存即将耗尽,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最后统一回收这些对象,完成标记清理工作后继续让程序恢复运行。
它的优点是不需要额外的空间,缺点是耗时严重且会产生内存碎片。
老年代一般是标记清除和标记整理(压缩)的混合实现。
-
标记压缩(整理)算法
标记压缩(整理)算法是在标记清除算法的基础上,再次扫描,并向一端滑动存活的对象(压缩)。优点是没有内存碎片,缺点是增加了移动对象的成本。
JMM(Java内存模型)
jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异。它的主要目的是解决由于多线程共享内存进行通信时,存在的本地内存数据不一致等带来的问题。可以保证并发编程中的原子性、可见性和有序性。
由于jvm运行程序的实体是线程,而每个线程创建时jvm多会为其创建一个工作内存(也可称为栈空间),工作内存是每个线程的私有数据区域。而Java内存模型中所有变量都存储在主内存,主内存是共享数据区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作空间,然后对变量进行操作,待操作完成后将变量写回主内存。不能直接操作主内存的变量,各个线程中的工作内存中存储着主内存的变量副本的拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
-
三个特性
- 可见性
- 原子性
- 有序性
-
jmm关于同步的规定
- 线程解锁前,必须把共享变量的值同步回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
-
代码示例
-
反面演示
public class Test { public static void main(String[] args) { MT mt = new MT(); // 一个线程对共享变量中的值进行修改 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "---------------------------"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } mt.updateNum(); System.out.println(Thread.currentThread().getName() + "num value is " + mt.num); }).start(); // 主线程读取共享变量中的值 while (mt.num == 10) { // 上面的操作对主线程来说是不可见的,因此将不会跳出循环 } System.out.println(Thread.currentThread().getName() + "done!"); } } class MT { // 主内存中的共享变量 int num = 10; public void updateNum() { this.num = 20; } }
result:
-
正确演示
使变量num具有可见性
volatile int num = 10;
result:
-