Skip to content

Commit 5e2ced4

Browse files
committed
8327247: C2 uses up to 2GB of RAM to compile complex string concat in extreme cases
Reviewed-by: mchung, shade
1 parent 2b7176a commit 5e2ced4

File tree

2 files changed

+297
-8
lines changed

2 files changed

+297
-8
lines changed

src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java

Lines changed: 254 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -27,17 +27,20 @@
2727

2828
import jdk.internal.access.JavaLangAccess;
2929
import jdk.internal.access.SharedSecrets;
30-
import jdk.internal.javac.PreviewFeature;
30+
import jdk.internal.misc.VM;
31+
import jdk.internal.org.objectweb.asm.ClassWriter;
32+
import jdk.internal.org.objectweb.asm.MethodVisitor;
33+
import jdk.internal.util.ClassFileDumper;
3134
import jdk.internal.vm.annotation.Stable;
3235
import sun.invoke.util.Wrapper;
3336

3437
import java.lang.invoke.MethodHandles.Lookup;
35-
import java.util.ArrayList;
36-
import java.util.Iterator;
37-
import java.util.List;
3838
import java.util.Objects;
39+
import java.util.Set;
3940

41+
import static java.lang.invoke.MethodHandles.Lookup.ClassOption.STRONG;
4042
import static java.lang.invoke.MethodType.methodType;
43+
import static jdk.internal.org.objectweb.asm.Opcodes.*;
4144

4245
/**
4346
* <p>Methods to facilitate the creation of String concatenation methods, that
@@ -98,6 +101,13 @@
98101
*/
99102
public final class StringConcatFactory {
100103

104+
private static final int HIGH_ARITY_THRESHOLD;
105+
106+
static {
107+
String highArity = VM.getSavedProperty("java.lang.invoke.StringConcat.highArityThreshold");
108+
HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 20;
109+
}
110+
101111
/**
102112
* Tag used to demarcate an ordinary argument.
103113
*/
@@ -354,9 +364,14 @@ public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
354364
}
355365

