标签:程序 平台 高性能 监控软件 结束 识别 内存监控 nim 区别
JVM 是 Java Virtual Machine 的缩写,它是一个java实现的虚拟计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现···
好,其实抛开这么专业的句子不说,就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。
比如我们现在写了一个 HelloWorld.java ,而我们的 JVM 是不认识文本文件的,所以它需要一个 编译 ,让其成为一个它会读二进制文件的 HelloWorld.class
如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个 类加载器 中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进JVM里面来。
方法区 是用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,编译后代码···等
类加载器将 .class 文件搬过来就是先丢到这一块上
堆 主要放了一些存储的数据,比如对象实例,数组···等,它和方法区都同属于 线程共享区域 。也就是说它们都是 线程不安全 的
jvm的调优就是在调线程共享区域。大部分调堆
栈 这是我们的代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。
我们会听说过 本地方法栈(Native Method Stack) 或者 本地方法接口 这两个名词,不过我们基本不会涉及这两块的内容,它俩底层是使用C来进行工作的,和Java没有太大的关系。
主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是 线程独享 的,就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。
执行main方法的步骤如下:
其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。
它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容中静态数据结构转换成运行时数据区的方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine(执行引擎) 来决定
从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接
通过“类全名”来获取定义此类的二进制字节流(文件)
将字节流class文件也就是这个类的信息加载到运行时数据区的方法区中(见上面的简单的代码例子)
将字节流(文件)中静态数据结构转化成运行时数据区的方法区中运行时的数据结构
在堆中生成一个代表这个类的 java.lang.Class对象,作为方法区这些数据的访问入口
loadClass()
方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
public static int value=111
,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。初始化其实就是执行类构造器的<clinit>()
方法的过程,而且要保证执行前父类的<clinit>()
方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。(例子:此时准备阶段时的那个 public static int value=111
由默认初始化的0变成了显式初始化的111. 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。)
<clinit>()
不同于类的构造函数,这个方法是字节码文件中只能给JVM识别的特殊方法。对类进行初始化(只有主动去使用类才会初始化类):
当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
使用 java.lang.reflect
包的方法对类进行反射调用时如Class.forname("..."), newInstance()等等。如果类没初始化,需要触发其初始化。
初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
= = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =
MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
「补充,来自issue745」 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
= = = = = = = = = = = = == = = = = = = = = = = = 了解== = = = == = == = = == = = = = = = = =
GC将无用对象从内存中卸载
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,jdk自带的BootstrapClassLoader, ExtClassLoader, AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
类加载器对字节流class文件的加载需要靠classLoader
java.lang.ClassLoader
:
%JAVA_HOME%/lib
目录下的jar包和类或者或被 -Xbootclasspath
参数指定的路径中的所有类。%JRE_HOME%/lib/ext
目录下的jar包和类,或被 java.ext.dirs
系统变量所指定的路径下的jar包。双亲委派模型
loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,自顶向下尝试加载类。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
见图如下:
线程私有的:
线程共享的:
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
程序计数器的作用
注意:程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
关键在于native关键字
(现在可以通过调用其他接口进行融合:比如Socket http~)
虚拟机栈和本地方法栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:return 语句和抛出异常。
不管哪种返回方式都会导致栈帧被弹出,线程结束,栈内存也就释放了(栈就为空了)
栈和队列
栈中可以存放:8大基本类型的变量+对象的引用变量(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置))+实例的方法(包括main方法---->>第一个入栈的)
Java 虚拟机栈会出现两种错误:StackOverFlowError
和 OutOfMemoryError
。
StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。(通常是程序中存在死循环或者深度递归调用造成的)
OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常异常。(通常是栈大小设置太小)
解决方法:
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造函数,接口定义)、常量(final)、静态变量(static)、运行时的常量池
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
- JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
- JDK1.7 字符串常量池被从方法区拿到了堆中, 运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
- JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
理解运行时常量池和字符串常量池:
运行时常量池是在类加载器的加载步骤完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类加载器在连接步骤的解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
字符串池里的内容是在类加载器的加载步骤完成之后,经过类加载器的连接步骤中的验证,准备阶段之后,如果有创建一个对象实例中包含了字符串,那么就会在堆中生成该对象实例和生成字符串对象实例(字符串也是个对象),然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)
public class Test(){
private int a;
private Sting name="lyj";
}
public static void main(String[] args){
Test test1=new Test();
}
//此处创建的实例是new Test(),因为该对象中包含了字符串,所以又要在堆中创建一个字符串对象实例,而字符串对象实例的引用值“lyj”会存放到字符串常量池中
理解方法区和永久代的关系:
hotspot移除了永久代 (PermGen) 用元空间(Metaspace)取而代之的原因:
当你元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。
-XX:MetaspaceSize
标志定义元空间的初始大小,如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。注意:对象的实例变量存放在堆内存中,字符串常量池在JDK1.7后也放入了堆内存中,和方法区无关
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间(伊甸园)、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
上图所示的 eden区、s0区(survive幸存0区)、s1区(survive幸存1区)都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,当Eden空间满了之后,会触发一个叫做Minor GC(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区(from指针),并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1)。
这时form指针指向的survivor0区的 里面有对象,这时to指针指向的survivor1区的 里面没有有对象
继续存放对象,也是首先在Eden区(伊甸园)存放,当Eden满之后,又触发Minor GC,原先form指针指向的survivor0区存活的对象会被复制到to指针指向的survivor1区,Eden区存活的对象也会放入to指针指向的survivor1区,然后form to指针进行互换
这时form指针指向的survivor1区的 里面有对象,这时to指针指向的survivor0区的 里面没有有对象
此处包含 动态对象年龄判定 的知识点
当survive区满,触发Minor GC(进行垃圾回收),存活下来的对象它的年龄会增加,当增加到一定程度(默认为15岁,因为年龄标志位只有4位,最多为15),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
或者
Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”,并且该年龄段开始及大于的年龄对象就要进入老年代。
或者
survive区中Minor GC 一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。
经过多次的 Minor GC后仍然存活的对象,就会移动到老年代
老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。
当老年区执行了full gc之后仍然无法进行对象保存的操作,就出现了堆中最容易出现的 OutOfMemoryError 错误,这就是虚拟机中的堆内存不足,并且出现这种错误之后的表现形式还会有几种,比如:
OutOfMemoryError: GC Overhead Limit Exceeded
: 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发该错误。(和本机物理内存无关,和你配置的内存大小有关)直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
部分收集 (Partial GC):
整堆收集 (Full GC):收集整个 Java 堆和方法区。
2.1)引用计数法(引用计数器)
2.2)可达性分析算法
这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中。搜索当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。
引用类型:强引用,软引用,弱引用,虚引用(引用强度逐渐减弱)
强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
虚引用:任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
优点:可以解决循环引用的问题
缺点:它的实现需要耗费大量资源和时间,同时它的分析过程引用关系不能发生变化,所以需要停止所有进程
该算法分为“标记”和“清除”阶段:
解决标记-清除算法中的空间出现碎片问题
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
缺点:还是存在效率问题(多次扫描)
为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。
HotSpot VM中的垃圾回收器,以及适用场景 [
Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
特点:并发收集、低停顿
缺点:CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片
到jdk8为止,默认的垃圾收集器是Parallel(并行) Scavenge 和 Parallel Old
从jdk9开始,G1(整堆回收器)成为默认的垃圾收集器 目前来看,G1回收器停顿时间最短而且没有明显缺点,非常适合Web应用。
= == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =
并行:真正的“同时”运行,在同一时刻,有多个任务同时执行。(例如,在多核处理器上,有两个线程同时执行同一段代码。
并发:两个或多个任务可以在重叠的时间段内启动,运行和完成
并行(多个线程同时执行) 一定是并发,并不一定意味着并发一定要求是并行(包含关系)
= == = = = == = = = = = = = == = = = == = == = = = = == = = = = = = = =
对JVM进行调优,主要针对堆内存
-Xmx –Xms:指定java堆最大值(默认值是物理内存的1/4(<1GB))和初始java堆最小值(默认值是物理内存的1/64(<1GB))
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。简单点来说,你不停地往堆内存里面丢数据,等它剩余大小小于40%了,JVM就会动态申请内存空间不过会小于-Xmx,如果剩余大小大于70%,又会动态缩小不过不会小于–Xms。就这么简单
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
// -Xmn5120m:代表新生代
//-XXSurvivorRatio=3:代表Eden:Survivor = 3
根据Generation-Collection算法(目前大部分JVM采用的算法),一般根据对象的生存周期将堆内存分为若干不同的区域,一般情况将新生代分为Eden ,两块Survivor;
//计算Survivor大小, Eden:Survivor = 3,总大小为5120,3x+x+x=5120 x=1024
根据实际事情调整新生代、幸存代的大小,官方推荐新生代占java堆的3/8,幸存代占新生代的1/10
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
这两种不同的创建方法是有差别的。
记住一点:只要使用 new 方法,便需要创建新的对象
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建一个“abc”对象,然后在堆空间中再创建一个String类的对象--字符串常量“abc”,因此将创建总共 2 个字符串对象。
验证:
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
对于基本类型来说,== 比较的是值是否相等;
对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String,本身的equals方法就被重写了),则比较的是地址里的内容。
作用:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象,一个是常量池中的 String 对象
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
为了解决线程之间的信息共享,数据一致性(完成底层封装)
为了屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果,Java虚拟机规范中定义了Java内存模型。
java内存模型(Java Memory Model, JMM)是一种规范。它规范了java虚拟机与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值。以及在必须时如何同步的访问共享变量 。
(线程之间通信必须要通过主内存(主内存其实是jvm内存模型的堆内存))
java内存模型一般指的就是下图。
从更低的层次来说主内存就是计算机硬件系统的内存,上面CPU缓存模型的内存
为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。Java内存模型中的线程的工作内存是cpu的寄存器和高速缓存的抽象描述。
而JVM静态存储模型就是jvm内存模型它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。
线程之间通信必须要通过主内存(主内存其实是堆内存)。
标签:程序 平台 高性能 监控软件 结束 识别 内存监控 nim 区别
原文地址:https://www.cnblogs.com/lyj-study/p/14669888.html