JVM
JVM内存结构
堆、栈、方法区、直接内存、堆和栈区别
-
堆
Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。可利用参数 -Xms -Xmx 进行堆内存控制。
这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用分代回收算法,所有堆内存也分为 新生代、老年代,可以方便垃圾的准确回收。
这块内存属于线程共享区域。
-
栈
虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。
每一个栈帧由局部变量区、操作数栈等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。
如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 StackOverflowError。
若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 StackOverflowError。
若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出 OutOfMemoryError。
这块内存区域也是线程私有的。
-
方法区
-
方法区(JDK1.7)
方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量。 这块区域也被称为永久代。
可利用参数 -XX:PermSize -XX:MaxPermSize 控制初始化方法区和最大方法区大小。 -
元数据区(JDK1.8)
在 JDK1.8 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(Metaspace)。
默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError: PermGen。
但也不能无限扩展,因此可以使用 -XX:MaxMetaspaceSize来控制最大内存。
-
-
运行时常量池
运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。
-
直接内存
直接内存又称为 Direct Memory(堆外内存),它并不是由 JVM 虚拟机所管理的一块内存区域。
有使用过 Netty 的朋友应该对这块并内存不陌生,在 Netty 中所有的 IO(nio) 操作都会通过 Native 函数直接分配堆外内存。
它是通过在堆内存中的 DirectByteBuffer 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为零拷贝。
既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用 -XX:+DisableExplicitGC 参数来禁止该方法)。
值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。
Java内存模型
内存可见性、重排序、顺序一致性、volatile、锁、final
-
内存可见性
CPU在执行代码的时候,为了减少变量访问的时间消耗可能将代码中访问的变量的值缓存到该CPU缓存区中,因此,相应的代码再次访问该变量的时候,相应的值可能从CPU缓存中而不是主内存中读取的。同样的,代码对这些被缓存过的变量的值的修改也可能仅是被写入CPU缓存区,而没有写入主内存。由于每个CPU都有自己的缓存区,因此一个CPU缓存区中的内容对于其他CPU而言是不可见的。这就导致了在其他CPU上运行的其他线程可能无法“看到”该线程对某个变量值的更改。这就是所谓的内存可见性。
synchronized关键字的另一个作用就是保证了一个线程执行临界区中的代码时,所修改的变量值对于稍后执行该临界区的线程来说是可见的。这对于保证多线程代码的正确性来说非常重要。
而volatile关键字也能够保证内存可见性。即一个线程对一个采用volatile关键字修饰的变量的值的更改,对于其他访问该变量的线程而言总是可见的。也就是说,其他线程不会读到一个“过期”的变量值。因此,有人将volatile关键字和synchronized关键字所代表的内部锁做比较,将其称为轻量级的锁。这种称呼其实并不恰当,volatile关键字只能保证内存可见性,它并不能像synchronized关键字所代表的内部锁那样能够保证操作的原子性。volatile关键字实现内存可见性的核心机制是:当一个线程修改了一个volatile修饰的变量的值时,该值会被写入主内存(即RAM)而不仅仅是当前线程所在的CPU的缓存区,而其他CPU的缓存区中存储的该变量的值也会因此而失效(从而得以更新为主内存中该变量的“新值”)。这就保证了其他线程访问该volatile修饰的变量时,总是可以获取到该变量的最新值。
- 重排序
volatile关键字的另一个作用是:它禁止了指令重排序(Re-order)。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行。
禁止指令重排序虽然导致编译器和CPU无法对一些指令进行可能的优化,但是它某种程度上让代码执行看起来更符合我们的期望。
Volatile、synchronized两者的区别联系:
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
- volatile仅能实现变量的修改可见性,不能保证原子性(线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为Volatile 变量没上锁);而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞和上下文切换。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- 在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原子操作。当变量的值由自身决定时,如n=n+1、n++ 等,volatile关键字将失效。只有当变量的值和自身无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
- “锁是昂贵的”,谨慎使用锁机制。
-
volatile
-
锁
-
final
根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。
final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。
内存分配策略、垃圾收集器(G1)、GC算法、GC参数、对象存活的判定
参考:https://blog.csdn.net/qq_31997407/article/details/79735411
JVM参数及调优
参考:https://www.cnblogs.com/trgl/p/7271111.html
参考:https://www.cnblogs.com/csniper/p/5592593.html
-Xms4096M:堆容量初始值
-Xmx4096M:堆容量最大值
-Xmn1024M:新生代容量,所以老年代容量 = 堆容量 - 新生代容量 = 3072M
-Xss256K:线程堆栈空间大小
-XX:MaxDirectMemorySize:Direct Buffer Memory大小
-Djava.awt.headless=true:使用缺少外设的系统配置模式
-Dfile.encoding=UTF-8:设置编码规范
jmx配置用于远程管理
-XX:+HeapDumpOutOfMemoryError:当出现OOM时,打印堆快照
-XX:HeapDumpPath:堆快照打印路径,建议文件后缀设为hprof,可被MAT识别
-XX:+DisableExplicitGC:关闭System.gc()
-XX:SurvivorRatio=1:Eden区与Survivor区的大小比值
-XX:+UserConcMarkSweepGC:使用CMS收集器
-XX:+UserParNewGC:新生代使用ParNew收集器
-XX:+CMSParallelRemarkEnabled:降低标记停顿
-XX+UseCMSCompactAtFullCollection:在full gc的时候,对年老代的压缩
-XX:CMSFullGCsBeforeCompaction=0:full gc后不压缩老年代内存空间
-XX:LargePageSizeInBytes:内存页的大小
-XX:+UseFastAccessorMethods:原始类型的快速优化
-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义初始化定义开始CMS收集,禁止hostspot自行触发CMS GC
-XX:CMSInitiatingOccupancyFraction=80:老年代使用80%后开始CMS收集
-XX:SoftRefLRUPolicyMSPerMB=0:每兆堆空闲空间中SoftReference的存活时间为0秒
Java对象模型
https://blog.csdn.net/w372426096/article/details/81167669
- oop-klass、对象头
oop-klass model
OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
oop体系:
//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池告诉缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;
如上面代码所示, oops模块包含多个子模块, 每个子模块对应一个类型, 每一个类型的oop都代表一个在JVM内部使用的特定对象的类型。其中有一个变量oop的类型oopDesc是oops模块的共同基类型。而oopDesc类型又包含instanceOopDesc (类实例)、arrayOopDesc (数组)等子类类型。其中instanceOopDesc 中主要包含以下几部分数据:markOop _mark和union _metadata 以及一些不同类型的 field。
在java程序运行过程中, 每创建一个新的java对象, 在JVM内部就会相应的创建一个对应类型的oop对象来表示该java对象。而在HotSpot虚拟机中, 对象在内存中包含三块区域: 对象头、实例数据和对齐填充。其中对象头包含两部分内容:_mark和_metadata,而实例数据则保存在oopDesc中定义的各种field中。
_mark:
_mark这一部分用于存储对象自身的运行时数据, 如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 官方称它为 "Mark Word"。对象需要存储的运行时数据很多, 其实已经超出了32位和64位Bitmap结构所能记录的限度, 但是对象头信息是与对象自身定义的数据无关的额外存储成本, 考虑到虚拟机的空间效率, Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息, 它会根据对象的状态复用自己的存储空间。
_metadata:
_metadata这一部分是类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针, 换句话说查找对象的元数据信息并不一定要经过对象本身, 其取决于虚拟机实现的对象访问方式。目前主流的访问方式有使用句柄和直接指针两种, 两者方式的不同这里先暂不做介绍。另外, 如果对象是一个Java数组, 那么在对象头中还必须有一块用于记录数组长度的数据, 因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小, 但是从数组的元数据中却无法确定数组的大小。
klass
Klass体系:
//klassOop的一部分,用来描述语言层的类型
class Klass;
//在虚拟机层面描述一个Java类
class instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class instanceRefKlass;
//表示methodOop的Klass
class methodKlass;
//表示constMethodOop的Klass
class constMethodKlass;
//表示methodDataOop的Klass
class methodDataKlass;
//作为klass链的端点,klassKlass的Klass就是它自身
class klassKlass;
//表示instanceKlass的Klass
class instanceKlassKlass;
//表示arrayKlass的Klass
class arrayKlassKlass;
//表示objArrayKlass的Klass
class objArrayKlassKlass;
//表示typeArrayKlass的Klass
class typeArrayKlassKlass;
//表示array类型的抽象基类
class arrayKlass;
//表示objArrayOop的Klass
class objArrayKlass;
//表示typeArrayOop的Klass
class typeArrayKlass;
//表示constantPoolOop的Klass
class constantPoolKlass;
//表示constantPoolCacheOop的Klass
class constantPoolCacheKlass;
和oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。
Klass向JVM提供两个功能:
实现语言层面的Java类(在Klass基类中已经实现)
实现Java对象的分发功能(由Klass的子类提供虚函数实现)
HotSpot JVM的设计者因为不想让每一个对象中都含有一个虚函数表, 所以设计了oop-klass模型, 将对象一分为二, 分为klass和oop。其中oop主要用于表示对象的实例数据, 所以不含有任何虚函数。而klass为了实现虚函数多态, 所以提供了虚函数表。所以,关于Java的多态,其实也有c++虚函数的影子在。
HotSpot
- 即时编译器、编译优化
https://blog.csdn.net/YingHuaNanHai/article/details/80731351
类加载机制
- classLoader、类加载过程、双亲委派(破坏双亲委派)、模块化(jboss modules、osgi、jigsaw)
类加载机制---classLoader、类加载过程、双亲委派(破坏双亲委派)、模块化(jboss modules、osgi、jigsaw)
jvm之java类加载机制和类加载器(ClassLoader)的详解
虚拟机性能监控与故障处理工具
- jps, jstack, jmap、jstat, jconsole, jinfo, jhat, javap, btrace、TProfiler
https://blog.csdn.net/qq_34337272/article/details/80265149
https://blog.csdn.net/baidu_39299382/article/details/80330476
编译与反编译
- javac 、javap 、jad 、CRF
作者: AntzUhl
首发地址博客园:http://www.cnblogs.com/LexMoon/
代码均可在Github上找到(求Star) : Github
个人博客 : http://antzuhl.cn/
公众号 |
![]() |
赞助
支付宝 |
微信 |
![]() |
![]() |