356366
try {
357-
return new ConstantCallSite(
358-
generateMHInlineCopy(concatType, constantStrings)
359-
.viewAsType(concatType, true));
367+
if (concatType.parameterCount() < HIGH_ARITY_THRESHOLD) {
368+
return new ConstantCallSite(
369+
generateMHInlineCopy(concatType, constantStrings)
370+
.viewAsType(concatType, true));
371+
} else {
372+
return new ConstantCallSite(
373+
SimpleStringBuilderStrategy.generate(lookup, concatType, constantStrings));
374+
}
360375
} catch (Error e) {
361376
// Pass through any error
362377
throw e;
@@ -1032,4 +1047,235 @@ private StringConcatFactory() {
10321047
// no instantiation
10331048
}
10341049

1050+
/**
1051+
* Bytecode StringBuilder strategy.
1052+
*
1053+
* <p>This strategy emits StringBuilder chains as similar as possible
1054+
* to what javac would. No exact sizing of parameters or estimates.
1055+
*/
1056+
private static final class SimpleStringBuilderStrategy {
1057+
static final int CLASSFILE_VERSION = 52; // JDK 8
1058+
static final String METHOD_NAME = "concat";
1059+
// ClassFileDumper replaced java.lang.invoke.ProxyClassDumper in JDK 21
1060+
// -- see JDK-8304846
1061+
static final ClassFileDumper DUMPER =
1062+
ClassFileDumper.getInstance("java.lang.invoke.StringConcatFactory.dump", "stringConcatClasses");
1063+
1064+
/**
1065+
* Ensure a capacity in the initial StringBuilder to accommodate all
1066+
* constants plus this factor times the number of arguments.
1067+
*/
1068+
static final int ARGUMENT_SIZE_FACTOR = 4;
1069+
1070+
static final Set<Lookup.ClassOption> SET_OF_STRONG = Set.of(STRONG);
1071+
1072+
private SimpleStringBuilderStrategy() {
1073+
// no instantiation
1074+
}
1075+
1076+
private static MethodHandle generate(Lookup lookup, MethodType args, String[] constants) throws Exception {
1077+
String className = getClassName(lookup.lookupClass());
1078+
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
1079+
1080+
cw.visit(CLASSFILE_VERSION,
1081+
ACC_SUPER + ACC_PUBLIC + ACC_FINAL + ACC_SYNTHETIC,
1082+
className,
1083+
null,
1084+
"java/lang/Object",
1085+
null
1086+
);
1087+
1088+
MethodVisitor mv = cw.visitMethod(
1089+
ACC_PUBLIC + ACC_STATIC + ACC_FINAL,
1090+
METHOD_NAME,
1091+
args.toMethodDescriptorString(),
1092+
null,
1093+
null);
1094+
1095+
mv.visitCode();
1096+
1097+
1098+
// Prepare StringBuilder instance
1099+
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
1100+
mv.visitInsn(DUP);
1101+
1102+
int len = 0;
1103+
for (String constant : constants) {
1104+
if (constant != null) {
1105+
len += constant.length();
1106+
}
1107+
}
1108+
len += args.parameterCount() * ARGUMENT_SIZE_FACTOR;
1109+
iconst(mv, len);
1110+
mv.visitMethodInsn(
1111+
INVOKESPECIAL,
1112+
"java/lang/StringBuilder",
1113+
"<init>",
1114+
"(I)V",
1115+
false
1116+
);
1117+
1118+
// At this point, we have a blank StringBuilder on stack, fill it in with .append calls.
1119+
{
1120+
int off = 0;
1121+
for (int c = 0; c < args.parameterCount(); c++) {
1122+
if (constants[c] != null) {
1123+
mv.visitLdcInsn(constants[c]);
1124+
sbAppend(mv, "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
1125+
}
1126+
Class<?> cl = args.parameterType(c);
1127+
mv.visitVarInsn(getLoadOpcode(cl), off);
1128+
off += getParameterSize(cl);
1129+
String desc = getSBAppendDesc(cl);
1130+
sbAppend(mv, desc);
1131+
}
1132+
if (constants[constants.length - 1] != null) {
1133+
mv.visitLdcInsn(constants[constants.length - 1]);
1134+
sbAppend(mv, "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
1135+
}
1136+
}
1137+
1138+
mv.visitMethodInsn(
1139+
INVOKEVIRTUAL,
1140+
"java/lang/StringBuilder",
1141+
"toString",
1142+
"()Ljava/lang/String;",
1143+
false
1144+
);
1145+
1146+
mv.visitInsn(ARETURN);
1147+
1148+
mv.visitMaxs(-1, -1);
1149+
mv.visitEnd();
1150+
cw.visitEnd();
1151+
1152+
byte[] classBytes = cw.toByteArray();
1153+
try {
1154+
Lookup hiddenLookup = lookup.makeHiddenClassDefiner(className, classBytes, SET_OF_STRONG, DUMPER)
1155+
.defineClassAsLookup(true);
1156+
Class<?> innerClass = hiddenLookup.lookupClass();
1157+
return hiddenLookup.findStatic(innerClass, METHOD_NAME, args);
1158+
} catch (Exception e) {
1159+
throw new StringConcatException("Exception while spinning the class", e);
1160+
}
1161+
}
1162+
1163+
private static void sbAppend(MethodVisitor mv, String desc) {
1164+
mv.visitMethodInsn(
1165+
INVOKEVIRTUAL,
1166+
"java/lang/StringBuilder",
1167+
"append",
1168+
desc,
1169+
false
1170+
);
1171+
}
1172+
1173+
/**
1174+
* The generated class is in the same package as the host class as
1175+
* it's the implementation of the string concatenation for the host
1176+
* class.
1177+
*/
1178+
private static String getClassName(Class<?> hostClass) {
1179+
String name = hostClass.isHidden() ? hostClass.getName().replace('/', '_')
1180+
: hostClass.getName();
1181+
return name.replace('.', '/') + "$$StringConcat";
1182+
}
1183+
1184+
private static String getSBAppendDesc(Class<?> cl) {
1185+
if (cl.isPrimitive()) {
1186+
if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) {
1187+
return "(I)Ljava/lang/StringBuilder;";
1188+
} else if (cl == Boolean.TYPE) {
1189+
return "(Z)Ljava/lang/StringBuilder;";
1190+
} else if (cl == Character.TYPE) {
1191+
return "(C)Ljava/lang/StringBuilder;";
1192+
} else if (cl == Double.TYPE) {
1193+
return "(D)Ljava/lang/StringBuilder;";
1194+
} else if (cl == Float.TYPE) {
1195+
return "(F)Ljava/lang/StringBuilder;";
1196+
} else if (cl == Long.TYPE) {
1197+
return "(J)Ljava/lang/StringBuilder;";
1198+
} else {
1199+
throw new IllegalStateException("Unhandled primitive StringBuilder.append: " + cl);
1200+
}
1201+
} else if (cl == String.class) {
1202+
return "(Ljava/lang/String;)Ljava/lang/StringBuilder;";
1203+
} else {
1204+
return "(Ljava/lang/Object;)Ljava/lang/StringBuilder;";
1205+
}
1206+
}
1207+
1208+
private static String getStringValueOfDesc(Class<?> cl) {
1209+
if (cl.isPrimitive()) {
1210+
if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) {
1211+
return "(I)Ljava/lang/String;";
1212+
} else if (cl == Boolean.TYPE) {
1213+
return "(Z)Ljava/lang/String;";
1214+
} else if (cl == Character.TYPE) {
1215+
return "(C)Ljava/lang/String;";
1216+
} else if (cl == Double.TYPE) {
1217+
return "(D)Ljava/lang/String;";
1218+
} else if (cl == Float.TYPE) {
1219+
return "(F)Ljava/lang/String;";
1220+
} else if (cl == Long.TYPE) {
1221+
return "(J)Ljava/lang/String;";
1222+
} else {
1223+
throw new IllegalStateException("Unhandled String.valueOf: " + cl);
1224+
}
1225+
} else if (cl == String.class) {
1226+
return "(Ljava/lang/String;)Ljava/lang/String;";
1227+
} else {
1228+
return "(Ljava/lang/Object;)Ljava/lang/String;";
1229+
}
1230+
}
1231+
1232+
/**
1233+
* The following method is copied from
1234+
* org.objectweb.asm.commons.InstructionAdapter. Part of ASM: a very small
1235+
* and fast Java bytecode manipulation framework.
1236+
* Copyright (c) 2000-2005 INRIA, France Telecom All rights reserved.
1237+
*/
1238+
private static void iconst(MethodVisitor mv, final int cst) {
1239+
if (cst >= -1 && cst <= 5) {
1240+
mv.visitInsn(ICONST_0 + cst);
1241+
} else if (cst >= Byte.MIN_VALUE && cst <= Byte.MAX_VALUE) {
1242+
mv.visitIntInsn(BIPUSH, cst);
1243+
} else if (cst >= Short.MIN_VALUE && cst <= Short.MAX_VALUE) {
1244+
mv.visitIntInsn(SIPUSH, cst);
1245+
} else {
1246+
mv.visitLdcInsn(cst);
1247+
}
1248+
}
1249+
1250+
private static int getLoadOpcode(Class<?> c) {
1251+
if (c == Void.TYPE) {
1252+
throw new InternalError("Unexpected void type of load opcode");
1253+
}
1254+
return ILOAD + getOpcodeOffset(c);
1255+
}
1256+
1257+
private static int getOpcodeOffset(Class<?> c) {
1258+
if (c.isPrimitive()) {
1259+
if (c == Long.TYPE) {
1260+
return 1;
1261+
} else if (c == Float.TYPE) {
1262+
return 2;
1263+
} else if (c == Double.TYPE) {
1264+
return 3;
1265+
}
1266+
return 0;
1267+
} else {
1268+
return 4;
1269+
}
1270+
}
1271+
1272+
private static int getParameterSize(Class<?> c) {
1273+
if (c == Void.TYPE) {
1274+
return 0;
1275+
} else if (c == Long.TYPE || c == Double.TYPE) {
1276+
return 2;
1277+
}
1278+
return 1;
1279+
}
1280+
}
10351281
}

test/micro/org/openjdk/bench/java/lang/StringConcat.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,47 @@ public String concatConst6Object() {
149149
return "string" + objectValue + objectValue + objectValue + objectValue + objectValue + objectValue;
150150
}
151151

152+
private String
153+
f0="1", f1="1", f2="1", f3="1", f4="1", f5="1", f6="1", f7="1", f8="1", f9="1",
154+
f10="1", f11="1", f12="1", f13="1", f14="1", f15="1", f16="1", f17="1", f18="1", f19="1",
155+
f20="1", f21="1", f22="1", f23="1", f24="1", f25="1", f26="1", f27="1", f28="1", f29="1",
156+
f30="1", f31="1", f32="1", f33="1", f34="1", f35="1", f36="1", f37="1", f38="1", f39="1",
157+
f40="1", f41="1", f42="1", f43="1", f44="1", f45="1", f46="1", f47="1", f48="1", f49="1",
158+
f50="1", f51="1", f52="1", f53="1", f54="1", f55="1", f56="1", f57="1", f58="1", f59="1",
159+
f60="1", f61="1", f62="1", f63="1", f64="1", f65="1", f66="1", f67="1", f68="1", f69="1",
160+
f70="1", f71="1", f72="1", f73="1", f74="1", f75="1", f76="1", f77="1", f78="1", f79="1",
161+
f80="1", f81="1", f82="1", f83="1", f84="1", f85="1", f86="1", f87="1", f88="1", f89="1",
162+
f90="1", f91="1", f92="1", f93="1", f94="1", f95="1", f96="1", f97="1", f98="1", f99="1",
163+
f100="1",f101="1",f102="1",f103="1",f104="1",f105="1",f106="1",f107="1",f108="1",f109="1",
164+
f110="1",f111="1",f112="1",f113="1",f114="1",f115="1",f116="1",f117="1",f118="1",f119="1",
165+
f120="1",f121="1",f122="1";
166+
167+
@Benchmark
168+
public String concat13String() {
169+
return f0 + ","+ f1 + ","+ f2 + ","+ f3 + ","+ f4 + ","+ f5 + ","+ f6 + ","+ f7 + ","+ f8 + ","+ f9 + ","
170+
+ f10 + ","+ f11 + ","+ f12;
171+
}
172+
173+
@Benchmark
174+
public String concat23String() {
175+
return f0 + ","+ f1 + ","+ f2 + ","+ f3 + ","+ f4 + ","+ f5 + ","+ f6 + ","+ f7 + ","+ f8 + ","+ f9 + ","
176+
+ f10 + ","+ f11 + ","+ f12 + ","+ f13 + ","+ f14 + ","+ f15 + ","+ f16 + ","+ f17 + ","+ f18 + ","+ f19 + ","
177+
+ f20 + ","+ f21 + ","+ f22;
178+
}
179+
@Benchmark
180+
public String concat123String() {
181+
return f0 + ","+ f1 + ","+ f2 + ","+ f3 + ","+ f4 + ","+ f5 + ","+ f6 + ","+ f7 + ","+ f8 + ","+ f9 + ","
182+
+ f10 + ","+ f11 + ","+ f12 + ","+ f13 + ","+ f14 + ","+ f15 + ","+ f16 + ","+ f17 + ","+ f18 + ","+ f19 + ","
183+
+ f20 + ","+ f21 + ","+ f22 + ","+ f23 + ","+ f24 + ","+ f25 + ","+ f26 + ","+ f27 + ","+ f28 + ","+ f29 + ","
184+
+ f30 + ","+ f31 + ","+ f32 + ","+ f33 + ","+ f34 + ","+ f35 + ","+ f36 + ","+ f37 + ","+ f38 + ","+ f39 + ","
185+
+ f40 + ","+ f41 + ","+ f42 + ","+ f43 + ","+ f44 + ","+ f45 + ","+ f46 + ","+ f47 + ","+ f48 + ","+ f49 + ","
186+
+ f50 + ","+ f51 + ","+ f52 + ","+ f53 + ","+ f54 + ","+ f55 + ","+ f56 + ","+ f57 + ","+ f58 + ","+ f59 + ","
187+
+ f60 + ","+ f61 + ","+ f62 + ","+ f63 + ","+ f64 + ","+ f65 + ","+ f66 + ","+ f67 + ","+ f68 + ","+ f69 + ","
188+
+ f70 + ","+ f71 + ","+ f72 + ","+ f73 + ","+ f74 + ","+ f75 + ","+ f76 + ","+ f77 + ","+ f78 + ","+ f79 + ","
189+
+ f80 + ","+ f81 + ","+ f82 + ","+ f83 + ","+ f84 + ","+ f85 + ","+ f86 + ","+ f87 + ","+ f88 + ","+ f89 + ","
190+
+ f90 + ","+ f91 + ","+ f92 + ","+ f93 + ","+ f94 + ","+ f95 + ","+ f96 + ","+ f97 + ","+ f98 + ","+ f99 + ","
191+
+f100 + ","+f101 + ","+f102 + ","+f103 + ","+f104 + ","+f105 + ","+f106 + ","+f107 + ","+f108 + ","+f109 + ","
192+
+f110 + ","+f111 + ","+f112 + ","+f113 + ","+f114 + ","+f115 + ","+f116 + ","+f117 + ","+f118 + ","+f119 + ","
193+
+f120 + ","+f121 + ","+f122;
194+
}
152195
}

0 commit comments

Comments
 (0)