由方法区,堆区,虚拟机栈,程序计数器和本地方法栈组成。下面我们依次介绍这5部分。
程序计数器:记录下一条要执行的JVM指令的执行地址,字节码解释器工作时就是通过改变程序计数器中的值来获取下一条要执行的指令,分支,循环,跳转,异常处理线程恢复等功能都是依赖程序计数器来完成。
程序计数器存放在cpu的寄存器中。每条线程都有自己独立的程序计数器,各个程序计数器互不影响,故程序计数器为线程隔离的数据。
如果当前执行的是Java的非native方法,那么程序计数器存储下一条要执行的指令。如果是native方法那么程序计数器中为null。
特点:
Java虚拟机栈和程序计数器一样,都是线程私有的,它的生命周期与线程相同。
Java虚拟机栈的内存模型:
1.Java虚拟机栈内存是否受到内存回收机制的管理?
不会,因为在每次执行完方法后,内存会自动释放。当然线程执行完成后内存也是自动释放。
2.Java虚拟机栈内存分配越大越好?
不是,合适即可,如果栈内存分配过小容易造成内存溢出,而如果内存过大,那么Java开启的线程数就会变小,降低了Java代码的运行效率。
设置虚拟机栈的大小的虚拟机指令:
3.方法内局部变量是否线程安全?
本地方法栈与Java虚拟机栈的作用非常相似,不过Java虚拟机栈调用的是Java方法(即二进制字节码),而本地方法栈调用虚拟机使用到的Native方法。虚拟机规范并未强制规定,该方法的具体实现的语言。
与虚拟机栈一样本地方法栈也会抛出StackOverflowError和OutofMemoryError异常。
堆是Java虚拟机管理中内存最大的一块,是一块线程共享的区域。在虚拟机启动时创建堆区。堆区的唯一目的是存放对象实例(即new出来的对象)。
Java堆是垃圾回收器管理的最主要区域,因此也被称为GC堆
Java堆内存溢出是堆中最常见的异常,出现的原因是当进程中不断有新的对象创建而老对象有在使用导致不能被垃圾回收机制回收,就会出现堆内存溢出情况
可设置堆内存的大小来观测堆内存溢出情况:
设置堆内存大小的参数是-Xmx size
-Xmx8m
java.lang.OutOfMemoryError: Java heap space
内存溢出异常
示例:
/*** java.lang.OutOfMemoryError:内存溢出问题* 设置堆的大小:* -Xmx8m* 设置为8m*/
public class heapdemo {public static void main(String[] args) {int count = 0;List<String> list = new ArrayList<>();String str = "hello";try {while (true){list.add(str);str+=str;count++;}}catch (Throwable throwable){throwable.printStackTrace();System.out.println(count);System.out.println(str.length());}}
}
输出结果:
java.lang.OutOfMemoryError: Java heap spaceat java.pyOf(Arrays.java:3332)at java.sureCapacityInternal(AbstractStringBuilder.java:124)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)at java.lang.StringBuilder.append(StringBuilder.java:136)heapdemo.main(heapdemo.java:21)
26
335544320
方法区( Method Area )与Java堆一样,是各个线程共享的内存区域**,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据**。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
方法区在1.6版本以后发生了很大的改变,1.6以前方法区在一个叫永久代,此时方法区的数据被存储在堆中,有虚拟机进行管理。而在1.7后把方法区中的stringTable(字符串常量池)移出永久代还是存放在堆中。1.8以后把剩下的部分称为元空间(MetaSpace)并把元空间移出堆,存入内存中。
1.8以前会导致永久代内存溢出
演示永久代内存溢出java. lang . OutOfMemoryError: PermGen space
-XX:MaxPernSize=8m
1.8以后会导致元空间内存溢出
演示元空间内存溢出java . lang . OutOfMemoryError: Metaspace
因为方法区主要存储的是被加载的Java的类文件信息,故如果使用cglib等代理方式在代码运行的过程中,不断加载新的lei,那么就可能引起内存溢出。
/*** -XX:MaxMetaspaceSize=size*/
public class Mareademo extends ClassLoader {public static void main(String[] args) {int count=0;try {Mareademo mareademo = new Mareademo();for (int i = 0; i < 100000; i++) {//ClassWriter作用是生成类的二进制字节码ClassWriter classWriter = new ClassWriter(0);//visit()方法各个参数的意义:1.Java的版本号,2.类修饰符(public);3.类名称;4.包名;4.父类;5.实现的接口classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"class"+count,null,"java/lang/Object",null);//返回byte数组及二进制字节码文件byte[] bytes = ByteArray();//加载二进制文件mareademo.defineClass("class"+count,bytes,0,bytes.length);count++;}}finally {System.out.println(count);}}
}
在执行代码时因为方法区存在于本地内存所以可以放下的我们申请的类,所以我们在运行前需要设置参数减小方法区的大小,才能观察到内存溢出现象。
输出:
5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspaceat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:763)at java.lang.ClassLoader.defineClass(ClassLoader.java:642)Mareademo.main(Mareademo.java:23)
运行时常量池( Runtime Constant Pool )是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant Pool Table ),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
即常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
我们来了解一下一个具体实例中常量池的信息有哪些:
二进制字节码由:类基本信息,常量池,类方法定义及虚拟机指令
//类基本信息
Classfile /E:/java/idea/ym/jvm/out/production/jvm/wf/memo/Demo.classLast modified 2020-2-11; size 531 bytesMD5 checksum c8ad1fc04006e932e4edcd9c9cfcebbaCompiled from "Demo.java"
public Demominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:#1 = Methodref #6.#20 // java/lang/Object."<init>":()V#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #23 // hello world#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #26 // wf/memo/Demo#6 = Class #27 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lwf/memo/Demo;#14 = Utf8 main#15 = Utf8 ([Ljava/lang/String;)V#16 = Utf8 args#17 = Utf8 [Ljava/lang/String;#18 = Utf8 SourceFile#19 = Utf8 Demo.java#20 = NameAndType #7:#8 // "<init>":()V#21 = Class #28 // java/lang/System#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 = Utf8 hello world#24 = Class #31 // java/io/PrintStream#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 = Utf8 wf/memo/Demo#27 = Utf8 java/lang/Object#28 = Utf8 java/lang/System#29 = Utf8 out#30 = Utf8 Ljava/io/PrintStream;#31 = Utf8 java/io/PrintStream#32 = Utf8 println#33 = Utf8 (Ljava/lang/String;)V、
//类方法定义
{
//默认的构造方法Demo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lwf/memo/Demo;
//main方法public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 9: 0line 10: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
在执行nain方法是主要是执行
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
指令,getstatic 后面接 #2所以执行到getstatic时会到常量池中查询#2对应的值,#2的Fieldref表示是一个成员变量,而#2又对应#20和#21继续查找最终可以确定,getstatic是一个java/io/PrintStream所调用的 java /lang / System的out。
存放在常量池中的类信息,都会被加载到运行时常量池中,但是如果类中存在字符串,那么字符串在刚载入运行时常量池时不会立即变为对象,只是常量池中的符号,只有执行的代码调用相应的字符串时,才会把字符串变为对象。这是一种懒加载。
String的intern()方法:
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
public class StringTableDemo {public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "ab";String s4 = s1 + s2;String s5 = "a" + "b";String s6 = s4.intern();String s7 = "a" + s2;System.out.println(s3 == s4);//falseSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s6);//trueSystem.out.println(s3 == s7);//falseString x1 = new String("c") + new String("d");String x2 = x1.intern();String x3 = "cd";System.out.println(x1 == x2);//trueSystem.out.println(x3 == x1);//true}
}
分析:
StringTable也会发生垃圾回收。
StringTable的性能调优:在使用String对象时,可以先调用一下intern函数把字符串入串表,来减少对象的空间占用。
直接内存并不属于JVM内存结构,即直接内存被不受JVM的管理。直接内存( Direct Memory )并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。
在JDK 1.4中新加入了NIO ( New Input/Output )类,引入了一种基于通道( Channel )与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutCfMemoryError异常。
这是不使用直接内存时,Java操作磁盘文件的过程。我们可以看到,如果Java想使用磁盘文件的内容,那么需要先把数据读入系统缓存区,然后从系统缓存区复制一份,给Java的缓冲区,才能使用。
而如果我们使用直接内存:
那么表示Java可以直接使用内存的一部分,所以Java操控磁盘文件时,当文件写入系统的内存后,Java可以直接使用,不在进行复制到Java自己缓存区的操作,大大的提供了程序运行的效率。
我们可以使用代码来演示使用与不使用直接内存的情况,来观察二者的效率:
public class DirectDemo {private static String from = "F:\game\view.mp4";private static String to = "F:\h.mp4";private static int SIZE = 1024*1024;public static void main(String[] args) {io();directBuffer();}private static void io() {long start = System.currentTimeMillis();try {FileOutputStream outputStream = new FileOutputStream(to);FileInputStream inputStream = new FileInputStream(from);byte[] bytes = new byte[SIZE];while (true){int read = ad(bytes);if (read == -1){break;}outputStream.write(bytes,0,read);}}catch (Exception e){e.printStackTrace();}System.out.println("no direct use time:" + (System.currentTimeMillis()-start));}private static void directBuffer() {long start = System.currentTimeMillis();try {FileChannel outputStream = new FileOutputStream(to).getChannel();FileChannel inputStream = new FileInputStream(from).getChannel();ByteBuffer bb = ByteBuffer.allocateDirect(SIZE);while (true){int read = ad(bb);if (read == -1){break;}bb.flip();outputStream.write(bb);bb. clear();}}catch (Exception e){e.printStackTrace();}System.out.println("direct use time:" + (System.currentTimeMillis()-start));}
}
输出结果:
no direct use time:423
direct use time:169
no direct use time:428
direct use time:169
no direct use time:425
direct use time:196
执行三次可以看到,使用直接内存的效率总是优于不使用直接内存。
虽然直接内存不由JVM管理,但是它仍然会存在内存溢出问题。
案例:
public class StringTableDemo2 {public static void main(String[] args) {List list = new ArrayList();int count = 0;try {while (true){//ByteBuffer.allocateDirect方法可以分配在直接内存分配空间ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*100);//100mlist.add(byteBuffer);count++;}}catch (Throwable throwable){System.out.println(count);throwable.printStackTrace();}}
}
输出结果:
36
java.lang.OutOfMemoryError: Direct buffer memoryat java.serveMemory(Bits.java:694)at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)StringTableDemo2.main(StringTableDemo2.java:19)
可以看到循环了36次才释放内存(3.6g左右),即直接内存由上限,如果超过就会导致内存溢出。
虽然直接内存不受JVM管理,但是Java可以通过程序来控制直接内存的释放和申请。Java中的Unsafe就提供了对直接内存的回收和释放。而Unsafe类在具体的直接内存的申请和释放是native修饰即为本地方法。故就如之前所说Java的确不能控制直接内存的申请和释放。但Java可以通过其他语言对内存的操作来实现内存的使用。从而变相的实现JVM对直接内存的管理。
示例;
public class UnsafeDemo {public static void main(String[] args) throws IOException {Unsafe unsafe = getUnsafe();int size = 1024*1024*1024;System.out.println("等待开始。。");ad();System.out.println("资源开始写入。。。");long l = unsafe.allocateMemory(size);unsafe.setMemory(l,size,(byte)0);ad();System.out.println("资源释放");unsafe.freeMemory(l);ad();}
//我们不能直接获取Unsafe对象,故需要通过反射实现private static Unsafe getUnsafe(){Field theUnsafe;Unsafe unsafe = null;try {theUnsafe = DeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) (null);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return unsafe;}
}
该类实现了对内存的操作,在运行该类后,可以查看任务管理器,可以看到在确定该类后,会增加Java程序对内存的使用量。
直接内存的分配与释放原理:
禁用显示回收对直接内存的影响:
-XX:+DisableExplicitGC
命令可以关闭显示的调用(),即在代码中使用该gc方法不会起任何作用。而有时我们要可能因为某些原因关闭显示调用gc方法,此时我们申请的直接内存得不到释放。所以如果我们想在程序中直接使用直接内存,那么最好在使用完成后调用Unsafe.freeMemory()方法对内存进行回收。和c++程序一样。
本文发布于:2024-01-28 06:53:50,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17063960365602.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |